| @@ -6,19 +6,29 @@ | ||
| 6 | 6 | */ |
| 7 | 7 | |
| 8 | 8 | namespace SRFM\Admin; |
| 9 | 9 | |
| 10 | -use SRFM\Inc\Traits\Get_Instance; | |
| 10 | +use Astra_Notices; | |
| 11 | 11 | use SRFM\Inc\AI_Form_Builder\AI_Helper; |
| 12 | +use SRFM\Inc\Client_Logger; | |
| 13 | +use SRFM\Inc\Database\Register; | |
| 14 | +use SRFM\Inc\Database\Tables\Entries; | |
| 15 | +use SRFM\Inc\Generate_Form_Markup; | |
| 16 | +use SRFM\Inc\Global_Settings\Global_Settings; | |
| 12 | 17 | use SRFM\Inc\Helper; |
| 13 | -use SRFM\Admin\Views\Single_Entry; | |
| 14 | -use SRFM\Admin\Views\Entries_List_Table; | |
| 15 | -use SRFM\Inc\Post_Types; | |
| 16 | -use SRFM\Inc\Database\Tables\Entries; | |
| 18 | +use SRFM\Inc\Onboarding; | |
| 19 | +use SRFM\Inc\Payments\Payment_Helper; | |
| 20 | +use SRFM\Inc\Payments\Stripe\Stripe_Helper; | |
| 21 | +use SRFM\Inc\Smart_Tags; | |
| 22 | +use SRFM\Inc\Traits\Get_Instance; | |
| 17 | 23 | |
| 18 | 24 | if ( ! defined( 'ABSPATH' ) ) { |
| 19 | 25 | exit; // Exit if accessed directly. |
| 20 | 26 | } |
| 27 | + | |
| 28 | +if ( ! class_exists( 'BSF_Admin_Notices' ) ) { | |
| 29 | + require_once SRFM_DIR . 'inc/lib/astra-notices/class-bsf-admin-notices.php'; | |
| 30 | +} | |
| 21 | 31 | /** |
| 22 | 32 | * Admin handler class. |
| 23 | 33 | * |
| 24 | 34 | * @since 0.0.1 |
| @@ -23,12 +33,141 @@ | ||
| 23 | 33 | * |
| 24 | 34 | * @since 0.0.1 |
| 25 | 35 | */ |
| 26 | 36 | class Admin { |
| 37 | + use Get_Instance; | |
| 27 | 38 | |
| 28 | - use Get_Instance; | |
| 39 | + /** | |
| 40 | + * Minimum number of forms or entries required to show the rating notice. | |
| 41 | + * | |
| 42 | + * @since 2.5.2 | |
| 43 | + */ | |
| 44 | + public const RATING_NOTICE_THRESHOLD = 3; | |
| 29 | 45 | |
| 30 | 46 | /** |
| 47 | + * Post meta the Starter Templates (Astra Sites) plugin stamps on every post it | |
| 48 | + * imports. The "Finish setting up" Thank You prompt (#3030) scopes to these | |
| 49 | + * forms only. Owned by a plugin that is NOT a SureForms dependency: on installs | |
| 50 | + * without Starter Templates nothing carries this meta and the prompt never shows. | |
| 51 | + * | |
| 52 | + * @since 2.12.4 | |
| 53 | + */ | |
| 54 | + public const ASTRA_SITES_IMPORT_META = '_astra_sites_imported_post'; | |
| 55 | + | |
| 56 | + /** | |
| 57 | + * Negative-cache transient: no form on this site carries the import marker. | |
| 58 | + * | |
| 59 | + * Set only when the marker query itself returns zero posts, which is a | |
| 60 | + * site-wide fact rather than a per-user one, and cleared as soon as any post is | |
| 61 | + * stamped with the marker (see invalidate_starter_template_cache()). This keeps | |
| 62 | + * the query off the majority of installs without tying the features to whether | |
| 63 | + * Starter Templates happens to still be active — the marker outlives it. | |
| 64 | + * | |
| 65 | + * @since 2.12.4 | |
| 66 | + */ | |
| 67 | + public const NO_IMPORTED_FORMS_TRANSIENT = 'srfm_no_starter_template_forms'; | |
| 68 | + | |
| 69 | + /** | |
| 70 | + * Inline CSS for Quill 1.x (react-quill) list markers. | |
| 71 | + * | |
| 72 | + * Quill 1.x renders bullet/numbered list markers via CSS ::before pseudo-elements, | |
| 73 | + * whereas the vendor quill.snow.css targets .ql-ui child elements (Quill 2.x approach). | |
| 74 | + * This constant is shared by enqueue_styles() and enqueue_scripts() to prevent drift. | |
| 75 | + * | |
| 76 | + * @since 2.5.2 | |
| 77 | + */ | |
| 78 | + public const QUILL_1X_INLINE_CSS = '.ql-editor ul,.ql-editor ol{padding-left:1.5em}.ql-editor ul>li,.ql-editor ol>li{list-style-type:none}.ql-editor ol li:not(.ql-direction-rtl),.ql-editor ul li:not(.ql-direction-rtl){padding-left:1.5em}.ql-editor ol li.ql-direction-rtl,.ql-editor ul li.ql-direction-rtl{padding-right:1.5em}.ql-editor ul>li::before{content:"\2022"}.ql-editor li::before{display:inline-block;white-space:nowrap;width:1.2em}.ql-editor li:not(.ql-direction-rtl)::before{margin-left:-1.5em;margin-right:.3em;text-align:right}.ql-editor li.ql-direction-rtl::before{margin-left:.3em;margin-right:-1.5em}.ql-editor ol li{counter-reset:list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9;counter-increment:list-0}.ql-editor ol li::before{content:counter(list-0,decimal) ". "}.ql-editor ol li.ql-indent-1{counter-increment:list-1;counter-reset:list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-1::before{content:counter(list-1,lower-alpha) ". "}.ql-editor ol li.ql-indent-2{counter-increment:list-2;counter-reset:list-3 list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-2::before{content:counter(list-2,lower-roman) ". "}.ql-editor ol li.ql-indent-3{counter-increment:list-3;counter-reset:list-4 list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-3::before{content:counter(list-3,decimal) ". "}.ql-editor ol li.ql-indent-4{counter-increment:list-4;counter-reset:list-5 list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-4::before{content:counter(list-4,lower-alpha) ". "}.ql-editor ol li.ql-indent-5{counter-increment:list-5;counter-reset:list-6 list-7 list-8 list-9}.ql-editor ol li.ql-indent-5::before{content:counter(list-5,lower-roman) ". "}.ql-editor ol li.ql-indent-6{counter-increment:list-6;counter-reset:list-7 list-8 list-9}.ql-editor ol li.ql-indent-6::before{content:counter(list-6,decimal) ". "}.ql-editor ol li.ql-indent-7{counter-increment:list-7;counter-reset:list-8 list-9}.ql-editor ol li.ql-indent-7::before{content:counter(list-7,lower-alpha) ". "}.ql-editor ol li.ql-indent-8{counter-increment:list-8;counter-reset:list-9}.ql-editor ol li.ql-indent-8::before{content:counter(list-8,lower-roman) ". "}.ql-editor ol li.ql-indent-9{counter-increment:list-9}.ql-editor ol li.ql-indent-9::before{content:counter(list-9,decimal) ". "}'; | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * Notice id for the "Finish setting up" Thank You prompt (#3030). | |
| 82 | + * | |
| 83 | + * A single stable id (not per-form): keeps both the autoloaded | |
| 84 | + * `allowed_astra_notices` option and the per-user dismissal meta bounded to one | |
| 85 | + * row, and lets a dismissed user short-circuit before the query runs. | |
| 86 | + * | |
| 87 | + * @since 2.12.6 | |
| 88 | + */ | |
| 89 | + public const THANKYOU_PROMPT_NOTICE_ID = 'srfm-thankyou-prompt'; | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * Where the dialog's Contact Support button goes. | |
| 93 | + * | |
| 94 | + * A form rather than an inbox: it collects the licence and site details support | |
| 95 | + * would otherwise have to ask for, and the diagnostics are already on the | |
| 96 | + * clipboard by the time someone gets here. | |
| 97 | + * | |
| 98 | + * @since 2.12.7 | |
| 99 | + */ | |
| 100 | + private const SUPPORT_CONTACT_URL = 'https://sureforms.com/form/troubleshooting-form/'; | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * Dashboard widget entries data. | |
| 104 | + * | |
| 105 | + * @var array | |
| 106 | + * @since 1.9.1 | |
| 107 | + */ | |
| 108 | + private $dashboard_widget_data = []; | |
| 109 | + | |
| 110 | + /** | |
| 111 | + * Cached result for whether the rating notice should display. | |
| 112 | + * | |
| 113 | + * @var bool|null | |
| 114 | + * @since 2.5.2 | |
| 115 | + */ | |
| 116 | + private $should_show_rating = null; | |
| 117 | + | |
| 118 | + /** | |
| 119 | + * SureForms Page Default permission. | |
| 120 | + * | |
| 121 | + * @var string | |
| 122 | + * @since 1.12.2 | |
| 123 | + */ | |
| 124 | + private static $sureforms_page_default_capability = 'manage_options'; | |
| 125 | + | |
| 126 | + /** | |
| 127 | + * Request memo for the "Finish setting up" Thank You prompt (#3030). | |
| 128 | + * | |
| 129 | + * A static property (not a function-local static) so tests can reset it via | |
| 130 | + * reflection / reset_thankyou_prompt_cache() — otherwise the first call pins | |
| 131 | + * the value for the whole process and the feature is untestable. | |
| 132 | + * | |
| 133 | + * @var array<int,array<string,mixed>>|null | |
| 134 | + * @since 2.12.4 | |
| 135 | + */ | |
| 136 | + private static $thankyou_prompt_cache = null; | |
| 137 | + | |
| 138 | + /** | |
| 139 | + * Request memo for the dashboard setup-checklist card (#3031). | |
| 140 | + * | |
| 141 | + * A static property (not a function-local static) so tests can reset it via | |
| 142 | + * reset_form_setup_card_cache() and exercise the populated path — a | |
| 143 | + * function-local static pins the first result for the whole process. Keyed by | |
| 144 | + * user id since the payload derives from that user's capabilities. | |
| 145 | + * `false` means "not computed yet"; `null`/array is a computed result. | |
| 146 | + * | |
| 147 | + * @var array<int,array<string,mixed>|null> | |
| 148 | + * @since 2.12.4 | |
| 149 | + */ | |
| 150 | + private static $setup_card_cache = []; | |
| 151 | + | |
| 152 | + /** | |
| 153 | + * Action items for this request, or null before the first build. | |
| 154 | + * | |
| 155 | + * Built twice on every admin page without this -- once for the localisation | |
| 156 | + * payload, once in the classic renderer -- and each open failure category reads | |
| 157 | + * a log excerpt. get_action_items() also records an impression, which running | |
| 158 | + * twice counted twice. | |
| 159 | + * | |
| 160 | + * Reset with reset_action_items_cache(). Admin is a singleton, so without that | |
| 161 | + * the first build pins the answer for the whole process and any test that | |
| 162 | + * records a failure and then asks again is testing the memo. | |
| 163 | + * | |
| 164 | + * @var array<int,array<string,mixed>>|null | |
| 165 | + * @since 2.12.7 | |
| 166 | + */ | |
| 167 | + private static $action_items_cache = null; | |
| 168 | + | |
| 169 | + /** | |
| 31 | 170 | * Class constructor. |
| 32 | 171 | * |
| 33 | 172 | * @return void |
| 34 | 173 | * @since 0.0.1 |
| @@ -36,108 +175,983 @@ | ||
| 36 | 175 | public function __construct() { |
| 37 | 176 | add_action( 'admin_menu', [ $this, 'add_menu_page' ], 9 ); |
| 38 | 177 | add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); |
| 39 | 178 | add_action( 'admin_menu', [ $this, 'settings_page' ] ); |
| 179 | + add_action( 'admin_menu', [ $this, 'add_learn_page' ] ); | |
| 40 | 180 | add_action( 'admin_menu', [ $this, 'add_new_form' ] ); |
| 181 | + add_action( 'admin_menu', [ $this, 'add_suremail_page' ] ); | |
| 182 | + if ( ! Helper::has_pro() ) { | |
| 183 | + add_action( 'admin_menu', [ $this, 'add_quiz_page' ] ); | |
| 184 | + add_action( 'admin_menu', [ $this, 'add_survey_reports_page' ] ); | |
| 185 | + add_action( 'admin_menu', [ $this, 'add_partial_entries_page' ] ); | |
| 186 | + add_action( 'admin_menu', [ $this, 'add_upgrade_to_pro' ] ); | |
| 187 | + add_action( 'admin_footer', [ $this, 'add_upgrade_to_pro_target_attr' ] ); | |
| 188 | + } | |
| 189 | + | |
| 41 | 190 | add_filter( 'plugin_action_links', [ $this, 'add_settings_link' ], 10, 2 ); |
| 42 | 191 | add_action( 'enqueue_block_assets', [ $this, 'enqueue_styles' ] ); |
| 43 | 192 | add_action( 'admin_head', [ $this, 'enqueue_header_styles' ] ); |
| 44 | - add_action( 'admin_body_class', [ $this, 'admin_template_picker_body_class' ] ); | |
| 193 | + add_filter( 'admin_body_class', [ $this, 'admin_template_picker_body_class' ] ); | |
| 45 | 194 | |
| 46 | 195 | // this action is used to restrict Spectra's quick action bar on SureForms CPTS. |
| 47 | 196 | add_action( 'uag_enable_quick_action_sidebar', [ $this, 'restrict_spectra_quick_action_bar' ] ); |
| 48 | 197 | |
| 49 | 198 | add_action( 'current_screen', [ $this, 'enable_gutenberg_for_sureforms' ], 100 ); |
| 199 | + // Register notices early for React pages (before admin_enqueue_scripts). | |
| 200 | + add_action( 'admin_init', [ $this, 'register_pro_compatibility_notices' ], 5 ); | |
| 50 | 201 | |
| 51 | - // Handle entry actions. | |
| 52 | - add_action( 'admin_init', [ $this, 'handle_entry_actions' ] ); | |
| 53 | - add_action( 'admin_notices', [ $this, 'entries_migration_notice' ] ); | |
| 54 | - add_action( 'admin_notices', [ Entries_List_Table::class, 'display_bulk_action_notice' ] ); | |
| 202 | + // Database maintenance notice: the entries table is missing, so submissions | |
| 203 | + // cannot be saved. Registered at admin_init priority 5 so the React notice is | |
| 204 | + // in place before admin_enqueue_scripts localizes it. | |
| 205 | + add_action( 'admin_init', [ $this, 'register_database_repair_notice' ], 5 ); | |
| 206 | + add_action( 'admin_notices', [ $this, 'render_action_item_notices' ] ); | |
| 207 | + // Late priority so the items are built after anything hooking | |
| 208 | + // srfm_action_items has had a chance to register. | |
| 209 | + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_action_item_styles' ], 20 ); | |
| 210 | + add_action( 'admin_notices', [ $this, 'render_database_repair_notice' ] ); | |
| 211 | + add_action( 'admin_post_srfm_repair_entries_table', [ $this, 'handle_database_repair' ] ); | |
| 212 | + // Display notices on traditional WordPress admin pages. | |
| 213 | + add_action( 'admin_notices', [ $this, 'srfm_pro_version_compatibility' ] ); | |
| 214 | + | |
| 215 | + // Enfold theme compatibility to enable block editor for SureForms post type. | |
| 216 | + add_filter( 'avf_use_block_editor_for_post', [ $this, 'enable_block_editor_in_enfold_theme' ] ); | |
| 217 | + | |
| 218 | + // Add action links to the plugin page. | |
| 219 | + add_filter( 'plugin_action_links_' . SRFM_BASENAME, [ $this, 'add_action_links' ] ); | |
| 220 | + // Check if admin notification is enabled and add entries badge. | |
| 221 | + $general_options = get_option( 'srfm_general_settings_options', [] ); | |
| 222 | + $admin_notification_on = isset( $general_options['srfm_admin_notification'] ) ? (bool) $general_options['srfm_admin_notification'] : true; | |
| 223 | + | |
| 224 | + if ( $admin_notification_on ) { | |
| 225 | + add_action( 'admin_menu', [ $this, 'maybe_add_entries_badge' ], 99 ); | |
| 226 | + } | |
| 227 | + | |
| 228 | + add_filter( 'wpforms_current_user_can', [ $this, 'disable_wpforms_capabilities' ], 10, 3 ); | |
| 229 | + | |
| 230 | + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_admin_pointer' ] ); | |
| 231 | + // Ajax callbacks for wp-pointer functionality. | |
| 232 | + add_action( 'wp_ajax_should_show_pointer', [ $this, 'pointer_should_show' ] ); | |
| 233 | + add_action( 'wp_ajax_sureforms_dismiss_pointer', [ $this, 'pointer_dismissed' ] ); | |
| 234 | + add_action( 'wp_ajax_sureforms_accept_cta', [ $this, 'pointer_accepted_cta' ] ); | |
| 235 | + add_action( 'wp_ajax_srfm_notice_response', [ $this, 'handle_notice_response' ] ); | |
| 236 | + add_action( 'wp_ajax_srfm_action_item_details', [ $this, 'handle_action_item_details' ] ); | |
| 237 | + add_action( 'wp_ajax_srfm_dismiss_action_item', [ $this, 'handle_dismiss_action_item' ] ); | |
| 238 | + add_action( 'admin_post_srfm_dismiss_action_item_link', [ $this, 'handle_dismiss_action_item_link' ] ); | |
| 239 | + add_action( 'wp_ajax_srfm_ai_widget_usage', [ $this, 'track_ai_widget_usage' ] ); | |
| 240 | + add_action( 'load-post.php', [ $this, 'maybe_track_edit_form_button_click' ] ); | |
| 241 | + add_filter( 'removable_query_args', [ $this, 'add_removable_query_args' ] ); | |
| 242 | + | |
| 243 | + // Register dashboard widget only if there are recent entries. | |
| 244 | + add_action( 'admin_init', [ $this, 'maybe_register_dashboard_widget' ] ); | |
| 245 | + | |
| 246 | + // Enqueue the AI quick draft widget script on the dashboard screen. | |
| 247 | + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_ai_dashboard_widget_assets' ] ); | |
| 248 | + | |
| 249 | + // "Finish setting up" checklist widget on the main WP dashboard (#3031). | |
| 250 | + add_action( 'wp_dashboard_setup', [ $this, 'register_form_setup_widget' ] ); | |
| 251 | + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_form_setup_widget_assets' ] ); | |
| 252 | + | |
| 253 | + // Drop the "no imported forms" negative cache as soon as a post is stamped | |
| 254 | + // with the import marker, so a template imported after the cache was written | |
| 255 | + // surfaces immediately instead of waiting for the transient to expire. | |
| 256 | + add_action( 'added_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 ); | |
| 257 | + add_action( 'updated_post_meta', [ $this, 'invalidate_starter_template_cache' ], 10, 3 ); | |
| 258 | + | |
| 259 | + // Save first form creation time stamp. | |
| 260 | + add_action( 'admin_init', [ $this, 'save_first_form_creation_time_stamp' ] ); | |
| 261 | + add_action( 'admin_notices', [ $this, 'display_srfm_rating_notice' ] ); | |
| 262 | + add_action( 'admin_notices', [ $this, 'display_srfm_getting_started_notice' ] ); | |
| 263 | + | |
| 264 | + // "Finish setting up" prompt, shown as an Astra Notices admin notice on | |
| 265 | + // every admin screen except the dashboard (#3030). | |
| 266 | + add_action( 'admin_notices', [ $this, 'render_thankyou_prompt_notice' ] ); | |
| 267 | + | |
| 268 | + /** | |
| 269 | + * Suppress foreign (third-party) admin notices on SureForms admin screens. | |
| 270 | + * | |
| 271 | + * Some plugins (e.g. Ninja Forms) print large promotional banners on every | |
| 272 | + * admin page via the admin_notices / all_admin_notices / network_admin_notices | |
| 273 | + * hooks. These bleed onto SureForms' own React admin screens and break the UI. | |
| 274 | + * We run at the EARLIEST priority on each notice hook (all third-party | |
| 275 | + * callbacks are registered before these hooks fire, during admin_init / | |
| 276 | + * plugin load) and strip the foreign ones before they are echoed, while | |
| 277 | + * preserving SureForms' own notices. Scoped strictly to SureForms screens. | |
| 278 | + */ | |
| 279 | + add_action( 'admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN ); | |
| 280 | + add_action( 'all_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN ); | |
| 281 | + add_action( 'network_admin_notices', [ $this, 'suppress_foreign_admin_notices' ], PHP_INT_MIN ); | |
| 55 | 282 | } |
| 56 | 283 | |
| 57 | 284 | /** |
| 58 | - * Print notice to inform users about entries database migration. | |
| 285 | + * Remove third-party admin notices on SureForms admin screens. | |
| 59 | 286 | * |
| 60 | - * @since 0.0.13 | |
| 287 | + * Iterates over the callbacks registered on the admin notice hooks and | |
| 288 | + * removes any that are not owned by SureForms. A callback is considered | |
| 289 | + * owned by SureForms when it belongs to a class in the `SRFM` / `SRFM_PRO` | |
| 290 | + * namespaces or to the bundled `BSF_Admin_Notices` / `Astra_Notices` | |
| 291 | + * notices library. SureForms' own notices are therefore preserved while | |
| 292 | + * foreign promotional banners are suppressed. | |
| 293 | + * | |
| 294 | + * This callback is hooked at `PHP_INT_MIN` so that it runs first on each | |
| 295 | + * notice hook and removes the foreign callbacks before WordPress echoes | |
| 296 | + * them (WP_Hook honours removals made during iteration). It is strictly | |
| 297 | + * scoped to SureForms admin screens via {@see Helper::is_sureforms_admin_page()} | |
| 298 | + * so no other admin page is affected. | |
| 299 | + * | |
| 300 | + * @since 2.10.0 | |
| 61 | 301 | * @return void |
| 62 | 302 | */ |
| 63 | - public function entries_migration_notice() { | |
| 64 | - if ( get_option( 'srfm_dismiss_entries_migration_notice', false ) ) { | |
| 303 | + public function suppress_foreign_admin_notices() { | |
| 304 | + // Bail early if we are not on a SureForms admin screen. This keeps the | |
| 305 | + // suppression strictly scoped and avoids touching any other admin page. | |
| 306 | + // is_sureforms_admin_page() covers the core screens (dashboard, add-new, | |
| 307 | + // settings, entries, the form CPT); we additionally match any admin page | |
| 308 | + // whose `page` slug is SureForms-owned (sureforms_* / srfm_*) so the | |
| 309 | + // suppression also applies to the payments/quiz/survey/learn/SMTP screens. | |
| 310 | + if ( ! Helper::is_sureforms_admin_page() && ! $this->is_sureforms_owned_admin_page() ) { | |
| 65 | 311 | return; |
| 66 | 312 | } |
| 67 | 313 | |
| 68 | - if ( empty( get_posts( [ 'post_type' => SRFM_ENTRIES_POST_TYPE ] ) ) ) { | |
| 69 | - // Bail if we don't have legacy post type entries. | |
| 314 | + global $wp_filter; | |
| 315 | + | |
| 316 | + // The hook currently being fired (admin_notices, all_admin_notices or network_admin_notices). | |
| 317 | + $current_hook = current_action(); | |
| 318 | + | |
| 319 | + if ( empty( $current_hook ) || empty( $wp_filter[ $current_hook ] ) || ! ( $wp_filter[ $current_hook ] instanceof \WP_Hook ) ) { | |
| 70 | 320 | return; |
| 71 | 321 | } |
| 72 | 322 | |
| 73 | - $ajaxurl = add_query_arg( | |
| 323 | + foreach ( $wp_filter[ $current_hook ]->callbacks as $priority => $callbacks ) { | |
| 324 | + foreach ( $callbacks as $callback ) { | |
| 325 | + $function = $callback['function'] ?? null; | |
| 326 | + | |
| 327 | + // Never remove our own suppression callback. | |
| 328 | + if ( is_array( $function ) && isset( $function[0] ) && $function[0] === $this && 'suppress_foreign_admin_notices' === $function[1] ) { | |
| 329 | + continue; | |
| 330 | + } | |
| 331 | + | |
| 332 | + // Preserve SureForms-owned notices, remove everything else. | |
| 333 | + if ( $this->is_sureforms_owned_notice_callback( $function ) ) { | |
| 334 | + continue; | |
| 335 | + } | |
| 336 | + | |
| 337 | + remove_action( $current_hook, $function, $priority ); | |
| 338 | + } | |
| 339 | + } | |
| 340 | + } | |
| 341 | + | |
| 342 | + /** | |
| 343 | + * Get the first form creation time stamp. | |
| 344 | + * | |
| 345 | + * @since 1.10.1 | |
| 346 | + * @return int|false | |
| 347 | + */ | |
| 348 | + public static function get_first_form_creation_time_stamp() { | |
| 349 | + return Helper::get_srfm_option( 'first_form_created_at', false ); | |
| 350 | + } | |
| 351 | + | |
| 352 | + /** | |
| 353 | + * Check if the first form has been created. | |
| 354 | + * | |
| 355 | + * @since 1.10.1 | |
| 356 | + * @return bool | |
| 357 | + */ | |
| 358 | + public static function is_first_form_created() { | |
| 359 | + // Convert the first form creation time stamp to a boolean. If it exists, it will return true, otherwise false. | |
| 360 | + $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp(); | |
| 361 | + | |
| 362 | + // If the first form creation time stamp is not set, return false. | |
| 363 | + if ( ! $first_form_creation_time_stamp ) { | |
| 364 | + return false; // No forms created yet. | |
| 365 | + } | |
| 366 | + | |
| 367 | + // Check if the first form creation time stamp is a valid integer and greater than zero. | |
| 368 | + return is_int( $first_form_creation_time_stamp ) && $first_form_creation_time_stamp > 0; | |
| 369 | + } | |
| 370 | + | |
| 371 | + /** | |
| 372 | + * Whether a form's confirmation message is still the shipped default. | |
| 373 | + * | |
| 374 | + * Compared on tag-stripped, entity-decoded, whitespace-collapsed text rather | |
| 375 | + * than raw HTML: the default is stored with a base64 icon on creation but | |
| 376 | + * regenerated with a URL icon, so the markup differs while the wording does | |
| 377 | + * not, and a starter-template import can store a literal apostrophe where the | |
| 378 | + * generated default carries the encoded `'` — decoding entities makes both | |
| 379 | + * compare equal. Any real edit to the heading or body text changes the text and | |
| 380 | + * flips this to false, which is exactly when the prompt should stop showing. | |
| 381 | + * | |
| 382 | + * Locale caveat: the comparison target is translated at call time, so a form | |
| 383 | + * whose default was stored under a different active locale won't match. That | |
| 384 | + * fails safe — the prompt simply doesn't show — never a false nag. | |
| 385 | + * | |
| 386 | + * @param int $form_id Form post ID. | |
| 387 | + * | |
| 388 | + * @since 2.12.4 | |
| 389 | + * @return bool | |
| 390 | + */ | |
| 391 | + public static function is_default_confirmation_message( $form_id ) { | |
| 392 | + $confirmation = get_post_meta( (int) $form_id, '_srfm_form_confirmation', true ); | |
| 393 | + | |
| 394 | + if ( ! is_array( $confirmation ) || ! isset( $confirmation[0]['message'] ) || ! is_string( $confirmation[0]['message'] ) ) { | |
| 395 | + return false; | |
| 396 | + } | |
| 397 | + | |
| 398 | + // The default message is only ever shown for a "same page" confirmation. | |
| 399 | + // A redirect ("different page" / "custom url") never renders it, yet the | |
| 400 | + // stored settings still seed the default message string — so without this | |
| 401 | + // guard a redirect form would be nagged forever about a message no visitor | |
| 402 | + // sees, with no way to clear the prompt by doing what it asks. | |
| 403 | + if ( ! isset( $confirmation[0]['confirmation_type'] ) || 'same page' !== $confirmation[0]['confirmation_type'] ) { | |
| 404 | + return false; | |
| 405 | + } | |
| 406 | + | |
| 407 | + $message = $confirmation[0]['message']; | |
| 408 | + | |
| 409 | + if ( '' === trim( $message ) ) { | |
| 410 | + return false; | |
| 411 | + } | |
| 412 | + | |
| 413 | + $normalize = static function ( $html ) { | |
| 414 | + // Decode entities too, so an encoded apostrophe (') in the generated | |
| 415 | + // default matches a literal one stored by a template import. | |
| 416 | + $text = html_entity_decode( wp_strip_all_tags( (string) $html ), ENT_QUOTES, 'UTF-8' ); | |
| 417 | + return trim( (string) preg_replace( '/\s+/', ' ', $text ) ); | |
| 418 | + }; | |
| 419 | + | |
| 420 | + return $normalize( $message ) === $normalize( Global_Settings::get_default_confirmation_message() ); | |
| 421 | + } | |
| 422 | + | |
| 423 | + /** | |
| 424 | + * Whether a form has somewhere to send replies (an enabled email notification | |
| 425 | + * with a non-empty recipient). | |
| 426 | + * | |
| 427 | + * @param int $form_id Form post ID. | |
| 428 | + * | |
| 429 | + * @since 2.12.4 | |
| 430 | + * @return bool | |
| 431 | + */ | |
| 432 | + public static function form_has_reply_destination( $form_id ) { | |
| 433 | + $notifications = get_post_meta( (int) $form_id, '_srfm_email_notification', true ); | |
| 434 | + | |
| 435 | + if ( ! is_array( $notifications ) ) { | |
| 436 | + return false; | |
| 437 | + } | |
| 438 | + | |
| 439 | + foreach ( $notifications as $notification ) { | |
| 440 | + if ( is_array( $notification ) && ! empty( $notification['status'] ) && ! empty( $notification['email_to'] ) ) { | |
| 441 | + return true; | |
| 442 | + } | |
| 443 | + } | |
| 444 | + | |
| 445 | + return false; | |
| 446 | + } | |
| 447 | + | |
| 448 | + /** | |
| 449 | + * The most recently created form still needing setup (default Thank You | |
| 450 | + * message, or no reply destination). | |
| 451 | + * | |
| 452 | + * Powers the "Finish setting up" prompt (#3030). Limited to the single latest | |
| 453 | + * such form to avoid clutter, and to forms the current user may actually edit. | |
| 454 | + * A form is a candidate when it still has an unfinished step (default Thank You | |
| 455 | + * message, or no reply destination). Dismissal is enforced by the caller, | |
| 456 | + * before this query runs. | |
| 457 | + * | |
| 458 | + * @since 2.12.4 | |
| 459 | + * @return array<int,array<string,mixed>> One entry, or none. | |
| 460 | + */ | |
| 461 | + public static function get_thankyou_prompt_forms() { | |
| 462 | + // Memoized for the request so repeated reads (e.g. the notice render plus | |
| 463 | + // any add-on consumer) share a single query. Sentinel is null, not false, | |
| 464 | + // so a filter returning false (__return_false to disable) still memoizes. | |
| 465 | + if ( null !== self::$thankyou_prompt_cache ) { | |
| 466 | + return self::$thankyou_prompt_cache; | |
| 467 | + } | |
| 468 | + | |
| 469 | + /** | |
| 470 | + * Filter the forms the "Finish setting up" Thank You notice may surface. | |
| 471 | + * | |
| 472 | + * @param array<int,array<string,mixed>> $prompts Candidate prompt payloads. | |
| 473 | + * | |
| 474 | + * @since 2.12.4 | |
| 475 | + */ | |
| 476 | + $filtered = apply_filters( 'srfm_thankyou_prompt_forms', self::compute_thankyou_prompt_forms() ); | |
| 477 | + self::$thankyou_prompt_cache = is_array( $filtered ) ? $filtered : []; | |
| 478 | + | |
| 479 | + return self::$thankyou_prompt_cache; | |
| 480 | + } | |
| 481 | + | |
| 482 | + /** | |
| 483 | + * Clear the request memo for the action items. | |
| 484 | + * | |
| 485 | + * Admin is a singleton, so the memo outlives a request in a test process. | |
| 486 | + * Anything that records or clears a failure inside one process has to call | |
| 487 | + * this, or it reads the answer from before the change. | |
| 488 | + * | |
| 489 | + * @since 2.12.7 | |
| 490 | + * @return void | |
| 491 | + */ | |
| 492 | + public static function reset_action_items_cache() { | |
| 493 | + self::$action_items_cache = null; | |
| 494 | + } | |
| 495 | + | |
| 496 | + /** | |
| 497 | + * Clear the request memo for the Thank You prompt (#3030). | |
| 498 | + * | |
| 499 | + * Lets tests exercise the memoized public path, and is a safe hook for anything | |
| 500 | + * that changes which form qualifies (e.g. a form save). | |
| 501 | + * | |
| 502 | + * @since 2.12.4 | |
| 503 | + * @return void | |
| 504 | + */ | |
| 505 | + public static function reset_thankyou_prompt_cache() { | |
| 506 | + self::$thankyou_prompt_cache = null; | |
| 507 | + } | |
| 508 | + | |
| 509 | + /** | |
| 510 | + * Setup-checklist data for the newest starter-template form (#3031). | |
| 511 | + * | |
| 512 | + * Picks the most recent form the current user can edit that was created from an | |
| 513 | + * Astra Sites starter template. The widget | |
| 514 | + * lists a fixed set of optional next-steps for it — their completion is not | |
| 515 | + * computed — so the payload carries only the form and the CTA targets. Memoized | |
| 516 | + * for the request so the widget register/enqueue/render passes share one query. | |
| 517 | + * | |
| 518 | + * @since 2.12.4 | |
| 519 | + * @return array<string,mixed>|null Card payload, or null when there is no candidate form. | |
| 520 | + */ | |
| 521 | + public static function get_form_setup_card() { | |
| 522 | + $user_id = get_current_user_id(); | |
| 523 | + | |
| 524 | + // Request memo, keyed per user — the payload derives from that user's | |
| 525 | + // capabilities. Reset via reset_form_setup_card_cache(). | |
| 526 | + if ( array_key_exists( $user_id, self::$setup_card_cache ) ) { | |
| 527 | + return self::$setup_card_cache[ $user_id ]; | |
| 528 | + } | |
| 529 | + | |
| 530 | + self::$setup_card_cache[ $user_id ] = self::compute_form_setup_card(); | |
| 531 | + | |
| 532 | + return self::$setup_card_cache[ $user_id ]; | |
| 533 | + } | |
| 534 | + | |
| 535 | + /** | |
| 536 | + * Drop the "no imported forms" negative cache when the marker is written. | |
| 537 | + * | |
| 538 | + * Hooked to added_post_meta/updated_post_meta. Without this, a starter template | |
| 539 | + * imported after the negative cache was written would show neither the Thank You | |
| 540 | + * prompt nor the setup widget until the transient expired. | |
| 541 | + * | |
| 542 | + * Arguments are read from func_get_args() rather than declared: the hook passes | |
| 543 | + * ( $meta_id, $post_id, $meta_key ) and the meta id is never needed, so declaring | |
| 544 | + * it would leave an unused parameter that the coding-standards gate rejects. | |
| 545 | + * | |
| 546 | + * @since 2.12.4 | |
| 547 | + * @return void | |
| 548 | + */ | |
| 549 | + public function invalidate_starter_template_cache() { | |
| 550 | + $args = func_get_args(); | |
| 551 | + $post_id = isset( $args[1] ) ? (int) $args[1] : 0; | |
| 552 | + $meta_key = isset( $args[2] ) ? (string) $args[2] : ''; | |
| 553 | + | |
| 554 | + if ( self::ASTRA_SITES_IMPORT_META !== $meta_key ) { | |
| 555 | + return; | |
| 556 | + } | |
| 557 | + | |
| 558 | + // A full-site import stamps this marker on every post it creates, so narrow to | |
| 559 | + // our own post type: both features only ever query sureforms_form, and this | |
| 560 | + // avoids clearing the cache repeatedly for pages and products during an import. | |
| 561 | + if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $post_id ) ) { | |
| 562 | + return; | |
| 563 | + } | |
| 564 | + | |
| 565 | + delete_transient( self::NO_IMPORTED_FORMS_TRANSIENT ); | |
| 566 | + } | |
| 567 | + | |
| 568 | + /** | |
| 569 | + * Clear the setup-card request memo (#3031). | |
| 570 | + * | |
| 571 | + * Lets tests exercise the populated path, and is a safe hook for anything that | |
| 572 | + * changes which form qualifies (e.g. a form save). | |
| 573 | + * | |
| 574 | + * @since 2.12.4 | |
| 575 | + * @return void | |
| 576 | + */ | |
| 577 | + public static function reset_form_setup_card_cache() { | |
| 578 | + self::$setup_card_cache = []; | |
| 579 | + } | |
| 580 | + | |
| 581 | + /** | |
| 582 | + * REST handler: record a "Finish setting up" widget interaction (#3031). | |
| 583 | + * | |
| 584 | + * Records the clicked CTA/view action as an analytics event. The request | |
| 585 | + * carries the displayed form id: capability is re-checked against it here — | |
| 586 | + * beyond the route's generic permission callback — so only a genuine editor of | |
| 587 | + * that form can act. | |
| 588 | + * | |
| 589 | + * @param \WP_REST_Request<array<string,mixed>> $request Request. | |
| 590 | + * | |
| 591 | + * @since 2.12.4 | |
| 592 | + * @return \WP_REST_Response|\WP_Error | |
| 593 | + */ | |
| 594 | + public function dismiss_form_setup_card( $request ) { | |
| 595 | + $form_id = absint( $request->get_param( 'form_id' ) ); | |
| 596 | + | |
| 597 | + if ( $form_id <= 0 || ! defined( 'SRFM_FORMS_POST_TYPE' ) || SRFM_FORMS_POST_TYPE !== get_post_type( $form_id ) || ! current_user_can( 'edit_post', $form_id ) ) { | |
| 598 | + return new \WP_Error( 'srfm_setup_card_forbidden', __( 'You are not allowed to update this prompt.', 'sureforms' ), [ 'status' => 403 ] ); | |
| 599 | + } | |
| 600 | + | |
| 601 | + $action = sanitize_key( (string) $request->get_param( 'action' ) ); | |
| 602 | + | |
| 603 | + // Interaction analytics for the setup widget (#3031). Each event carries a | |
| 604 | + // date automatically (see BSF_Analytics_Events::track()) and dedupes per | |
| 605 | + // event name, matching the sibling notice's telemetry. | |
| 606 | + $events = [ | |
| 607 | + 'edit_form' => 'form_setup_widget_edit_form', | |
| 608 | + 'edit_thankyou' => 'form_setup_widget_edit_thankyou', | |
| 609 | + 'set_up_email' => 'form_setup_widget_set_up_email', | |
| 610 | + 'view_form' => 'form_setup_widget_view_form', | |
| 611 | + ]; | |
| 612 | + | |
| 613 | + if ( isset( $events[ $action ] ) ) { | |
| 614 | + Analytics::events()->track( $events[ $action ], (string) $form_id ); | |
| 615 | + } | |
| 616 | + | |
| 617 | + return new \WP_REST_Response( [ 'success' => true ], 200 ); | |
| 618 | + } | |
| 619 | + | |
| 620 | + /** | |
| 621 | + * Register the "Finish setting up" checklist widget on the main WP dashboard (#3031). | |
| 622 | + * | |
| 623 | + * Only for capable users, and only when there is a form still needing setup — | |
| 624 | + * so the widget never appears empty. The data is memoized in get_form_setup_card() | |
| 625 | + * and reused by the enqueue and render passes. | |
| 626 | + * | |
| 627 | + * @since 2.12.4 | |
| 628 | + * @return void | |
| 629 | + */ | |
| 630 | + public function register_form_setup_widget() { | |
| 631 | + if ( ! Helper::current_user_can() ) { | |
| 632 | + return; | |
| 633 | + } | |
| 634 | + | |
| 635 | + if ( null === self::get_form_setup_card() ) { | |
| 636 | + return; | |
| 637 | + } | |
| 638 | + | |
| 639 | + wp_add_dashboard_widget( | |
| 640 | + 'srfm_form_setup_checklist', | |
| 641 | + __( 'Finish setting up your form', 'sureforms' ), | |
| 642 | + [ $this, 'render_form_setup_widget' ], | |
| 643 | + null, | |
| 644 | + null, | |
| 645 | + 'normal', | |
| 646 | + 'high' | |
| 647 | + ); | |
| 648 | + } | |
| 649 | + | |
| 650 | + /** | |
| 651 | + * Render the setup-checklist widget content (#3031). | |
| 652 | + * | |
| 653 | + * A heading (with a link to view the form), a subtitle, and a fixed list of | |
| 654 | + * optional next-steps — each always shown with its CTA; completion is not | |
| 655 | + * computed. Each CTA deep-links into the editor (edit form / Thank You message / | |
| 656 | + * email notification) and records an analytics event via the REST endpoint | |
| 657 | + * wired in the enqueued inline script. | |
| 658 | + * | |
| 659 | + * @since 2.12.4 | |
| 660 | + * @return void | |
| 661 | + */ | |
| 662 | + public function render_form_setup_widget() { | |
| 663 | + $card = self::get_form_setup_card(); | |
| 664 | + | |
| 665 | + if ( null === $card ) { | |
| 666 | + return; | |
| 667 | + } | |
| 668 | + | |
| 669 | + // Optional next-steps — always offered, their completion is not computed. | |
| 670 | + // 'event' is the analytics action key beaconed on click (see the widget JS). | |
| 671 | + $rows = [ | |
| 74 | 672 | [ |
| 75 | - 'action' => 'sureforms_dismiss_plugin_notice', | |
| 76 | - 'security' => wp_create_nonce( 'srfm_notice_dismiss_nonce' ), | |
| 673 | + 'label' => __( 'Review or edit your form', 'sureforms' ), | |
| 674 | + 'cta' => __( 'Edit form', 'sureforms' ), | |
| 675 | + 'url' => $card['edit_url'], | |
| 676 | + 'event' => 'edit_form', | |
| 77 | 677 | ], |
| 78 | - admin_url( 'admin-ajax.php' ) | |
| 678 | + [ | |
| 679 | + 'label' => __( 'Personalize the Thank You message', 'sureforms' ), | |
| 680 | + 'cta' => __( 'Edit message', 'sureforms' ), | |
| 681 | + 'url' => $card['thankyou_url'], | |
| 682 | + 'event' => 'edit_thankyou', | |
| 683 | + ], | |
| 684 | + [ | |
| 685 | + 'label' => __( 'Choose who gets notified of new replies', 'sureforms' ), | |
| 686 | + 'cta' => __( 'Set up email', 'sureforms' ), | |
| 687 | + 'url' => $card['email_url'], | |
| 688 | + 'event' => 'set_up_email', | |
| 689 | + ], | |
| 690 | + ]; | |
| 691 | + | |
| 692 | + // Fall back to a generic label for an untitled form so the heading never | |
| 693 | + // renders "Finish setting up " with a dangling space. | |
| 694 | + $card_title = '' !== trim( (string) $card['title'] ) ? $card['title'] : __( 'your form', 'sureforms' ); | |
| 695 | + $heading = sprintf( | |
| 696 | + /* translators: %s: form title. */ | |
| 697 | + __( 'Finish setting up %s', 'sureforms' ), | |
| 698 | + $card_title | |
| 79 | 699 | ); |
| 80 | 700 | ?> |
| 81 | - <!-- Adding this internal style to maintain notice styling without worrying about conditional loading of the css files. --> | |
| 82 | - <style> | |
| 83 | - .srfm-plugin-notice-container { | |
| 84 | - display: flex; | |
| 85 | - align-items: center; | |
| 86 | - gap: 20px; | |
| 87 | - padding: 10px 0; | |
| 701 | + <div class="srfm-setup-checklist" id="srfm-setup-checklist"> | |
| 702 | + <p class="srfm-setup-checklist__title"> | |
| 703 | + <?php echo esc_html( $heading ); ?> | |
| 704 | + <?php if ( ! empty( $card['view_url'] ) ) { ?> | |
| 705 | + <a class="srfm-setup-checklist__view" data-srfm-event="view_form" href="<?php echo esc_url( $card['view_url'] ); ?>" target="_blank" rel="noopener noreferrer" aria-label="<?php echo esc_attr( sprintf( /* translators: %s: form title. */ __( 'View %s (opens in a new tab)', 'sureforms' ), $card_title ) ); ?>"> | |
| 706 | + <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path><polyline points="15 3 21 3 21 9"></polyline><line x1="10" y1="14" x2="21" y2="3"></line></svg> | |
| 707 | + </a> | |
| 708 | + <?php } ?> | |
| 709 | + </p> | |
| 710 | + <p class="srfm-setup-checklist__subtitle"><?php esc_html_e( 'Customize your form to get it ready for real submissions:', 'sureforms' ); ?></p> | |
| 711 | + | |
| 712 | + <ul class="srfm-setup-checklist__steps"> | |
| 713 | + <?php foreach ( $rows as $row ) { ?> | |
| 714 | + <li class="srfm-setup-checklist__step"> | |
| 715 | + <span class="srfm-setup-checklist__label"><?php echo esc_html( $row['label'] ); ?></span> | |
| 716 | + <a class="srfm-setup-checklist__cta" data-srfm-event="<?php echo esc_attr( $row['event'] ); ?>" href="<?php echo esc_url( $row['url'] ); ?>" target="_blank" rel="noopener noreferrer"><?php echo esc_html( $row['cta'] ); ?></a> | |
| 717 | + </li> | |
| 718 | + <?php } ?> | |
| 719 | + </ul> | |
| 720 | + </div> | |
| 721 | + <?php | |
| 722 | + } | |
| 723 | + | |
| 724 | + /** | |
| 725 | + * Enqueue the setup-checklist widget's styles and behavior on the dashboard (#3031). | |
| 726 | + * | |
| 727 | + * Mirrors the AI widget convention: an inline-only handle carries the CSS and the | |
| 728 | + * behavior (CTA click analytics), with server values — | |
| 729 | + * the REST URL, nonce and form id — passed through wp_localize_script rather than | |
| 730 | + * printed into the markup, so it stays Plugin-Check clean. | |
| 731 | + * | |
| 732 | + * @param string $hook_suffix Current admin page hook suffix. | |
| 733 | + * | |
| 734 | + * @since 2.12.4 | |
| 735 | + * @return void | |
| 736 | + */ | |
| 737 | + public function enqueue_form_setup_widget_assets( $hook_suffix ) { | |
| 738 | + if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) { | |
| 739 | + return; | |
| 88 | 740 | } |
| 89 | - .srfm-plugin-notice--logo svg { | |
| 90 | - width: 60px; | |
| 91 | - height: 60px; | |
| 741 | + | |
| 742 | + $card = self::get_form_setup_card(); | |
| 743 | + | |
| 744 | + if ( null === $card ) { | |
| 745 | + return; | |
| 92 | 746 | } |
| 93 | - .srfm-plugin-notice--content .srfm-plugin-notice--title { | |
| 94 | - margin: 0; | |
| 95 | - font-size: 20px; | |
| 96 | - line-height: 1.5; | |
| 747 | + | |
| 748 | + $css = <<<'CSS' | |
| 749 | +#srfm_form_setup_checklist .inside { margin: 0; padding: 0; } | |
| 750 | +.srfm-setup-checklist { padding: 12px 16px 16px; } | |
| 751 | +.srfm-setup-checklist__title { margin: 0 0 4px; font-size: 15px; font-weight: 600; color: #1e1e1e; } | |
| 752 | +.srfm-setup-checklist__view { display: inline-flex; align-items: center; margin-left: 6px; color: #d54e21; vertical-align: middle; } | |
| 753 | +.srfm-setup-checklist__view:hover, .srfm-setup-checklist__view:focus { color: #b83c14; } | |
| 754 | +.srfm-setup-checklist__subtitle { margin: 0 0 12px; color: #646970; font-size: 13px; } | |
| 755 | +.srfm-setup-checklist__steps { margin: 0; padding: 0; list-style: none; } | |
| 756 | +.srfm-setup-checklist__step { display: flex; align-items: center; gap: 12px; padding: 10px 12px; border-radius: 8px; } | |
| 757 | +.srfm-setup-checklist__step + .srfm-setup-checklist__step { margin-top: 6px; } | |
| 758 | +.srfm-setup-checklist__step { background: #f6f7f7; } | |
| 759 | +.srfm-setup-checklist__label { flex: 1 1 auto; font-size: 14px; color: #1e1e1e; } | |
| 760 | +.srfm-setup-checklist__cta { margin-left: auto; border: 0; background: transparent; padding: 0; font-size: 14px; font-weight: 600; color: #d54e21; text-decoration: underline; cursor: pointer; } | |
| 761 | +.srfm-setup-checklist__cta:hover { color: #b83c14; } | |
| 762 | +/* Keep visited links on-brand — WP admin's a:visited would otherwise turn them blue. */ | |
| 763 | +.srfm-setup-checklist a:visited { color: #d54e21; } | |
| 764 | +.srfm-setup-checklist a:visited:hover, .srfm-setup-checklist a:visited:focus { color: #b83c14; } | |
| 765 | +/* Drop WP's blue focus ring on the widget's links; keep an accessible, on-brand keyboard outline. */ | |
| 766 | +.srfm-setup-checklist a:focus { outline: none; box-shadow: none; } | |
| 767 | +.srfm-setup-checklist a:focus-visible { outline: 2px solid #d54e21; outline-offset: 2px; box-shadow: none; } | |
| 768 | +CSS; | |
| 769 | + | |
| 770 | + wp_register_style( 'srfm-setup-checklist-widget', false, [], SRFM_VER ); | |
| 771 | + wp_enqueue_style( 'srfm-setup-checklist-widget' ); | |
| 772 | + wp_add_inline_style( 'srfm-setup-checklist-widget', $css ); | |
| 773 | + | |
| 774 | + wp_register_script( 'srfm-setup-checklist-widget', '', [], SRFM_VER, true ); | |
| 775 | + wp_enqueue_script( 'srfm-setup-checklist-widget' ); | |
| 776 | + | |
| 777 | + wp_localize_script( | |
| 778 | + 'srfm-setup-checklist-widget', | |
| 779 | + 'srfmSetupChecklist', | |
| 780 | + [ | |
| 781 | + 'restUrl' => esc_url_raw( rest_url( 'sureforms/v1/dismiss-form-setup-card' ) ), | |
| 782 | + 'nonce' => wp_create_nonce( 'wp_rest' ), | |
| 783 | + 'formId' => $card['id'], | |
| 784 | + ] | |
| 785 | + ); | |
| 786 | + | |
| 787 | + $inline_script = <<<'JS' | |
| 788 | +( function () { | |
| 789 | + const cfg = window.srfmSetupChecklist || {}; | |
| 790 | + const widget = document.getElementById( 'srfm-setup-checklist' ); | |
| 791 | + if ( ! widget ) { | |
| 792 | + return; | |
| 793 | + } | |
| 794 | + | |
| 795 | + const persist = function ( action ) { | |
| 796 | + return fetch( cfg.restUrl, { | |
| 797 | + method: 'POST', | |
| 798 | + credentials: 'same-origin', | |
| 799 | + keepalive: true, | |
| 800 | + headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': cfg.nonce }, | |
| 801 | + body: JSON.stringify( { form_id: cfg.formId, action: action } ), | |
| 802 | + } ).catch( function () {} ); | |
| 803 | + }; | |
| 804 | + | |
| 805 | + // Beacon the CTA / view-form clicks for analytics. keepalive on the fetch lets | |
| 806 | + // the request finish even though the CTA immediately navigates away. | |
| 807 | + widget.addEventListener( 'click', function ( e ) { | |
| 808 | + const target = e.target?.closest?.( '[data-srfm-event]' ); | |
| 809 | + if ( target ) { | |
| 810 | + persist( target.getAttribute( 'data-srfm-event' ) ); | |
| 97 | 811 | } |
| 98 | - .srfm-plugin-notice--content .srfm-plugin-notice--message { | |
| 99 | - margin-top: 4px; | |
| 100 | - padding: 0; | |
| 812 | + } ); | |
| 813 | +}() ); | |
| 814 | +JS; | |
| 815 | + | |
| 816 | + wp_add_inline_script( 'srfm-setup-checklist-widget', $inline_script ); | |
| 817 | + } | |
| 818 | + | |
| 819 | + /** | |
| 820 | + * Register the "Finish setting up" prompt as an Astra Notices admin notice (#3030). | |
| 821 | + * | |
| 822 | + * Hooked to admin_notices so it registers before the Astra Notices library | |
| 823 | + * renders (priority 30). Shown on every admin screen EXCEPT the main dashboard, | |
| 824 | + * for the newest form the current user can edit that still has an unfinished | |
| 825 | + * step (default Thank You message, or no reply destination). Uses a single | |
| 826 | + * stable notice id so the library's built-in ✕ dismissal is one persistent | |
| 827 | + * choice ("stop nudging me"), not a per-form row. | |
| 828 | + * | |
| 829 | + * Split out from the renderer so the decision has exactly one home. The Getting | |
| 830 | + * Started notice suppresses itself when this returns a form, and duplicating the | |
| 831 | + * conditions there would have meant two copies drifting apart. Reading it costs | |
| 832 | + * nothing extra — get_thankyou_prompt_forms() memoizes its query per request. | |
| 833 | + * | |
| 834 | + * @since 2.12.6 | |
| 835 | + * @return array<string,mixed>|null The form to prompt for, or null when no prompt should render. | |
| 836 | + */ | |
| 837 | + public function get_displayable_thankyou_prompt() { | |
| 838 | + if ( ! Helper::current_user_can() || ! class_exists( 'Astra_Notices' ) ) { | |
| 839 | + return null; | |
| 101 | 840 | } |
| 841 | + | |
| 842 | + /** | |
| 843 | + * Short-circuit the "Finish setting up" Thank You notice. | |
| 844 | + * | |
| 845 | + * @param bool $show Whether to show the notice. Default true. | |
| 846 | + * | |
| 847 | + * @since 2.12.4 | |
| 848 | + */ | |
| 849 | + if ( ! apply_filters( 'srfm_show_thankyou_prompt', true ) ) { | |
| 850 | + return null; | |
| 851 | + } | |
| 852 | + | |
| 853 | + // Everywhere in wp-admin except the main dashboard. A null screen fails | |
| 854 | + // closed (return) rather than registering the notice on an unknown screen. | |
| 855 | + $screen = get_current_screen(); | |
| 856 | + | |
| 857 | + if ( ! $screen || 'dashboard' === $screen->id ) { | |
| 858 | + return null; | |
| 859 | + } | |
| 860 | + | |
| 861 | + // The library only checks dismissal at render (priority 30, after this | |
| 862 | + // query would already have run). Check it up front so a user who dismissed | |
| 863 | + // the prompt never pays for the WP_Query on subsequent admin page views. | |
| 864 | + if ( 'notice-dismissed' === get_user_meta( get_current_user_id(), self::THANKYOU_PROMPT_NOTICE_ID, true ) ) { | |
| 865 | + return null; | |
| 866 | + } | |
| 867 | + | |
| 868 | + // array_values so a filter returning a key-preserving array (e.g. the | |
| 869 | + // result of array_filter()) still exposes the newest prompt at index 0. | |
| 870 | + $prompts = array_values( (array) self::get_thankyou_prompt_forms() ); | |
| 871 | + | |
| 872 | + // Validate every key build_thankyou_notice_markup() reads, not just id/edit_url | |
| 873 | + // — a filter returning a partial payload would otherwise trip "Undefined array | |
| 874 | + // key" warnings and esc_url( null ) deprecations on every admin page. | |
| 875 | + if ( | |
| 876 | + empty( $prompts[0] ) || ! is_array( $prompts[0] ) | |
| 877 | + || empty( $prompts[0]['id'] ) || empty( $prompts[0]['edit_url'] ) | |
| 878 | + || empty( $prompts[0]['thankyou_url'] ) || empty( $prompts[0]['replies_url'] ) | |
| 879 | + || ! isset( $prompts[0]['title'] ) | |
| 880 | + ) { | |
| 881 | + return null; | |
| 882 | + } | |
| 883 | + | |
| 884 | + return $prompts[0]; | |
| 885 | + } | |
| 886 | + | |
| 887 | + /** | |
| 888 | + * Render the "Finish setting up" Thank You notice (#3030). | |
| 889 | + * | |
| 890 | + * @since 2.12.4 | |
| 891 | + * @return void | |
| 892 | + */ | |
| 893 | + public function render_thankyou_prompt_notice() { | |
| 894 | + $notice_id = self::THANKYOU_PROMPT_NOTICE_ID; | |
| 895 | + $form = $this->get_displayable_thankyou_prompt(); | |
| 896 | + | |
| 897 | + if ( null === $form ) { | |
| 898 | + return; | |
| 899 | + } | |
| 900 | + | |
| 901 | + // A broken form outranks a setup prompt. This is the top of the existing | |
| 902 | + // precedence chain, so the action-item check goes here rather than the | |
| 903 | + // action items standing down for an engagement notice. | |
| 904 | + if ( $this->has_action_item_warnings() ) { | |
| 905 | + return; | |
| 906 | + } | |
| 907 | + | |
| 908 | + \Astra_Notices::add_notice( | |
| 909 | + [ | |
| 910 | + 'id' => $notice_id, | |
| 911 | + 'type' => 'info', | |
| 912 | + 'message' => self::build_thankyou_notice_markup( $form ), | |
| 913 | + 'class' => 'srfm-notice srfm-thankyou-notice', | |
| 914 | + 'is_dismissible' => true, | |
| 915 | + 'display-with-other-notices' => true, | |
| 916 | + // Render late so this nudge never pre-empts higher-priority notices | |
| 917 | + // (e.g. Astra's minimum-version warnings, which are display-with- | |
| 918 | + // other-notices => false and would be skipped once ours renders). | |
| 919 | + 'priority' => 100, | |
| 920 | + ] | |
| 921 | + ); | |
| 922 | + | |
| 923 | + // The message is wp_kses_post'd by the library, so the brand-orange styling | |
| 924 | + // is printed through the notice's pre-markup hook instead of inline. | |
| 925 | + add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] ); | |
| 926 | + | |
| 927 | + // Track clicks on the CTAs and the dismiss ✕ via the shared notice-response | |
| 928 | + // endpoint, enqueued only when the notice actually renders. | |
| 929 | + add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_thankyou_notice_tracking' ] ); | |
| 930 | + } | |
| 931 | + | |
| 932 | + /** | |
| 933 | + * Enqueue the click-tracking for the Thank You notice (#3030). | |
| 934 | + * | |
| 935 | + * Sends an analytics beacon to the shared `srfm_notice_response` AJAX handler | |
| 936 | + * when a CTA or the dismiss ✕ is clicked. Uses `keepalive` so the beacon | |
| 937 | + * survives the navigation the CTA links trigger. | |
| 938 | + * | |
| 939 | + * @since 2.12.4 | |
| 940 | + * @return void | |
| 941 | + */ | |
| 942 | + public function enqueue_thankyou_notice_tracking() { | |
| 943 | + if ( wp_script_is( 'srfm-thankyou-notice-track', 'enqueued' ) ) { | |
| 944 | + return; | |
| 945 | + } | |
| 946 | + | |
| 947 | + wp_register_script( 'srfm-thankyou-notice-track', '', [], SRFM_VER, true ); | |
| 948 | + wp_enqueue_script( 'srfm-thankyou-notice-track' ); | |
| 949 | + | |
| 950 | + $config = wp_json_encode( | |
| 951 | + [ | |
| 952 | + 'ajaxurl' => admin_url( 'admin-ajax.php' ), | |
| 953 | + 'nonce' => wp_create_nonce( 'srfm_notice_response' ), | |
| 954 | + ] | |
| 955 | + ); | |
| 956 | + | |
| 957 | + wp_add_inline_script( 'srfm-thankyou-notice-track', 'window.srfmThankYouNoticeTrack = ' . $config . ';', 'before' ); | |
| 958 | + | |
| 959 | + $inline_script = <<<'JS' | |
| 960 | +( function () { | |
| 961 | + const cfg = window.srfmThankYouNoticeTrack || {}; | |
| 962 | + const wrap = document.querySelector( '.srfm-thankyou-notice' ); | |
| 963 | + if ( ! wrap ) { | |
| 964 | + return; | |
| 965 | + } | |
| 966 | + const noticeId = wrap.id || ''; | |
| 967 | + const send = function ( button ) { | |
| 968 | + const body = new URLSearchParams(); | |
| 969 | + body.append( 'action', 'srfm_notice_response' ); | |
| 970 | + body.append( 'nonce', cfg.nonce ); | |
| 971 | + body.append( 'notice_id', noticeId ); | |
| 972 | + body.append( 'button', button ); | |
| 973 | + fetch( cfg.ajaxurl, { | |
| 974 | + method: 'POST', | |
| 975 | + credentials: 'same-origin', | |
| 976 | + keepalive: true, | |
| 977 | + headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' }, | |
| 978 | + body: body.toString(), | |
| 979 | + } ).catch( function () {} ); | |
| 980 | + }; | |
| 981 | + // Delegate from the wrapper: this inline script runs at parse time, before | |
| 982 | + // core's common.js injects the .notice-dismiss ✕ (on DOMContentLoaded), so a | |
| 983 | + // direct querySelector for it would find nothing and the dismiss beacon would | |
| 984 | + // never fire. Delegation catches the ✕ and the CTAs whenever they exist. | |
| 985 | + const ctas = [ | |
| 986 | + [ '.srfm-ty-edit-form', 'edit_form' ], | |
| 987 | + [ '.srfm-ty-set-replies', 'set_replies' ], | |
| 988 | + [ '.srfm-ty-edit-thankyou', 'edit_thankyou' ], | |
| 989 | + ]; | |
| 990 | + wrap.addEventListener( 'click', function ( e ) { | |
| 991 | + if ( e.target.closest( '.notice-dismiss' ) ) { | |
| 992 | + send( 'dismissed' ); | |
| 993 | + return; | |
| 994 | + } | |
| 995 | + for ( let i = 0; i < ctas.length; i++ ) { | |
| 996 | + if ( e.target.closest( ctas[ i ][ 0 ] ) ) { | |
| 997 | + send( ctas[ i ][ 1 ] ); | |
| 998 | + return; | |
| 999 | + } | |
| 1000 | + } | |
| 1001 | + } ); | |
| 1002 | +}() ); | |
| 1003 | +JS; | |
| 1004 | + | |
| 1005 | + wp_add_inline_script( 'srfm-thankyou-notice-track', $inline_script ); | |
| 1006 | + } | |
| 1007 | + | |
| 1008 | + /** | |
| 1009 | + * Print the Thank You notice's brand-orange styling (#3030). | |
| 1010 | + * | |
| 1011 | + * Fired via astra_notice_before_markup_{id} so it lands right before the notice | |
| 1012 | + * and only when the notice actually renders. | |
| 1013 | + * | |
| 1014 | + * @since 2.12.4 | |
| 1015 | + * @return void | |
| 1016 | + */ | |
| 1017 | + public function print_srfm_notice_styles() { | |
| 1018 | + // The library wp_kses_post()'s the message, which strips <svg> and data: | |
| 1019 | + // image srcs, so the SureForms mark is painted as a CSS background here | |
| 1020 | + // (this hook fires outside that kses call). URL-encoded, not base64, so the | |
| 1021 | + // value is fully percent-encoded and safe to pass through esc_url. | |
| 1022 | + $icon = 'data:image/svg+xml,' . rawurlencode( | |
| 1023 | + '<svg xmlns="http://www.w3.org/2000/svg" width="36" height="36" viewBox="0 0 32 32"><path fill="#D54407" fill-rule="evenodd" clip-rule="evenodd" d="M32 0H0V32H32V0ZM22.8573 6.85728H9.14304V11.4287V13.7144L11.4288 11.4287H22.8573V6.85728ZM20.5717 13.7146H9.14314V18.286V20.5714V20.5718V25.1428H16.0003V20.5714H9.14351L11.4289 18.286H20.5717V13.7146Z"/></svg>' | |
| 1024 | + ); | |
| 1025 | + ?> | |
| 1026 | + <style id="srfm-notice-styles"> | |
| 1027 | + .srfm-notice.notice { border-left-color: #D54407; } | |
| 1028 | + /* Stack our blocks (the library lays the container out as a flex row) and reserve room on the left for the SureForms mark. */ | |
| 1029 | + .srfm-notice .astra-notice-container { display: block; padding: 4px 0 4px 52px; background: url('<?php echo esc_url( $icon, [ 'data' ] ); ?>') no-repeat 4px 6px; background-size: 32px 32px; } | |
| 1030 | + .srfm-notice .srfm-notice__title { margin: 0 0 4px; font-size: 14px; font-weight: 600; color: #1d2327; } | |
| 1031 | + .srfm-notice .srfm-notice__text { margin: 0 0 10px; color: #50575e; } | |
| 1032 | + .srfm-notice .srfm-notice__actions { margin: 12px 0 2px; display: flex; flex-wrap: wrap; gap: 10px 20px; align-items: center; } | |
| 1033 | + .srfm-notice .button-primary { background: #D54407; border-color: #D54407; color: #fff; box-shadow: none; text-shadow: none; } | |
| 1034 | + .srfm-notice .button-primary:hover, .srfm-notice .button-primary:focus { background: #C83B00; border-color: #C83B00; color: #fff; box-shadow: none; } | |
| 1035 | + .srfm-notice .button:not(.button-primary) { background: transparent; border-color: transparent; color: #D54407; box-shadow: none; padding: 0; } | |
| 1036 | + .srfm-notice .button:not(.button-primary):hover, .srfm-notice .button:not(.button-primary):focus { background: transparent; border-color: transparent; color: #C83B00; box-shadow: none; } | |
| 1037 | + .srfm-notice .button-primary:focus { outline: 2px solid #D54407; outline-offset: 1px; } | |
| 102 | 1038 | </style> |
| 103 | - <div class="notice notice-warning is-dismissible srfm-plugin-notice"> | |
| 104 | - <div class="srfm-plugin-notice-container"> | |
| 105 | - <div class="srfm-plugin-notice--logo"> | |
| 106 | - <svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 107 | - <path fill-rule="evenodd" clip-rule="evenodd" d="M250 500C388.072 500 500 388.071 500 250C500 111.929 388.072 0 250 0C111.929 0 0 111.929 0 250C0 388.071 111.929 500 250 500ZM251.076 125C231.002 125 203.224 136.48 189.028 150.641L150.477 189.103H342.635L406.887 125H251.076ZM310.648 349.359C296.454 363.52 268.673 375 248.599 375H92.7892L157.043 310.897H349.199L310.648 349.359ZM373.1 221.154H118.42L106.39 233.173C77.9043 258.814 86.3526 278.846 126.246 278.846H381.615L393.649 266.827C421.859 241.336 412.993 221.154 373.1 221.154Z" fill="#D54407"/> | |
| 108 | - </svg> | |
| 109 | - </div> | |
| 110 | - <div class="srfm-plugin-notice--content"> | |
| 111 | - <h3 class="srfm-plugin-notice--title"><?php esc_html_e( 'SureForms - Important Update Notice', 'sureforms' ); ?></h3> | |
| 112 | - <p class="srfm-plugin-notice--message"><strong><?php esc_html_e( "From version 0.0.13 we're migrating to a custom database to enhance SureForms' performance and features.", 'sureforms' ); ?></strong></p> | |
| 113 | - <p class="srfm-plugin-notice--message"><?php esc_html_e( 'This step is necessary and irreversible and your current existing entries will be lost. Thank you for your understanding!', 'sureforms' ); ?></p> | |
| 114 | - </div> | |
| 115 | - </div> | |
| 116 | - </div> | |
| 117 | - <script> | |
| 118 | - (function() { | |
| 119 | - window.addEventListener('load', function() { | |
| 120 | - const dismissNotice = document.querySelector('.is-dismissible.srfm-plugin-notice .notice-dismiss'); | |
| 1039 | + <?php | |
| 1040 | + } | |
| 121 | 1041 | |
| 122 | - dismissNotice.addEventListener('click', function() { | |
| 123 | - fetch('<?php echo esc_url_raw( $ajaxurl ); ?>'); | |
| 124 | - }); | |
| 125 | - }); | |
| 126 | - }()); | |
| 127 | - </script> | |
| 128 | - <?php | |
| 1042 | + /** | |
| 1043 | + * Check and save the first form creation time stamp. | |
| 1044 | + * If not already saved. | |
| 1045 | + * | |
| 1046 | + * @since 1.10.1 | |
| 1047 | + * @return void | |
| 1048 | + */ | |
| 1049 | + public static function save_first_form_creation_time_stamp() { | |
| 1050 | + if ( ! Helper::current_user_can() || self::is_first_form_created() || ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) { | |
| 1051 | + return; | |
| 1052 | + } | |
| 1053 | + | |
| 1054 | + // Get the first form creation time from the database that is published. | |
| 1055 | + $query = new \WP_Query( | |
| 1056 | + [ | |
| 1057 | + 'post_type' => SRFM_FORMS_POST_TYPE, | |
| 1058 | + 'posts_per_page' => 1, | |
| 1059 | + 'orderby' => 'date', | |
| 1060 | + 'order' => 'ASC', | |
| 1061 | + 'fields' => 'ids', | |
| 1062 | + 'post_status' => 'publish', | |
| 1063 | + ] | |
| 1064 | + ); | |
| 1065 | + | |
| 1066 | + if ( ! empty( $query->posts ) && isset( $query->posts[0] ) ) { | |
| 1067 | + // Get the first post from the query result. | |
| 1068 | + $post_id = $query->posts[0]; | |
| 1069 | + // Get the post creation time in GMT. | |
| 1070 | + $creation_time = get_post_field( 'post_date_gmt', $post_id ); | |
| 1071 | + // Convert the creation time to a timestamp. | |
| 1072 | + $timestamp = strtotime( $creation_time ); | |
| 1073 | + | |
| 1074 | + if ( ! $timestamp ) { | |
| 1075 | + return; | |
| 1076 | + } | |
| 1077 | + | |
| 1078 | + Helper::update_srfm_option( 'first_form_created_at', $timestamp ); | |
| 1079 | + } | |
| 129 | 1080 | } |
| 130 | 1081 | |
| 131 | 1082 | /** |
| 1083 | + * Check if n days have passed since the first form creation. | |
| 1084 | + * This is used to determine if the dynamic nudges should be shown. | |
| 1085 | + * | |
| 1086 | + * @param int $days Number of days to check. | |
| 1087 | + * @since 1.10.1 | |
| 1088 | + * @return bool | |
| 1089 | + */ | |
| 1090 | + public static function check_first_form_creation_threshold( $days = 3 ) { | |
| 1091 | + $first_form_creation_time_stamp = self::get_first_form_creation_time_stamp(); | |
| 1092 | + | |
| 1093 | + if ( ! $first_form_creation_time_stamp ) { | |
| 1094 | + return false; // No forms created yet. | |
| 1095 | + } | |
| 1096 | + | |
| 1097 | + /** | |
| 1098 | + * Calculate the number of days since the first form was created. | |
| 1099 | + */ | |
| 1100 | + $days_from_creation = ( strtotime( current_time( 'mysql' ) ) - $first_form_creation_time_stamp ) / DAY_IN_SECONDS; | |
| 1101 | + | |
| 1102 | + // Return a boolean indicating if the number of days since creation is greater than the specified days. | |
| 1103 | + return $days_from_creation > $days; | |
| 1104 | + } | |
| 1105 | + | |
| 1106 | + /** | |
| 1107 | + * Show action on plugin page. | |
| 1108 | + * | |
| 1109 | + * @param array $links links. | |
| 1110 | + * @return array | |
| 1111 | + * @since 1.4.2 | |
| 1112 | + */ | |
| 1113 | + public function add_action_links( $links ) { | |
| 1114 | + if ( ! Helper::has_pro() ) { | |
| 1115 | + // Display upsell link if SureForms Pro is not installed. | |
| 1116 | + $upsell_link = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'plugin-list' ] ); | |
| 1117 | + | |
| 1118 | + ob_start(); | |
| 1119 | + ?> | |
| 1120 | + <a href="<?php echo esc_url( $upsell_link ); ?>" target="_blank" rel="noreferrer" class="sureforms-plugins-go-pro"> | |
| 1121 | + <?php echo esc_html__( 'Get SureForms Pro', 'sureforms' ); ?> | |
| 1122 | + </a> | |
| 1123 | + <?php | |
| 1124 | + $links[] = trim( ob_get_clean() ); | |
| 1125 | + } | |
| 1126 | + | |
| 1127 | + return $links; | |
| 1128 | + } | |
| 1129 | + | |
| 1130 | + /** | |
| 1131 | + * Enable block editor in Enfold theme for SureForms post type. | |
| 1132 | + * | |
| 1133 | + * @param bool $use_block_editor Whether to use block editor. | |
| 1134 | + * @since 1.3.1 | |
| 1135 | + */ | |
| 1136 | + public function enable_block_editor_in_enfold_theme( $use_block_editor ) { | |
| 1137 | + // if SureForms form post type then return true. | |
| 1138 | + if ( SRFM_FORMS_POST_TYPE === get_current_screen()->post_type ) { | |
| 1139 | + return true; | |
| 1140 | + } | |
| 1141 | + return $use_block_editor; | |
| 1142 | + } | |
| 1143 | + | |
| 1144 | + /** | |
| 132 | 1145 | * Enable Gutenberg for SureForms associated post types. |
| 133 | 1146 | * |
| 134 | 1147 | * @since 0.0.10 |
| 135 | 1148 | */ |
| 136 | 1149 | public function enable_gutenberg_for_sureforms() { |
| 137 | - | |
| 138 | - // Check if the Classic Editor plugin is active and if it is set to replace the block editor. | |
| 139 | - if ( ! class_exists( 'Classic_Editor' ) || 'block' === get_option( 'classic-editor-replace' ) ) { | |
| 1150 | + /** | |
| 1151 | + * Check if the classic editor is enabled from Classic Editor plugin settings or Divi settings. | |
| 1152 | + */ | |
| 1153 | + if ( 'block' === get_option( 'classic-editor-replace' ) || 'on' === get_option( 'et_enable_classic_editor' ) ) { | |
| 140 | 1154 | return; |
| 141 | 1155 | } |
| 142 | 1156 | |
| 143 | 1157 | $srfm_post_types = apply_filters( 'srfm_enable_gutenberg_post_types', [ SRFM_FORMS_POST_TYPE ] ); |
| @@ -147,9 +1161,8 @@ | ||
| 147 | 1161 | add_filter( 'gutenberg_can_edit_post_type', '__return_true', 110 ); |
| 148 | 1162 | } |
| 149 | 1163 | } |
| 150 | 1164 | |
| 151 | - | |
| 152 | 1165 | /** |
| 153 | 1166 | * Sureforms editor header styles. |
| 154 | 1167 | * |
| 155 | 1168 | * @since 0.0.1 |
| @@ -177,22 +1190,18 @@ | ||
| 177 | 1190 | * @return void |
| 178 | 1191 | * @since 0.0.1 |
| 179 | 1192 | */ |
| 180 | 1193 | public function add_menu_page() { |
| 181 | - if ( ! current_user_can( 'manage_options' ) ) { | |
| 182 | - return; | |
| 183 | - } | |
| 1194 | + $menu_slug = 'sureforms_menu'; | |
| 184 | 1195 | |
| 185 | - $capability = 'manage_options'; | |
| 186 | - $menu_slug = 'sureforms_menu'; | |
| 187 | - | |
| 188 | 1196 | $logo = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/icon.svg' ); |
| 189 | 1197 | add_menu_page( |
| 190 | 1198 | __( 'SureForms', 'sureforms' ), |
| 191 | 1199 | __( 'SureForms', 'sureforms' ), |
| 192 | - 'edit_others_posts', | |
| 1200 | + self::$sureforms_page_default_capability, | |
| 193 | 1201 | $menu_slug, |
| 194 | - function () {}, | |
| 1202 | + static function () { | |
| 1203 | + }, | |
| 195 | 1204 | 'data:image/svg+xml;base64,' . base64_encode( $logo ), // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode |
| 196 | 1205 | 30 |
| 197 | 1206 | ); |
| 198 | 1207 | |
| @@ -200,9 +1209,9 @@ | ||
| 200 | 1209 | add_submenu_page( |
| 201 | 1210 | $menu_slug, |
| 202 | 1211 | __( 'Dashboard', 'sureforms' ), |
| 203 | 1212 | __( 'Dashboard', 'sureforms' ), |
| 204 | - $capability, | |
| 1213 | + self::$sureforms_page_default_capability, | |
| 205 | 1214 | $menu_slug, |
| 206 | 1215 | [ $this, 'render_dashboard' ] |
| 207 | 1216 | ); |
| 208 | 1217 | } |
| @@ -218,9 +1227,9 @@ | ||
| 218 | 1227 | add_submenu_page( |
| 219 | 1228 | 'sureforms_menu', |
| 220 | 1229 | __( 'Settings', 'sureforms' ), |
| 221 | 1230 | __( 'Settings', 'sureforms' ), |
| 222 | - 'edit_others_posts', | |
| 1231 | + self::$sureforms_page_default_capability, | |
| 223 | 1232 | 'sureforms_form_settings', |
| 224 | 1233 | $callback |
| 225 | 1234 | ); |
| 226 | 1235 | |
| @@ -233,8 +1242,190 @@ | ||
| 233 | 1242 | } |
| 234 | 1243 | } |
| 235 | 1244 | |
| 236 | 1245 | /** |
| 1246 | + * Open to Upgrade to Pro submenu link in new tab. | |
| 1247 | + * | |
| 1248 | + * @return void | |
| 1249 | + * @since 1.6.1 | |
| 1250 | + */ | |
| 1251 | + public function add_upgrade_to_pro_target_attr() { | |
| 1252 | + ?> | |
| 1253 | + <script type="text/javascript"> | |
| 1254 | + document.addEventListener('DOMContentLoaded', function () { | |
| 1255 | + // Upgrade link handler. | |
| 1256 | + // IMPORTANT: If this URL changes, also update it in the `add_upgrade_to_pro` function. | |
| 1257 | + const upgradeLink = document.querySelector('a[href*="https://sureforms.com/upgrade"]'); | |
| 1258 | + if (upgradeLink) { | |
| 1259 | + upgradeLink.addEventListener('click', e => { | |
| 1260 | + e.preventDefault(); | |
| 1261 | + window.open(upgradeLink.href, '_blank'); | |
| 1262 | + }); | |
| 1263 | + } | |
| 1264 | + }); | |
| 1265 | + </script> | |
| 1266 | + <?php | |
| 1267 | + } | |
| 1268 | + | |
| 1269 | + /** | |
| 1270 | + * Add Upgrade to pro menu item. | |
| 1271 | + * | |
| 1272 | + * @return void | |
| 1273 | + * @since 1.6.1 | |
| 1274 | + */ | |
| 1275 | + public function add_upgrade_to_pro() { | |
| 1276 | + // The url used here is used as a selector for css to style the upgrade to pro submenu. | |
| 1277 | + // If you are changing this url, please make sure to update the css as well. | |
| 1278 | + $upgrade_url = Helper::get_sureforms_website_url( 'upgrade', [ 'utm_medium' => 'submenu_link_upgrade' ] ); | |
| 1279 | + | |
| 1280 | + add_submenu_page( | |
| 1281 | + 'sureforms_menu', | |
| 1282 | + __( 'Upgrade', 'sureforms' ), | |
| 1283 | + __( 'Upgrade', 'sureforms' ), | |
| 1284 | + self::$sureforms_page_default_capability, | |
| 1285 | + $upgrade_url | |
| 1286 | + ); | |
| 1287 | + } | |
| 1288 | + | |
| 1289 | + /** | |
| 1290 | + * Add Quiz empty state submenu page for free users. | |
| 1291 | + * | |
| 1292 | + * @return void | |
| 1293 | + * @since 2.7.0 | |
| 1294 | + */ | |
| 1295 | + public function add_quiz_page() { | |
| 1296 | + add_submenu_page( | |
| 1297 | + 'sureforms_menu', | |
| 1298 | + __( 'Quiz Entries', 'sureforms' ), | |
| 1299 | + __( 'Quizzes', 'sureforms' ) . | |
| 1300 | + ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' . | |
| 1301 | + esc_html__( 'New', 'sureforms' ) . | |
| 1302 | + '</span>', | |
| 1303 | + self::$sureforms_page_default_capability, | |
| 1304 | + 'sureforms_quiz_entries', | |
| 1305 | + [ $this, 'render_quiz_empty_state' ], | |
| 1306 | + 5 | |
| 1307 | + ); | |
| 1308 | + } | |
| 1309 | + | |
| 1310 | + /** | |
| 1311 | + * Quiz empty state page callback. | |
| 1312 | + * | |
| 1313 | + * @return void | |
| 1314 | + * @since 2.7.0 | |
| 1315 | + */ | |
| 1316 | + public function render_quiz_empty_state() { | |
| 1317 | + ?> | |
| 1318 | + <div id="srfm-quiz-entries-root" class="srfm-admin-wrapper"></div> | |
| 1319 | + <?php | |
| 1320 | + } | |
| 1321 | + | |
| 1322 | + /** | |
| 1323 | + * Add Survey Reports promotional submenu page for free users. | |
| 1324 | + * | |
| 1325 | + * @return void | |
| 1326 | + * @since 2.8.0 | |
| 1327 | + */ | |
| 1328 | + public function add_survey_reports_page() { | |
| 1329 | + add_submenu_page( | |
| 1330 | + 'sureforms_menu', | |
| 1331 | + __( 'Survey Reports', 'sureforms' ), | |
| 1332 | + __( 'Survey Reports', 'sureforms' ) . | |
| 1333 | + ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' . | |
| 1334 | + esc_html__( 'New', 'sureforms' ) . | |
| 1335 | + '</span>', | |
| 1336 | + self::$sureforms_page_default_capability, | |
| 1337 | + 'sureforms_survey_reports', | |
| 1338 | + [ $this, 'render_survey_empty_state' ], | |
| 1339 | + 6 | |
| 1340 | + ); | |
| 1341 | + } | |
| 1342 | + | |
| 1343 | + /** | |
| 1344 | + * Survey empty state page callback. | |
| 1345 | + * | |
| 1346 | + * @return void | |
| 1347 | + * @since 2.8.0 | |
| 1348 | + */ | |
| 1349 | + public function render_survey_empty_state() { | |
| 1350 | + ?> | |
| 1351 | + <div id="srfm-survey-empty-state-root" class="srfm-admin-wrapper"></div> | |
| 1352 | + <?php | |
| 1353 | + } | |
| 1354 | + | |
| 1355 | + /** | |
| 1356 | + * Add Partial Entries promotional submenu page for free users. | |
| 1357 | + * | |
| 1358 | + * @return void | |
| 1359 | + * @since 2.9.0 | |
| 1360 | + */ | |
| 1361 | + public function add_partial_entries_page() { | |
| 1362 | + add_submenu_page( | |
| 1363 | + 'sureforms_menu', | |
| 1364 | + __( 'Partial Entries', 'sureforms' ), | |
| 1365 | + __( 'Partial Entries', 'sureforms' ) . | |
| 1366 | + ' <span style="color:#4ADE80;font-size:9px;font-weight:600;">' . | |
| 1367 | + esc_html__( 'New', 'sureforms' ) . | |
| 1368 | + '</span>', | |
| 1369 | + self::$sureforms_page_default_capability, | |
| 1370 | + 'sureforms_partial_entries', | |
| 1371 | + [ $this, 'render_partial_entries_empty_state' ], | |
| 1372 | + 7 | |
| 1373 | + ); | |
| 1374 | + } | |
| 1375 | + | |
| 1376 | + /** | |
| 1377 | + * Partial Entries empty state page callback. | |
| 1378 | + * | |
| 1379 | + * @return void | |
| 1380 | + * @since 2.9.0 | |
| 1381 | + */ | |
| 1382 | + public function render_partial_entries_empty_state() { | |
| 1383 | + ?> | |
| 1384 | + <div id="srfm-partial-entries-empty-state-root" class="srfm-admin-wrapper"></div> | |
| 1385 | + <?php | |
| 1386 | + } | |
| 1387 | + | |
| 1388 | + /** | |
| 1389 | + * Add SMTP promotional submenu page. | |
| 1390 | + * | |
| 1391 | + * @return void | |
| 1392 | + * @since 1.7.1 | |
| 1393 | + */ | |
| 1394 | + public function add_suremail_page() { | |
| 1395 | + add_submenu_page( | |
| 1396 | + 'sureforms_menu', | |
| 1397 | + __( 'SMTP', 'sureforms' ), | |
| 1398 | + __( 'SMTP', 'sureforms' ), | |
| 1399 | + self::$sureforms_page_default_capability, | |
| 1400 | + 'sureforms_smtp', | |
| 1401 | + [ $this, 'suremail_page_callback' ] | |
| 1402 | + ); | |
| 1403 | + | |
| 1404 | + // Get the current submenu page. | |
| 1405 | + $submenu_page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- $_GET['page'] does not provide nonce. | |
| 1406 | + | |
| 1407 | + // Check if SureMail is installed and active. | |
| 1408 | + if ( 'sureforms_smtp' === $submenu_page && file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' ) && is_plugin_active( 'suremails/suremails.php' ) ) { | |
| 1409 | + // Plugin is installed and active - redirect to SureMail dashboard. | |
| 1410 | + wp_safe_redirect( admin_url( 'options-general.php?page=suremail#/dashboard' ) ); | |
| 1411 | + exit; | |
| 1412 | + } | |
| 1413 | + } | |
| 1414 | + | |
| 1415 | + /** | |
| 1416 | + * SMTP promotional page callback. | |
| 1417 | + * | |
| 1418 | + * @return void | |
| 1419 | + * @since 1.7.1 | |
| 1420 | + */ | |
| 1421 | + public function suremail_page_callback() { | |
| 1422 | + ?> | |
| 1423 | + <div id="srfm-suremail-container" class="srfm-admin-wrapper"></div> | |
| 1424 | + <?php | |
| 1425 | + } | |
| 1426 | + | |
| 1427 | + /** | |
| 237 | 1428 | * Render Admin Dashboard. |
| 238 | 1429 | * |
| 239 | 1430 | * @return void |
| 240 | 1431 | * @since 0.0.1 |
| @@ -239,9 +1430,11 @@ | ||
| 239 | 1430 | * @return void |
| 240 | 1431 | * @since 0.0.1 |
| 241 | 1432 | */ |
| 242 | 1433 | public function render_dashboard() { |
| 243 | - echo '<div id="srfm-dashboard-container"></div>'; | |
| 1434 | + ?> | |
| 1435 | + <div id="srfm-dashboard-container" class="srfm-admin-wrapper"></div> | |
| 1436 | + <?php | |
| 244 | 1437 | } |
| 245 | 1438 | |
| 246 | 1439 | /** |
| 247 | 1440 | * Settings page callback. |
| @@ -249,12 +1442,43 @@ | ||
| 249 | 1442 | * @return void |
| 250 | 1443 | * @since 0.0.1 |
| 251 | 1444 | */ |
| 252 | 1445 | public function settings_page_callback() { |
| 253 | - echo '<div id="srfm-settings-container"></div>'; | |
| 1446 | + ?> | |
| 1447 | + <div id="srfm-settings-container" class="srfm-admin-wrapper"></div> | |
| 1448 | + <?php | |
| 254 | 1449 | } |
| 255 | 1450 | |
| 256 | 1451 | /** |
| 1452 | + * Add Learn submenu page. | |
| 1453 | + * | |
| 1454 | + * @return void | |
| 1455 | + * @since 2.5.2 | |
| 1456 | + */ | |
| 1457 | + public function add_learn_page() { | |
| 1458 | + add_submenu_page( | |
| 1459 | + 'sureforms_menu', | |
| 1460 | + __( 'Learn', 'sureforms' ), | |
| 1461 | + __( 'Learn', 'sureforms' ), | |
| 1462 | + self::$sureforms_page_default_capability, | |
| 1463 | + 'sureforms_learn', | |
| 1464 | + [ $this, 'render_learn' ] | |
| 1465 | + ); | |
| 1466 | + } | |
| 1467 | + | |
| 1468 | + /** | |
| 1469 | + * Learn page callback. | |
| 1470 | + * | |
| 1471 | + * @return void | |
| 1472 | + * @since 2.5.2 | |
| 1473 | + */ | |
| 1474 | + public function render_learn() { | |
| 1475 | + ?> | |
| 1476 | + <div id="srfm-learn-root" class="srfm-admin-wrapper"></div> | |
| 1477 | + <?php | |
| 1478 | + } | |
| 1479 | + | |
| 1480 | + /** | |
| 257 | 1481 | * Add new form menu item. |
| 258 | 1482 | * |
| 259 | 1483 | * @return void |
| 260 | 1484 | * @since 0.0.1 |
| @@ -261,27 +1485,62 @@ | ||
| 261 | 1485 | */ |
| 262 | 1486 | public function add_new_form() { |
| 263 | 1487 | add_submenu_page( |
| 264 | 1488 | 'sureforms_menu', |
| 1489 | + __( 'Forms', 'sureforms' ), | |
| 1490 | + __( 'Forms', 'sureforms' ), | |
| 1491 | + self::$sureforms_page_default_capability, | |
| 1492 | + 'sureforms_forms', | |
| 1493 | + [ $this, 'render_forms' ], | |
| 1494 | + 1 | |
| 1495 | + ); | |
| 1496 | + add_submenu_page( | |
| 1497 | + 'sureforms_menu', | |
| 265 | 1498 | __( 'New Form', 'sureforms' ), |
| 266 | 1499 | __( 'New Form', 'sureforms' ), |
| 267 | - 'edit_others_posts', | |
| 1500 | + self::$sureforms_page_default_capability, | |
| 268 | 1501 | 'add-new-form', |
| 269 | 1502 | [ $this, 'add_new_form_callback' ], |
| 270 | 1503 | 2 |
| 271 | 1504 | ); |
| 272 | - add_submenu_page( | |
| 1505 | + $entries_hook = add_submenu_page( | |
| 273 | 1506 | 'sureforms_menu', |
| 274 | 1507 | __( 'Entries', 'sureforms' ), |
| 275 | 1508 | __( 'Entries', 'sureforms' ), |
| 276 | - 'edit_others_posts', | |
| 277 | - 'sureforms_entries', | |
| 1509 | + self::$sureforms_page_default_capability, | |
| 1510 | + SRFM_ENTRIES, | |
| 278 | 1511 | [ $this, 'render_entries' ], |
| 279 | 1512 | 3 |
| 280 | 1513 | ); |
| 1514 | + | |
| 1515 | + add_submenu_page( | |
| 1516 | + 'sureforms_menu', | |
| 1517 | + __( 'Payments', 'sureforms' ), | |
| 1518 | + __( 'Payments', 'sureforms' ), | |
| 1519 | + self::$sureforms_page_default_capability, | |
| 1520 | + SRFM_PAYMENTS, | |
| 1521 | + [ $this, 'render_payments' ], | |
| 1522 | + 4 | |
| 1523 | + ); | |
| 1524 | + | |
| 1525 | + if ( $entries_hook ) { | |
| 1526 | + add_action( 'load-' . $entries_hook, [ $this, 'mark_entries_page_visit' ] ); | |
| 1527 | + } | |
| 281 | 1528 | } |
| 282 | 1529 | |
| 283 | 1530 | /** |
| 1531 | + * Payments page callback. | |
| 1532 | + * | |
| 1533 | + * @return void | |
| 1534 | + * @since 2.0.0 | |
| 1535 | + */ | |
| 1536 | + public function render_payments() { | |
| 1537 | + ?> | |
| 1538 | + <div id="srfm-payments-react-container" class="srfm-admin-wrapper"></div> | |
| 1539 | + <?php | |
| 1540 | + } | |
| 1541 | + | |
| 1542 | + /** | |
| 284 | 1543 | * Add new form mentu item callback. |
| 285 | 1544 | * |
| 286 | 1545 | * @return void |
| 287 | 1546 | * @since 0.0.1 |
| @@ -286,44 +1545,109 @@ | ||
| 286 | 1545 | * @return void |
| 287 | 1546 | * @since 0.0.1 |
| 288 | 1547 | */ |
| 289 | 1548 | public function add_new_form_callback() { |
| 290 | - echo '<div id="srfm-add-new-form-container"></div>'; | |
| 1549 | + ?> | |
| 1550 | + <div id="srfm-add-new-form-container" class="srfm-admin-wrapper"></div> | |
| 1551 | + <?php | |
| 291 | 1552 | } |
| 292 | 1553 | |
| 293 | 1554 | /** |
| 1555 | + * Forms page callback. | |
| 1556 | + * | |
| 1557 | + * @return void | |
| 1558 | + * @since 2.0.0 | |
| 1559 | + */ | |
| 1560 | + public function render_forms() { | |
| 1561 | + ?> | |
| 1562 | + <div id="srfm-forms-root" class="srfm-admin-wrapper"></div> | |
| 1563 | + <?php | |
| 1564 | + } | |
| 1565 | + | |
| 1566 | + /** | |
| 294 | 1567 | * Entries page callback. |
| 295 | 1568 | * |
| 296 | 1569 | * @since 0.0.13 |
| 1570 | + * @since 2.0.0 - Updated the entries UI and the function definition. | |
| 297 | 1571 | * @return void |
| 298 | 1572 | */ |
| 299 | 1573 | public function render_entries() { |
| 300 | - // Render single entry view. | |
| 301 | - // Adding the phpcs ignore nonce verification as no database operations are performed in this function, it is used to display the single entry view. | |
| 302 | - if ( isset( $_GET['entry_id'] ) && is_numeric( $_GET['entry_id'] ) && isset( $_GET['view'] ) && 'details' === $_GET['view'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 303 | - $single_entry_view = new Single_Entry(); | |
| 304 | - $single_entry_view->render(); | |
| 1574 | + echo '<div id="srfm-entries-root"></div>'; | |
| 1575 | + } | |
| 1576 | + | |
| 1577 | + /** | |
| 1578 | + * Add notification badge to SureForms menu when there are new entries. | |
| 1579 | + * | |
| 1580 | + * @since 1.7.3 | |
| 1581 | + * @return void | |
| 1582 | + */ | |
| 1583 | + public function maybe_add_entries_badge() { | |
| 1584 | + if ( ! Helper::current_user_can() ) { | |
| 305 | 1585 | return; |
| 306 | 1586 | } |
| 307 | 1587 | |
| 308 | - // Render all entries view. | |
| 309 | - $entries_table = new Entries_List_Table(); | |
| 310 | - $entries_table->prepare_items(); | |
| 311 | - echo '<div class="wrap"><h1 class="wp-heading-inline">Entries</h1>'; | |
| 312 | - if ( 0 >= $entries_table->all_entries_count && 0 >= $entries_table->trash_count ) { | |
| 313 | - $instance = Post_Types::get_instance(); | |
| 314 | - $instance->sureforms_render_blank_state( SRFM_ENTRIES_POST_TYPE ); | |
| 315 | - $instance->get_blank_state_styles(); | |
| 1588 | + // If currently viewing the entries listing page, mark it as visited and skip the badge. | |
| 1589 | + if ( isset( $_GET['page'] ) && SRFM_ENTRIES === $_GET['page'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Only checking the page slug. | |
| 1590 | + $this->mark_entries_page_visit(); | |
| 316 | 1591 | return; |
| 317 | 1592 | } |
| 318 | - echo '<form method="get">'; | |
| 319 | - echo '<input type="hidden" name="page" value="sureforms_entries">'; | |
| 320 | - $entries_table->display(); | |
| 321 | - echo '</form>'; | |
| 322 | - echo '</div>'; | |
| 1593 | + | |
| 1594 | + $srfm_options = get_option( 'srfm_options', [] ); | |
| 1595 | + $last_visit = isset( $srfm_options['entries_last_visited'] ) ? absint( $srfm_options['entries_last_visited'] ) : 0; | |
| 1596 | + $new_entries = Entries::get_entries_count_after( $last_visit ); | |
| 1597 | + | |
| 1598 | + if ( $new_entries <= 0 ) { | |
| 1599 | + return; | |
| 1600 | + } | |
| 1601 | + | |
| 1602 | + global $menu; | |
| 1603 | + foreach ( $menu as $index => $item ) { | |
| 1604 | + if ( isset( $item[2] ) && 'sureforms_menu' === $item[2] ) { | |
| 1605 | + ob_start(); | |
| 1606 | + ?> | |
| 1607 | + <span class="srfm-update-dot"></span> | |
| 1608 | + <?php | |
| 1609 | + $dot_html = ob_get_clean(); | |
| 1610 | + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for menu item. | |
| 1611 | + $menu[ $index ][0] .= $dot_html; | |
| 1612 | + break; | |
| 1613 | + } | |
| 1614 | + } | |
| 1615 | + | |
| 1616 | + global $submenu; | |
| 1617 | + if ( isset( $submenu['sureforms_menu'] ) ) { | |
| 1618 | + foreach ( $submenu['sureforms_menu'] as $index => $sub_item ) { | |
| 1619 | + if ( isset( $sub_item[2] ) && SRFM_ENTRIES === $sub_item[2] ) { | |
| 1620 | + ob_start(); | |
| 1621 | + ?> | |
| 1622 | + <span class="update-plugins count-<?php echo absint( $new_entries ); ?>"> | |
| 1623 | + <span class="plugin-count"><?php echo absint( $new_entries ); ?></span> | |
| 1624 | + </span> | |
| 1625 | + <?php | |
| 1626 | + $badge_html = ob_get_clean(); | |
| 1627 | + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Adding notifications for submenu item. | |
| 1628 | + $submenu['sureforms_menu'][ $index ][0] .= $badge_html; | |
| 1629 | + break; | |
| 1630 | + } | |
| 1631 | + } | |
| 1632 | + } | |
| 323 | 1633 | } |
| 324 | 1634 | |
| 325 | 1635 | /** |
| 1636 | + * Mark the user's visit to the entries page. | |
| 1637 | + * | |
| 1638 | + * @since 1.7.3 | |
| 1639 | + * @return void | |
| 1640 | + */ | |
| 1641 | + public function mark_entries_page_visit() { | |
| 1642 | + if ( Helper::current_user_can() ) { | |
| 1643 | + $srfm_options = get_option( 'srfm_options', [] ); | |
| 1644 | + $srfm_options['entries_last_visited'] = time(); | |
| 1645 | + \SRFM\Inc\Helper::update_admin_settings_option( 'srfm_options', $srfm_options ); | |
| 1646 | + } | |
| 1647 | + } | |
| 1648 | + | |
| 1649 | + /** | |
| 326 | 1650 | * Adds a settings link to the plugin action links on the plugins page. |
| 327 | 1651 | * |
| 328 | 1652 | * @param array $links An array of plugin action links. |
| 329 | 1653 | * @param string $file The plugin file path. |
| @@ -331,15 +1655,22 @@ | ||
| 331 | 1655 | * @since 0.0.1 |
| 332 | 1656 | */ |
| 333 | 1657 | public function add_settings_link( $links, $file ) { |
| 334 | 1658 | if ( 'sureforms/sureforms.php' === $file ) { |
| 335 | - $plugin_links = apply_filters( | |
| 1659 | + ob_start(); | |
| 1660 | + ?> | |
| 1661 | + <a href="<?php echo esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ); ?>"> | |
| 1662 | + <?php echo esc_html__( 'Settings', 'sureforms' ); ?> | |
| 1663 | + </a> | |
| 1664 | + <?php | |
| 1665 | + $settings_link_html = ob_get_clean(); | |
| 1666 | + $plugin_links = apply_filters( | |
| 336 | 1667 | 'sureforms_plugin_action_links', |
| 337 | 1668 | [ |
| 338 | - 'sureforms_settings' => '<a href="' . esc_url( admin_url( 'admin.php?page=sureforms_form_settings&tab=general-settings' ) ) . '">' . esc_html__( 'Settings', 'sureforms' ) . '</a>', | |
| 1669 | + 'sureforms_settings' => $settings_link_html, | |
| 339 | 1670 | ] |
| 340 | 1671 | ); |
| 341 | - $links = array_merge( $plugin_links, $links ); | |
| 1672 | + $links = array_merge( $plugin_links, $links ); | |
| 342 | 1673 | } |
| 343 | 1674 | return $links; |
| 344 | 1675 | } |
| 345 | 1676 | |
| @@ -349,8 +1680,9 @@ | ||
| 349 | 1680 | * @since 0.0.1 |
| 350 | 1681 | */ |
| 351 | 1682 | public function enqueue_styles() { |
| 352 | 1683 | $current_screen = get_current_screen(); |
| 1684 | + global $wp_version; | |
| 353 | 1685 | |
| 354 | 1686 | $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; |
| 355 | 1687 | $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; |
| 356 | 1688 | |
| @@ -368,9 +1700,18 @@ | ||
| 368 | 1700 | wp_enqueue_style( SRFM_SLUG . '-backend-blocks', $css_uri . 'blocks/default/backend' . $file_prefix . '.css', [], SRFM_VER ); |
| 369 | 1701 | wp_enqueue_style( SRFM_SLUG . '-intl', $vendor_css_uri . 'intl/intlTelInput-backend.min.css', [], SRFM_VER ); |
| 370 | 1702 | wp_enqueue_style( SRFM_SLUG . '-common', $css_uri . 'common' . $file_prefix . '.css', [], SRFM_VER ); |
| 371 | 1703 | wp_enqueue_style( SRFM_SLUG . '-reactQuill', $vendor_css_uri . 'quill/quill.snow.css', [], SRFM_VER ); |
| 1704 | + wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS ); | |
| 372 | 1705 | wp_enqueue_style( SRFM_SLUG . '-single-form-modal', $css_uri . 'single-form-setting' . $file_prefix . '.css', [], SRFM_VER ); |
| 1706 | + | |
| 1707 | + // if version is equal to or lower than 6.6.2 then add compatibility css. | |
| 1708 | + if ( version_compare( $wp_version, '6.6.2', '<=' ) ) { | |
| 1709 | + $srfm_inline_css = '.srfm-settings-modal .srfm-setting-modal-container .components-toggle-control .components-base-control__help{ | |
| 1710 | + margin-left: 4em; | |
| 1711 | + }'; | |
| 1712 | + wp_add_inline_style( SRFM_SLUG . '-single-form-modal', $srfm_inline_css ); | |
| 1713 | + } | |
| 373 | 1714 | } |
| 374 | 1715 | |
| 375 | 1716 | wp_enqueue_style( SRFM_SLUG . '-form-selector', $css_uri . 'srfm-form-selector' . $file_prefix . '.css', [], SRFM_VER ); |
| 376 | 1717 | wp_enqueue_style( SRFM_SLUG . '-common-editor', SRFM_URL . 'assets/build/common-editor.css', [], SRFM_VER, 'all' ); |
| @@ -385,9 +1726,9 @@ | ||
| 385 | 1726 | public function get_breadcrumbs_for_current_page() { |
| 386 | 1727 | global $post, $pagenow; |
| 387 | 1728 | $breadcrumbs = []; |
| 388 | 1729 | |
| 389 | - if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 1730 | + if ( 'admin.php' === $pagenow && isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We don't need nonce verification here. | |
| 390 | 1731 | $page_title = get_admin_page_title(); |
| 391 | 1732 | $breadcrumbs[] = [ |
| 392 | 1733 | 'title' => $page_title, |
| 393 | 1734 | 'link' => '', |
| @@ -400,9 +1741,9 @@ | ||
| 400 | 1741 | 'title' => $post_type_plural, |
| 401 | 1742 | 'link' => admin_url( 'edit.php?post_type=' . $post_type_obj->name ), |
| 402 | 1743 | ]; |
| 403 | 1744 | |
| 404 | - if ( 'edit.php' === $pagenow && ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 1745 | + if ( 'edit.php' === $pagenow && ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- We don't need nonce verification here. | |
| 405 | 1746 | $breadcrumbs[ count( $breadcrumbs ) - 1 ]['link'] = ''; |
| 406 | 1747 | } else { |
| 407 | 1748 | $breadcrumbs[] = [ |
| 408 | 1749 | /* Translators: Post Title. */ |
| @@ -417,13 +1758,8 @@ | ||
| 417 | 1758 | $breadcrumbs[] = [ |
| 418 | 1759 | 'title' => 'Forms', |
| 419 | 1760 | 'link' => '', |
| 420 | 1761 | ]; |
| 421 | - } elseif ( $current_screen && 'sureforms_entry' === $current_screen->post_type ) { | |
| 422 | - $breadcrumbs[] = [ | |
| 423 | - 'title' => 'Entries', | |
| 424 | - 'link' => '', | |
| 425 | - ]; | |
| 426 | 1762 | } else { |
| 427 | 1763 | $breadcrumbs[] = [ |
| 428 | 1764 | 'title' => '', |
| 429 | 1765 | 'link' => '', |
| @@ -433,9 +1769,8 @@ | ||
| 433 | 1769 | |
| 434 | 1770 | return $breadcrumbs; |
| 435 | 1771 | } |
| 436 | 1772 | |
| 437 | - | |
| 438 | 1773 | /** |
| 439 | 1774 | * Enqueue Admin Scripts. |
| 440 | 1775 | * |
| 441 | 1776 | * @return void |
| @@ -442,39 +1777,159 @@ | ||
| 442 | 1777 | * @since 0.0.1 |
| 443 | 1778 | */ |
| 444 | 1779 | public function enqueue_scripts() { |
| 445 | 1780 | $current_screen = get_current_screen(); |
| 1781 | + global $wp_version; | |
| 446 | 1782 | |
| 447 | - $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; | |
| 448 | - $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; | |
| 449 | - $js_uri = SRFM_URL . 'assets/js/' . $dir_name . '/'; | |
| 450 | - $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/'; | |
| 1783 | + $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; | |
| 1784 | + $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; | |
| 1785 | + $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/'; | |
| 1786 | + $is_rtl = is_rtl(); | |
| 1787 | + $rtl = $is_rtl ? '-rtl' : ''; | |
| 451 | 1788 | |
| 452 | - /* RTL */ | |
| 453 | - if ( is_rtl() ) { | |
| 454 | - $file_prefix .= '-rtl'; | |
| 455 | - } | |
| 1789 | + /** | |
| 1790 | + * List of the handles in which we need to add translation compatibility. | |
| 1791 | + */ | |
| 1792 | + $script_translations_handlers = []; | |
| 1793 | + $onboarding_instance = Onboarding::get_instance(); | |
| 1794 | + $current_user = wp_get_current_user(); | |
| 456 | 1795 | |
| 457 | 1796 | $localization_data = [ |
| 458 | - 'site_url' => get_site_url(), | |
| 459 | - 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(), | |
| 460 | - 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ), | |
| 461 | - 'plugin_version' => SRFM_VER, | |
| 462 | - 'global_settings_nonce' => current_user_can( 'manage_options' ) ? wp_create_nonce( 'wp_rest' ) : '', | |
| 463 | - 'is_pro_active' => defined( 'SRFM_PRO_VER' ), | |
| 464 | - 'pro_plugin_version' => defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : '', | |
| 465 | - 'sureforms_pricing_page' => $this->get_sureforms_website_url( 'pricing' ), | |
| 466 | - 'field_spacing_vars' => Helper::get_css_vars(), | |
| 1797 | + 'site_url' => get_site_url(), | |
| 1798 | + 'current_user_login' => $current_user->user_login ?? '', | |
| 1799 | + 'website_lead_details' => [ | |
| 1800 | + 'first_name' => $current_user->first_name ?? '', | |
| 1801 | + 'last_name' => $current_user->last_name ?? '', | |
| 1802 | + 'email' => $current_user->user_email ?? '', | |
| 1803 | + ], | |
| 1804 | + 'breadcrumbs' => $this->get_breadcrumbs_for_current_page(), | |
| 1805 | + 'sureforms_dashboard_url' => admin_url( '/admin.php?page=sureforms_menu' ), | |
| 1806 | + 'plugin_version' => SRFM_VER, | |
| 1807 | + 'global_settings_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '', | |
| 1808 | + 'is_pro_active' => Helper::has_pro(), | |
| 1809 | + 'is_first_form_created' => self::is_first_form_created(), | |
| 1810 | + 'check_three_days_threshold' => self::check_first_form_creation_threshold(), | |
| 1811 | + 'check_eight_days_threshold' => self::check_first_form_creation_threshold( 8 ), | |
| 1812 | + 'pro_plugin_version' => Helper::has_pro() ? SRFM_PRO_VER : '', | |
| 1813 | + 'pro_plugin_name' => Helper::has_pro() && defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro', | |
| 1814 | + 'sureforms_pricing_page' => Helper::get_sureforms_website_url( 'pricing' ), | |
| 1815 | + 'field_spacing_vars' => Helper::get_css_vars(), | |
| 1816 | + 'is_ver_lower_than_6_7' => version_compare( $wp_version, '6.6.2', '<=' ), | |
| 1817 | + 'integrations' => Helper::sureforms_get_integration(), | |
| 1818 | + 'rotating_plugin_banner' => Helper::get_rotating_plugin_banner(), | |
| 1819 | + 'ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 1820 | + 'client_logs_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_client_logs' ) : '', | |
| 1821 | + 'action_items' => $this->get_action_items(), | |
| 1822 | + 'details_dialog' => $this->get_details_dialog_labels(), | |
| 1823 | + // Where Contact Support goes when the details fetch fails and there is | |
| 1824 | + // no category-tagged URL to use. Untagged, because at that point we do | |
| 1825 | + // not know which check sent them -- but still a way out: these notices | |
| 1826 | + // are not dismissible and Contact Support is the only action that | |
| 1827 | + // retires them. | |
| 1828 | + 'support_url' => $this->get_support_contact_url( '' ), | |
| 1829 | + 'action_item_details_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_action_item_details' ) : '', | |
| 1830 | + 'notice_response_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_notice_response' ) : '', | |
| 1831 | + 'dismiss_action_item_nonce' => Helper::current_user_can() ? wp_create_nonce( 'srfm_dismiss_action_item' ) : '', | |
| 1832 | + 'sf_plugin_manager_nonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ), | |
| 1833 | + 'plugin_installer_nonce' => wp_create_nonce( 'updates' ), | |
| 1834 | + 'plugin_activating_text' => __( 'Activating...', 'sureforms' ), | |
| 1835 | + 'plugin_activated_text' => __( 'Activated', 'sureforms' ), | |
| 1836 | + 'plugin_activate_text' => __( 'Activate', 'sureforms' ), | |
| 1837 | + 'plugin_installing_text' => __( 'Installing...', 'sureforms' ), | |
| 1838 | + 'plugin_installed_text' => __( 'Installed', 'sureforms' ), | |
| 1839 | + 'privacy_policy_url' => Helper::get_sureforms_website_url( 'privacy-policy/' ), | |
| 1840 | + 'is_rtl' => $is_rtl, | |
| 1841 | + 'onboarding_completed' => method_exists( $onboarding_instance, 'get_onboarding_status' ) ? $onboarding_instance->get_onboarding_status() : false, | |
| 1842 | + 'migration_banner_dismissed' => method_exists( $onboarding_instance, 'is_migration_banner_dismissed' ) ? $onboarding_instance->is_migration_banner_dismissed() : false, | |
| 1843 | + 'migration_settings_url' => admin_url( 'admin.php?page=sureforms_form_settings&tab=migration-settings' ), | |
| 1844 | + 'onboarding_redirect' => isset( $_GET['srfm-activation-redirect'] ), // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce is not required for the activation redirection. | |
| 1845 | + 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ), | |
| 1846 | + 'general_settings_url' => admin_url( '/options-general.php' ), | |
| 1847 | + 'additional_header_nav_items' => [], | |
| 1848 | + // Smart tags for the Global Defaults email notification fields. | |
| 1849 | + // srfm_block_data is only available in the block editor; these keys | |
| 1850 | + // make the same data accessible on the settings page via srfm_admin. | |
| 1851 | + 'smart_tags_array' => Smart_Tags::smart_tag_list(), | |
| 1852 | + 'smart_tags_array_email' => Smart_Tags::email_smart_tag_list(), | |
| 1853 | + // Default confirmation message HTML (icon + heading + text) used as | |
| 1854 | + // the initial React state before the settings API response arrives. | |
| 1855 | + 'default_confirmation_message' => Global_Settings::get_default_confirmation_message(), | |
| 1856 | + 'payments' => apply_filters( | |
| 1857 | + 'srfm_admin_localize_payments_data', | |
| 1858 | + [ | |
| 1859 | + 'stripe_connected' => Stripe_Helper::is_stripe_connected(), | |
| 1860 | + 'stripe_mode' => Stripe_Helper::get_stripe_mode(), | |
| 1861 | + 'stripe_connect_url' => Stripe_Helper::get_stripe_settings_url(), | |
| 1862 | + 'currencies_data' => Payment_Helper::get_all_currencies_data(), | |
| 1863 | + 'zero_decimal_currencies' => Payment_Helper::get_zero_decimal_currencies(), | |
| 1864 | + 'webhook_url' => Stripe_Helper::get_webhook_url(), | |
| 1865 | + 'webhook_test_connected' => Stripe_Helper::is_webhook_configured( 'test', true ), | |
| 1866 | + 'webhook_live_connected' => Stripe_Helper::is_webhook_configured( 'live', true ), | |
| 1867 | + 'is_transaction_present' => Stripe_Helper::is_transaction_present(), | |
| 1868 | + 'payment_currency' => Payment_Helper::get_currency(), | |
| 1869 | + 'currency_sign_position' => Payment_Helper::get_currency_sign_position(), | |
| 1870 | + ] | |
| 1871 | + ), | |
| 1872 | + 'mcp_adapter_status' => file_exists( WP_PLUGIN_DIR . '/mcp-adapter/mcp-adapter.php' ) | |
| 1873 | + ? ( is_plugin_active( 'mcp-adapter/mcp-adapter.php' ) ? 'active' : 'installed' ) | |
| 1874 | + : 'not_installed', | |
| 1875 | + 'mcp_endpoint_url' => esc_url_raw( rest_url( 'sureforms/v1/mcp' ) ), | |
| 467 | 1876 | ]; |
| 468 | 1877 | |
| 469 | - if ( class_exists( 'SRFM_PRO\Admin\Licensing' ) ) { | |
| 470 | - $license_active = \SRFM_PRO\Admin\Licensing::is_license_active(); | |
| 471 | - $localization_data['is_license_active'] = $license_active; | |
| 1878 | + $is_screen_sureforms_menu = Helper::validate_request_context( 'sureforms_menu', 'page' ); | |
| 1879 | + $is_screen_add_new_form = Helper::validate_request_context( 'add-new-form', 'page' ); | |
| 1880 | + $is_screen_sureforms_forms = Helper::validate_request_context( 'sureforms_forms', 'page' ); | |
| 1881 | + $is_screen_sureforms_form_settings = Helper::validate_request_context( 'sureforms_form_settings', 'page' ); | |
| 1882 | + $is_screen_sureforms_payments = Helper::validate_request_context( 'sureforms_payments', 'page' ); | |
| 1883 | + $is_screen_sureforms_entries = Helper::validate_request_context( SRFM_ENTRIES, 'page' ); | |
| 1884 | + $is_screen_sureforms_learn = Helper::validate_request_context( 'sureforms_learn', 'page' ); | |
| 1885 | + $is_screen_quiz_empty_state = Helper::validate_request_context( 'sureforms_quiz_entries', 'page' ); | |
| 1886 | + $is_screen_survey_empty_state = Helper::validate_request_context( 'sureforms_survey_reports', 'page' ); | |
| 1887 | + $is_screen_partial_entries_empty_state = Helper::validate_request_context( 'sureforms_partial_entries', 'page' ); | |
| 1888 | + $is_post_type_sureforms_form = SRFM_FORMS_POST_TYPE === $current_screen->post_type; | |
| 1889 | + | |
| 1890 | + /** | |
| 1891 | + * Check if the current screen is the SureForms Menu and AI Auth Email is present then we will add user type as registered. | |
| 1892 | + * Compatibility with existing UI code that checks for this condition. | |
| 1893 | + */ | |
| 1894 | + if ( $is_screen_sureforms_menu ) { | |
| 1895 | + // If email is stored send the user type as registered else non-registered. | |
| 1896 | + $localization_data['srfm_ai_details'] = [ | |
| 1897 | + 'type' => ! empty( get_option( 'srfm_ai_auth_user_email' ) ) ? 'registered' : 'non-registered', | |
| 1898 | + ]; | |
| 472 | 1899 | } |
| 473 | 1900 | |
| 474 | - if ( SRFM_FORMS_POST_TYPE === $current_screen->post_type || 'toplevel_page_sureforms_menu' === $current_screen->base || SRFM_ENTRIES_POST_TYPE === $current_screen->post_type | |
| 475 | - || 'sureforms_page_sureforms_form_settings' === $current_screen->id || 'sureforms_page_sureforms_entries' === $current_screen->id | |
| 476 | - ) { | |
| 1901 | + // Add the Quizzes and Survey Reports nav items when pro is not active. | |
| 1902 | + if ( ! Helper::has_pro() ) { | |
| 1903 | + $localization_data['additional_header_nav_items'][] = [ | |
| 1904 | + 'slug' => 'sureforms_quiz_entries', | |
| 1905 | + 'text' => __( 'Quizzes', 'sureforms' ), | |
| 1906 | + 'link' => admin_url( 'admin.php?page=sureforms_quiz_entries' ), | |
| 1907 | + ]; | |
| 1908 | + $localization_data['additional_header_nav_items'][] = [ | |
| 1909 | + 'slug' => 'sureforms_survey_reports', | |
| 1910 | + 'text' => __( 'Survey Reports', 'sureforms' ), | |
| 1911 | + 'link' => admin_url( 'admin.php?page=sureforms_survey_reports' ), | |
| 1912 | + ]; | |
| 1913 | + $localization_data['additional_header_nav_items'][] = [ | |
| 1914 | + 'slug' => 'sureforms_partial_entries', | |
| 1915 | + 'text' => __( 'Partial Entries', 'sureforms' ), | |
| 1916 | + 'link' => admin_url( 'admin.php?page=sureforms_partial_entries' ), | |
| 1917 | + ]; | |
| 1918 | + } | |
| 1919 | + | |
| 1920 | + $is_sureforms_screen = $is_screen_sureforms_menu || $is_post_type_sureforms_form || $is_screen_add_new_form || $is_screen_sureforms_forms || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_screen_sureforms_payments || $is_screen_sureforms_learn || $is_screen_quiz_empty_state || $is_screen_survey_empty_state || $is_screen_partial_entries_empty_state; | |
| 1921 | + | |
| 1922 | + /** | |
| 1923 | + * Filter to allow extending the SureForms dashboard screen check. | |
| 1924 | + * | |
| 1925 | + * @since 2.6.0 | |
| 1926 | + * | |
| 1927 | + * @param bool $is_sureforms_screen Whether the current screen is a SureForms dashboard screen. | |
| 1928 | + */ | |
| 1929 | + $is_sureforms_screen = apply_filters( 'srfm_is_dashboard_screen', $is_sureforms_screen ); | |
| 1930 | + | |
| 1931 | + if ( $is_sureforms_screen ) { | |
| 477 | 1932 | $asset_handle = '-dashboard'; |
| 478 | 1933 | |
| 479 | 1934 | wp_enqueue_style( SRFM_SLUG . $asset_handle . '-font', 'https://fonts.googleapis.com/css2?family=Inter:wght@400;500&display=swap', [], SRFM_VER ); |
| 480 | 1935 | |
| @@ -488,9 +1943,24 @@ | ||
| 488 | 1943 | wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/dashboard.js', $script_info['dependencies'], SRFM_VER, true ); |
| 489 | 1944 | |
| 490 | 1945 | wp_localize_script( SRFM_SLUG . $asset_handle, 'scIcons', [ 'path' => SRFM_URL . 'assets/build/icon-assets' ] ); |
| 491 | 1946 | |
| 492 | - $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings' ); | |
| 1947 | + $script_translations_handlers[] = SRFM_SLUG . $asset_handle; | |
| 1948 | + | |
| 1949 | + if ( class_exists( 'SRFM_PRO\Admin\Licensing' ) ) { | |
| 1950 | + $license_active = \SRFM_PRO\Admin\Licensing::is_license_active(); | |
| 1951 | + $localization_data['is_license_active'] = $license_active; | |
| 1952 | + | |
| 1953 | + // Updating current licensing status. | |
| 1954 | + $srfm_pro_license_status = get_option( 'srfm_pro_license_status', '' ); | |
| 1955 | + $current_license_status = $license_active ? 'licensed' : 'unlicensed'; | |
| 1956 | + if ( $current_license_status !== $srfm_pro_license_status ) { | |
| 1957 | + update_option( 'srfm_pro_license_status', $current_license_status ); | |
| 1958 | + } | |
| 1959 | + } | |
| 1960 | + | |
| 1961 | + $localization_data['security_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=security-settings&subpage=recaptcha' ); | |
| 1962 | + $localization_data['integration_settings_url'] = admin_url( '/admin.php?page=sureforms_form_settings&tab=integration-settings' ); | |
| 493 | 1963 | wp_localize_script( |
| 494 | 1964 | SRFM_SLUG . $asset_handle, |
| 495 | 1965 | SRFM_SLUG . '_admin', |
| 496 | 1966 | apply_filters( |
| @@ -498,39 +1968,170 @@ | ||
| 498 | 1968 | $localization_data |
| 499 | 1969 | ) |
| 500 | 1970 | ); |
| 501 | 1971 | wp_enqueue_style( SRFM_SLUG . '-dashboard', SRFM_URL . 'assets/build/dashboard.css', [], SRFM_VER, 'all' ); |
| 502 | - | |
| 503 | 1972 | } |
| 504 | 1973 | |
| 505 | - if ( 'sureforms_page_sureforms_form_settings' === $current_screen->id ) { | |
| 506 | - wp_enqueue_style( SRFM_SLUG . '-settings', $css_uri . 'backend/settings' . $file_prefix . '.css', [], SRFM_VER ); | |
| 1974 | + if ( $is_screen_sureforms_form_settings || $is_screen_sureforms_forms ) { | |
| 1975 | + wp_enqueue_style( SRFM_SLUG . '-settings', $css_uri . 'backend/settings' . $file_prefix . $rtl . '.css', [], SRFM_VER ); | |
| 507 | 1976 | } |
| 508 | 1977 | |
| 509 | 1978 | // Enqueue styles for the entries page. |
| 510 | - if ( 'sureforms_page_sureforms_entries' === $current_screen->id ) { | |
| 1979 | + if ( $is_screen_sureforms_entries ) { | |
| 511 | 1980 | $asset_handle = '-entries'; |
| 512 | - wp_enqueue_style( SRFM_SLUG . $asset_handle, $css_uri . 'backend/entries' . $file_prefix . '.css', [], SRFM_VER ); | |
| 513 | 1981 | wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/entries.js', $script_info['dependencies'], SRFM_VER, true ); |
| 1982 | + | |
| 1983 | + wp_localize_script( | |
| 1984 | + SRFM_SLUG . $asset_handle, | |
| 1985 | + SRFM_SLUG . '_admin', | |
| 1986 | + apply_filters( | |
| 1987 | + SRFM_SLUG . '_admin_filter', | |
| 1988 | + $localization_data | |
| 1989 | + ) | |
| 1990 | + ); | |
| 1991 | + $script_translations_handlers[] = SRFM_SLUG . $asset_handle; | |
| 514 | 1992 | } |
| 515 | 1993 | |
| 516 | - // Admin Submenu Styles. | |
| 517 | - wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . '.css', [], SRFM_VER ); | |
| 1994 | + // Enqueue scripts for the learn page. | |
| 1995 | + if ( $is_screen_sureforms_learn ) { | |
| 1996 | + $asset_handle = '-learn'; | |
| 1997 | + wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/learn.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 518 | 1998 | |
| 519 | - if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id || 'edit-' . SRFM_ENTRIES_POST_TYPE === $current_screen->id ) { | |
| 520 | - $asset_handle = 'page_header'; | |
| 1999 | + wp_localize_script( | |
| 2000 | + SRFM_SLUG . $asset_handle, | |
| 2001 | + SRFM_SLUG . '_admin', | |
| 2002 | + apply_filters( | |
| 2003 | + SRFM_SLUG . '_admin_filter', | |
| 2004 | + $localization_data | |
| 2005 | + ) | |
| 2006 | + ); | |
| 2007 | + $script_translations_handlers[] = SRFM_SLUG . $asset_handle; | |
| 2008 | + } | |
| 521 | 2009 | |
| 2010 | + // Enqueue scripts for the forms page. | |
| 2011 | + if ( $is_screen_sureforms_forms ) { | |
| 2012 | + $asset_handle = '-forms'; | |
| 2013 | + | |
| 2014 | + $script_asset_path = SRFM_DIR . 'assets/build/forms.asset.php'; | |
| 2015 | + $script_info = file_exists( $script_asset_path ) | |
| 2016 | + ? include $script_asset_path | |
| 2017 | + : [ | |
| 2018 | + 'dependencies' => [], | |
| 2019 | + 'version' => SRFM_VER, | |
| 2020 | + ]; | |
| 2021 | + | |
| 2022 | + wp_enqueue_script( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 2023 | + wp_localize_script( | |
| 2024 | + SRFM_SLUG . $asset_handle, | |
| 2025 | + SRFM_SLUG . '_admin', | |
| 2026 | + apply_filters( | |
| 2027 | + SRFM_SLUG . '_admin_filter', | |
| 2028 | + $localization_data | |
| 2029 | + ) | |
| 2030 | + ); | |
| 2031 | + wp_enqueue_style( SRFM_SLUG . $asset_handle, SRFM_URL . 'assets/build/forms.css', [], SRFM_VER, 'all' ); | |
| 2032 | + | |
| 2033 | + $script_translations_handlers[] = SRFM_SLUG . $asset_handle; | |
| 2034 | + } | |
| 2035 | + | |
| 2036 | + // Enqueue scripts for the SureMail promotional page. | |
| 2037 | + $is_screen_sureforms_smtp = Helper::validate_request_context( 'sureforms_smtp', 'page' ); | |
| 2038 | + if ( $is_screen_sureforms_smtp ) { | |
| 2039 | + $asset_handle = 'suremail'; | |
| 2040 | + | |
| 522 | 2041 | $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php'; |
| 523 | 2042 | $script_info = file_exists( $script_asset_path ) |
| 524 | - ? include $script_asset_path | |
| 525 | - : [ | |
| 526 | - 'dependencies' => [], | |
| 527 | - 'version' => SRFM_VER, | |
| 2043 | + ? include $script_asset_path | |
| 2044 | + : [ | |
| 2045 | + 'dependencies' => [], | |
| 2046 | + 'version' => SRFM_VER, | |
| 2047 | + ]; | |
| 2048 | + | |
| 2049 | + wp_enqueue_script( SRFM_SLUG . '-suremail', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 2050 | + wp_enqueue_style( SRFM_SLUG . '-suremail', SRFM_URL . 'assets/build/suremail.css', [], SRFM_VER, 'all' ); | |
| 2051 | + | |
| 2052 | + // Localize script for SureMail page. | |
| 2053 | + $suremail_localization_data = [ | |
| 2054 | + 'ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 2055 | + 'admin_url' => admin_url(), | |
| 2056 | + 'suremail_url' => 'https://sureforms.com/suremail/', | |
| 2057 | + 'plugin_installer_nonce' => wp_create_nonce( 'updates' ), | |
| 2058 | + 'sfPluginManagerNonce' => wp_create_nonce( 'sf_plugin_manager_nonce' ), | |
| 2059 | + 'suremail_status' => file_exists( WP_PLUGIN_DIR . '/suremails/suremails.php' ) | |
| 2060 | + ? ( is_plugin_active( 'suremails/suremails.php' ) ? 'active' : 'installed' ) | |
| 2061 | + : 'not_installed', | |
| 528 | 2062 | ]; |
| 529 | - wp_enqueue_script( SRFM_SLUG . '-form-page-header', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 530 | - wp_enqueue_style( SRFM_SLUG . '-form-archive-styles', $css_uri . 'form-archive-styles' . $file_prefix . '.css', [], SRFM_VER ); | |
| 2063 | + | |
| 2064 | + wp_localize_script( | |
| 2065 | + SRFM_SLUG . '-suremail', | |
| 2066 | + SRFM_SLUG . '_admin', | |
| 2067 | + apply_filters( | |
| 2068 | + SRFM_SLUG . '_suremail_admin_filter', | |
| 2069 | + $suremail_localization_data | |
| 2070 | + ) | |
| 2071 | + ); | |
| 2072 | + | |
| 2073 | + $script_translations_handlers[] = SRFM_SLUG . '-suremail'; | |
| 531 | 2074 | } |
| 532 | - if ( 'sureforms_page_' . SRFM_FORMS_POST_TYPE . '_settings' === $current_screen->base ) { | |
| 2075 | + | |
| 2076 | + // Enqueue scripts for the Quiz empty state page (free users only). | |
| 2077 | + if ( $is_screen_quiz_empty_state && ! Helper::has_pro() ) { | |
| 2078 | + $asset_handle = 'quizEmptyState'; | |
| 2079 | + | |
| 2080 | + $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php'; | |
| 2081 | + $script_info = file_exists( $script_asset_path ) | |
| 2082 | + ? include $script_asset_path | |
| 2083 | + : [ | |
| 2084 | + 'dependencies' => [], | |
| 2085 | + 'version' => SRFM_VER, | |
| 2086 | + ]; | |
| 2087 | + | |
| 2088 | + wp_enqueue_script( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 2089 | + wp_enqueue_style( SRFM_SLUG . '-quiz-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' ); | |
| 2090 | + | |
| 2091 | + $script_translations_handlers[] = SRFM_SLUG . '-quiz-empty-state'; | |
| 2092 | + } | |
| 2093 | + | |
| 2094 | + // Enqueue scripts for the Survey Reports empty state page (free users only). | |
| 2095 | + if ( $is_screen_survey_empty_state && ! Helper::has_pro() ) { | |
| 2096 | + $asset_handle = 'surveyEmptyState'; | |
| 2097 | + | |
| 2098 | + $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php'; | |
| 2099 | + $script_info = file_exists( $script_asset_path ) | |
| 2100 | + ? include $script_asset_path | |
| 2101 | + : [ | |
| 2102 | + 'dependencies' => [], | |
| 2103 | + 'version' => SRFM_VER, | |
| 2104 | + ]; | |
| 2105 | + | |
| 2106 | + wp_enqueue_script( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 2107 | + wp_enqueue_style( SRFM_SLUG . '-survey-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' ); | |
| 2108 | + | |
| 2109 | + $script_translations_handlers[] = SRFM_SLUG . '-survey-empty-state'; | |
| 2110 | + } | |
| 2111 | + | |
| 2112 | + // Enqueue scripts for the Partial Entries empty state page (free users only). | |
| 2113 | + if ( $is_screen_partial_entries_empty_state && ! Helper::has_pro() ) { | |
| 2114 | + $asset_handle = 'partialEntriesEmptyState'; | |
| 2115 | + | |
| 2116 | + $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php'; | |
| 2117 | + $script_info = file_exists( $script_asset_path ) | |
| 2118 | + ? include $script_asset_path | |
| 2119 | + : [ | |
| 2120 | + 'dependencies' => [], | |
| 2121 | + 'version' => SRFM_VER, | |
| 2122 | + ]; | |
| 2123 | + | |
| 2124 | + wp_enqueue_script( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); | |
| 2125 | + wp_enqueue_style( SRFM_SLUG . '-partial-entries-empty-state', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [], SRFM_VER, 'all' ); | |
| 2126 | + | |
| 2127 | + $script_translations_handlers[] = SRFM_SLUG . '-partial-entries-empty-state'; | |
| 2128 | + } | |
| 2129 | + | |
| 2130 | + // Admin Submenu Styles. | |
| 2131 | + wp_enqueue_style( SRFM_SLUG . '-admin', $css_uri . 'backend/admin' . $file_prefix . $rtl . '.css', [], SRFM_VER ); | |
| 2132 | + | |
| 2133 | + if ( $is_screen_sureforms_form_settings ) { | |
| 533 | 2134 | $asset_handle = 'settings'; |
| 534 | 2135 | |
| 535 | 2136 | $script_asset_path = SRFM_DIR . 'assets/build/' . $asset_handle . '.asset.php'; |
| 536 | 2137 | $script_info = file_exists( $script_asset_path ) |
| @@ -540,55 +2141,28 @@ | ||
| 540 | 2141 | 'version' => SRFM_VER, |
| 541 | 2142 | ]; |
| 542 | 2143 | |
| 543 | 2144 | wp_enqueue_script( SRFM_SLUG . '-settings', SRFM_URL . 'assets/build/' . $asset_handle . '.js', $script_info['dependencies'], SRFM_VER, true ); |
| 544 | - wp_enqueue_style( SRFM_SLUG . '-setting-styles', SRFM_URL . 'assets/build/' . $asset_handle . '.css', [ 'wp-components' ], SRFM_VER, 'all' ); | |
| 545 | 2145 | wp_localize_script( |
| 546 | 2146 | SRFM_SLUG . '-settings', |
| 547 | 2147 | SRFM_SLUG . '_admin', |
| 548 | - $localization_data | |
| 2148 | + apply_filters( | |
| 2149 | + SRFM_SLUG . '_admin_filter', | |
| 2150 | + $localization_data | |
| 2151 | + ) | |
| 549 | 2152 | ); |
| 550 | - } | |
| 551 | - if ( 'edit-' . SRFM_FORMS_POST_TYPE === $current_screen->id ) { | |
| 552 | - wp_enqueue_script( SRFM_SLUG . '-form-archive', $js_uri . 'form-archive' . $file_prefix . '.js', [], SRFM_VER, true ); | |
| 553 | - wp_enqueue_script( SRFM_SLUG . '-export', $js_uri . 'export' . $file_prefix . '.js', [ 'wp-i18n' ], SRFM_VER, true ); | |
| 554 | - wp_localize_script( | |
| 555 | - SRFM_SLUG . '-export', | |
| 556 | - SRFM_SLUG . '_export', | |
| 557 | - [ | |
| 558 | - 'ajaxurl' => admin_url( 'admin-ajax.php' ), | |
| 559 | - 'srfm_export_nonce' => wp_create_nonce( 'export_form_nonce' ), | |
| 560 | - 'site_url' => get_site_url(), | |
| 561 | - 'srfm_import_endpoint' => '/wp-json/sureforms/v1/sureforms_import', | |
| 562 | - 'import_form_nonce' => ( current_user_can( 'edit_posts' ) ) ? wp_create_nonce( 'wp_rest' ) : '', | |
| 563 | - ] | |
| 564 | - ); | |
| 565 | 2153 | |
| 566 | - wp_enqueue_script( SRFM_SLUG . '-backend', $js_uri . 'backend' . $file_prefix . '.js', [], SRFM_VER, true ); | |
| 567 | - wp_localize_script( | |
| 568 | - SRFM_SLUG . '-backend', | |
| 569 | - SRFM_SLUG . '_backend', | |
| 570 | - [ | |
| 571 | - 'site_url' => get_site_url(), | |
| 572 | - ] | |
| 573 | - ); | |
| 2154 | + // Enqueue Tailwind and Quill editor styles for the settings page. | |
| 2155 | + wp_enqueue_style( SRFM_SLUG . '-settings-build', SRFM_URL . 'assets/build/settings.css', [], SRFM_VER, 'all' ); | |
| 2156 | + wp_enqueue_style( SRFM_SLUG . '-reactQuill', SRFM_URL . 'assets/css/minified/deps/quill/quill.snow.css', [], SRFM_VER ); | |
| 2157 | + wp_add_inline_style( SRFM_SLUG . '-reactQuill', self::QUILL_1X_INLINE_CSS ); | |
| 574 | 2158 | |
| 2159 | + $script_translations_handlers[] = SRFM_SLUG . '-settings'; | |
| 575 | 2160 | } |
| 576 | 2161 | |
| 577 | - if ( 'sureforms_page_add-new-form' === $current_screen->id ) { | |
| 2162 | + if ( $is_screen_add_new_form ) { | |
| 2163 | + wp_enqueue_style( SRFM_SLUG . '-template-picker', $css_uri . 'template-picker' . $file_prefix . $rtl . '.css', [], SRFM_VER ); | |
| 578 | 2164 | |
| 579 | - $file_prefix = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? '' : '.min'; | |
| 580 | - $dir_name = defined( 'SRFM_DEBUG' ) && SRFM_DEBUG ? 'unminified' : 'minified'; | |
| 581 | - | |
| 582 | - $css_uri = SRFM_URL . 'assets/css/' . $dir_name . '/'; | |
| 583 | - | |
| 584 | - /* RTL */ | |
| 585 | - if ( is_rtl() ) { | |
| 586 | - $file_prefix .= '-rtl'; | |
| 587 | - } | |
| 588 | - | |
| 589 | - wp_enqueue_style( SRFM_SLUG . '-template-picker', $css_uri . 'template-picker' . $file_prefix . '.css', [], SRFM_VER ); | |
| 590 | - | |
| 591 | 2165 | $sureforms_admin = 'templatePicker'; |
| 592 | 2166 | |
| 593 | 2167 | $script_asset_path = SRFM_DIR . 'assets/build/' . $sureforms_admin . '.asset.php'; |
| 594 | 2168 | $script_info = file_exists( $script_asset_path ) |
| @@ -604,20 +2178,22 @@ | ||
| 604 | 2178 | SRFM_SLUG . '_admin', |
| 605 | 2179 | [ |
| 606 | 2180 | 'site_url' => get_site_url(), |
| 607 | 2181 | 'plugin_url' => SRFM_URL, |
| 608 | - 'preview_images_url' => SRFM_URL . 'images/template-previews/', | |
| 609 | 2182 | 'admin_url' => admin_url( 'admin.php' ), |
| 610 | 2183 | 'new_template_picker_base_url' => admin_url( 'post-new.php?post_type=sureforms_form' ), |
| 611 | - 'capability' => current_user_can( 'edit_posts' ), | |
| 612 | - 'template_picker_nonce' => current_user_can( 'edit_posts' ) ? wp_create_nonce( 'wp_rest' ) : '', | |
| 613 | - 'is_pro_active' => defined( 'SRFM_PRO_VER' ), | |
| 2184 | + 'capability' => Helper::current_user_can(), | |
| 2185 | + 'template_picker_nonce' => Helper::current_user_can() ? wp_create_nonce( 'wp_rest' ) : '', | |
| 2186 | + 'is_pro_active' => Helper::has_pro(), | |
| 614 | 2187 | 'srfm_ai_usage_details' => AI_Helper::get_current_usage_details(), |
| 615 | 2188 | 'is_pro_license_active' => AI_Helper::is_pro_license_active(), |
| 616 | 2189 | 'srfm_ai_auth_user_email' => get_option( 'srfm_ai_auth_user_email' ), |
| 617 | - 'pricing_page_url' => $this->get_sureforms_website_url( 'pricing' ), | |
| 2190 | + 'pricing_page_url' => Helper::get_sureforms_website_url( 'pricing' ), | |
| 2191 | + 'licensing_nonce' => wp_create_nonce( 'srfm_pro_licensing_nonce' ), | |
| 618 | 2192 | ] |
| 619 | 2193 | ); |
| 2194 | + | |
| 2195 | + $script_translations_handlers[] = SRFM_SLUG . '-template-picker'; | |
| 620 | 2196 | } |
| 621 | 2197 | // Quick action sidebar. |
| 622 | 2198 | $default_allowed_quick_sidebar_blocks = apply_filters( |
| 623 | 2199 | 'srfm_quick_sidebar_allowed_blocks', |
| @@ -628,8 +2204,9 @@ | ||
| 628 | 2204 | 'srfm/checkbox', |
| 629 | 2205 | 'srfm/number', |
| 630 | 2206 | 'srfm/inline-button', |
| 631 | 2207 | 'srfm/advanced-heading', |
| 2208 | + 'srfm/payment', | |
| 632 | 2209 | ] |
| 633 | 2210 | ); |
| 634 | 2211 | if ( ! is_array( $default_allowed_quick_sidebar_blocks ) ) { |
| 635 | 2212 | $default_allowed_quick_sidebar_blocks = []; |
| @@ -641,41 +2218,80 @@ | ||
| 641 | 2218 | } |
| 642 | 2219 | $quick_sidebar_allowed_blocks = get_option( 'srfm_quick_sidebar_allowed_blocks' ); |
| 643 | 2220 | $quick_sidebar_allowed_blocks = ! empty( $quick_sidebar_allowed_blocks ) && is_array( $quick_sidebar_allowed_blocks ) ? $quick_sidebar_allowed_blocks : $default_allowed_quick_sidebar_blocks; |
| 644 | 2221 | $srfm_ajax_nonce = wp_create_nonce( 'srfm_ajax_nonce' ); |
| 645 | - wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true ); | |
| 646 | - wp_localize_script( | |
| 647 | - SRFM_SLUG . '-quick-action-siderbar', | |
| 648 | - SRFM_SLUG . '_quick_sidebar_blocks', | |
| 649 | - [ | |
| 650 | - 'allowed_blocks' => $quick_sidebar_allowed_blocks, | |
| 651 | - 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar, | |
| 652 | - 'srfm_ajax_nonce' => $srfm_ajax_nonce, | |
| 653 | - 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 654 | - ] | |
| 655 | - ); | |
| 656 | 2222 | |
| 2223 | + if ( Helper::is_sureforms_admin_page() ) { | |
| 2224 | + wp_enqueue_script( SRFM_SLUG . '-quick-action-siderbar', SRFM_URL . 'assets/build/quickActionSidebar.js', [], SRFM_VER, true ); | |
| 2225 | + wp_localize_script( | |
| 2226 | + SRFM_SLUG . '-quick-action-siderbar', | |
| 2227 | + SRFM_SLUG . '_quick_sidebar_blocks', | |
| 2228 | + [ | |
| 2229 | + 'allowed_blocks' => $quick_sidebar_allowed_blocks, | |
| 2230 | + 'srfm_enable_quick_action_sidebar' => $srfm_enable_quick_action_sidebar, | |
| 2231 | + 'srfm_ajax_nonce' => $srfm_ajax_nonce, | |
| 2232 | + 'srfm_ajax_url' => admin_url( 'admin-ajax.php' ), | |
| 2233 | + ] | |
| 2234 | + ); | |
| 2235 | + | |
| 2236 | + $script_translations_handlers[] = SRFM_SLUG . '-quick-action-siderbar'; | |
| 2237 | + } | |
| 2238 | + | |
| 657 | 2239 | /** |
| 658 | 2240 | * Enqueuing SureTriggers Integration script. |
| 659 | 2241 | * This script loads suretriggers iframe in Intergations tab. |
| 660 | 2242 | */ |
| 661 | - if ( SRFM_FORMS_POST_TYPE === $current_screen->post_type ) { | |
| 662 | - wp_enqueue_script( SRFM_SLUG . '-suretriggers-integration', SRFM_SURETRIGGERS_INTERGATION_BASE_URL . 'js/v2/embed.js', [], SRFM_VER, true ); | |
| 2243 | + if ( $is_post_type_sureforms_form ) { | |
| 2244 | + wp_enqueue_script( SRFM_SLUG . '-suretriggers-integration', SRFM_SURETRIGGERS_INTEGRATION_BASE_URL . 'js/v2/embed.js', [], SRFM_VER, true ); | |
| 663 | 2245 | } |
| 2246 | + | |
| 2247 | + // Check $script_translations_handlers is not empty before calling the function. | |
| 2248 | + if ( ! empty( $script_translations_handlers ) ) { | |
| 2249 | + // Remove duplicates values from the array. | |
| 2250 | + $script_translations_handlers = array_unique( $script_translations_handlers ); | |
| 2251 | + | |
| 2252 | + foreach ( $script_translations_handlers as $script_handle ) { | |
| 2253 | + Helper::register_script_translations( $script_handle ); | |
| 2254 | + } | |
| 2255 | + } | |
| 664 | 2256 | } |
| 665 | 2257 | |
| 666 | 2258 | /** |
| 667 | 2259 | * Form Template Picker Admin Body Classes |
| 2260 | + * WordPress sometimes translates class names in the admin body tag, which can result in | |
| 2261 | + * incorrect or missing class names when rendering the admin pages. This function ensures | |
| 2262 | + * that essential class names are manually added to the body tag to maintain proper functionality. | |
| 668 | 2263 | * |
| 669 | 2264 | * @since 0.0.1 |
| 670 | 2265 | * @param string $classes Space separated class string. |
| 671 | 2266 | */ |
| 672 | 2267 | public function admin_template_picker_body_class( $classes = '' ) { |
| 673 | - $theme_builder_class = isset( $_GET['page'] ) && 'add-new-form' === $_GET['page'] ? 'srfm-template-picker' : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Fetching a $_GET value, no nonce available to validate. | |
| 674 | - $classes .= ' ' . $theme_builder_class . ' '; | |
| 2268 | + // Define an associative array of class names and their corresponding conditions. | |
| 2269 | + // Each condition checks whether a specific request context matches. | |
| 2270 | + $srfm_classes = [ | |
| 2271 | + 'sureforms_page_sureforms_entries' => Helper::validate_request_context( SRFM_ENTRIES, 'page' ), | |
| 2272 | + 'sureforms_page_sureforms_form_settings' => Helper::validate_request_context( 'sureforms_form_settings', 'page' ), | |
| 2273 | + 'srfm-template-picker' => Helper::validate_request_context( 'add-new-form', 'page' ), | |
| 2274 | + ]; | |
| 675 | 2275 | |
| 2276 | + $add_srfm_classes = ''; | |
| 2277 | + | |
| 2278 | + // Loop through the defined classes and conditions. | |
| 2279 | + foreach ( $srfm_classes as $class => $condition ) { | |
| 2280 | + // Check if the condition evaluates to true. | |
| 2281 | + if ( $condition ) { | |
| 2282 | + // Append the class to the existing classes string, followed by a space. | |
| 2283 | + $add_srfm_classes .= empty( $add_srfm_classes ) ? $class : ' ' . $class; | |
| 2284 | + } | |
| 2285 | + } | |
| 2286 | + | |
| 2287 | + // Append the new classes to the existing classes string. | |
| 2288 | + if ( ! empty( $add_srfm_classes ) ) { | |
| 2289 | + $classes .= ' ' . $add_srfm_classes; | |
| 2290 | + } | |
| 2291 | + | |
| 2292 | + // Return the updated list of classes. | |
| 676 | 2293 | return $classes; |
| 677 | - | |
| 678 | 2294 | } |
| 679 | 2295 | |
| 680 | 2296 | /** |
| 681 | 2297 | * Disable spectra's quick action bar in sureforms CPT. |
| @@ -693,56 +2309,2980 @@ | ||
| 693 | 2309 | return $status; |
| 694 | 2310 | } |
| 695 | 2311 | |
| 696 | 2312 | /** |
| 697 | - * Get SureForms Website URL. | |
| 2313 | + * Register Pro compatibility notices early for React pages. | |
| 698 | 2314 | * |
| 699 | - * @param string $trail The URL trail to append to SureForms website URL. The parameter should not include a leading slash as the base URL already ends with a trailing slash. | |
| 700 | - * @since 0.0.7 | |
| 701 | - * @return string | |
| 2315 | + * This method runs on admin_init (priority 5) to ensure notices are | |
| 2316 | + * registered BEFORE admin_enqueue_scripts, so they're available when | |
| 2317 | + * wp_localize_script runs. | |
| 2318 | + * | |
| 2319 | + * Hooked - admin_init (priority 5) | |
| 2320 | + * | |
| 2321 | + * @return void | |
| 2322 | + * @since 2.5.0 | |
| 702 | 2323 | */ |
| 703 | - public static function get_sureforms_website_url( $trail ) { | |
| 704 | - $url = SRFM_WEBSITE; | |
| 705 | - if ( ! empty( $trail ) && is_string( $trail ) ) { | |
| 706 | - $url = SRFM_WEBSITE . $trail; | |
| 2324 | + public function register_pro_compatibility_notices() { | |
| 2325 | + // Early exit if Pro is not active, user lacks permissions, or Notice_Manager is unavailable. | |
| 2326 | + if ( ! Helper::has_pro() || ! Helper::current_user_can() || ! class_exists( 'SRFM\Admin\Notice_Manager' ) ) { | |
| 2327 | + return; | |
| 707 | 2328 | } |
| 708 | 2329 | |
| 709 | - return esc_url( $url ); | |
| 2330 | + // Register version outdated notice for React pages. | |
| 2331 | + if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) { | |
| 2332 | + $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro'; | |
| 2333 | + $react_outdated_message = sprintf( | |
| 2334 | + // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version. | |
| 2335 | + esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version.', 'sureforms' ), | |
| 2336 | + esc_html( SRFM_VER ), | |
| 2337 | + esc_html( $pro_plugin_name ), | |
| 2338 | + esc_html( SRFM_PRO_RECOMMENDED_VER ) | |
| 2339 | + ); | |
| 2340 | + | |
| 2341 | + \SRFM\Admin\Notice_Manager::register_notice( | |
| 2342 | + [ | |
| 2343 | + 'id' => 'sureforms-pro-version-outdated', | |
| 2344 | + 'variant' => 'warning', | |
| 2345 | + 'message' => $react_outdated_message, | |
| 2346 | + 'actions' => [ | |
| 2347 | + [ | |
| 2348 | + 'label' => esc_html__( 'Update Now', 'sureforms' ), | |
| 2349 | + 'url' => admin_url( 'update-core.php' ), | |
| 2350 | + 'variant' => 'primary', | |
| 2351 | + ], | |
| 2352 | + ], | |
| 2353 | + 'pages' => [ 'all' ], | |
| 2354 | + ] | |
| 2355 | + ); | |
| 2356 | + } | |
| 710 | 2357 | } |
| 711 | 2358 | |
| 712 | - // Entries methods. | |
| 2359 | + /** | |
| 2360 | + * Register the React notice when the entries table is missing. | |
| 2361 | + * | |
| 2362 | + * Hooked - admin_init, priority 5. | |
| 2363 | + * | |
| 2364 | + * Priority 5 is load-bearing: Notice_Manager hands notices to the front end | |
| 2365 | + * through the `srfm_admin_filter` applied during admin_enqueue_scripts, so | |
| 2366 | + * anything registering later never reaches the page. | |
| 2367 | + * | |
| 2368 | + * @since 2.12.6 | |
| 2369 | + * @return void | |
| 2370 | + */ | |
| 2371 | + public function register_database_repair_notice() { | |
| 2372 | + // admin_init also fires on admin-ajax.php. Nothing there renders a notice, so | |
| 2373 | + // skip the work rather than reading a transient on every AJAX request. | |
| 2374 | + if ( wp_doing_ajax() ) { | |
| 2375 | + return; | |
| 2376 | + } | |
| 713 | 2377 | |
| 2378 | + if ( ! Helper::current_user_can() ) { | |
| 2379 | + return; | |
| 2380 | + } | |
| 2381 | + | |
| 2382 | + if ( ! class_exists( 'SRFM\Admin\Notice_Manager' ) ) { | |
| 2383 | + return; | |
| 2384 | + } | |
| 2385 | + | |
| 2386 | + // A just-completed repair reports its outcome instead of the warning. | |
| 2387 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display flag; the repair itself is nonce-checked in handle_database_repair(). | |
| 2388 | + $result = isset( $_GET['srfm_db_repair'] ) ? sanitize_key( wp_unslash( $_GET['srfm_db_repair'] ) ) : ''; | |
| 2389 | + | |
| 2390 | + if ( 'done' === $result ) { | |
| 2391 | + Notice_Manager::register_notice( | |
| 2392 | + [ | |
| 2393 | + 'id' => 'srfm-database-repaired', | |
| 2394 | + 'variant' => 'success', | |
| 2395 | + 'message' => __( 'Your SureForms database is up to date. New form entries will be saved as usual.', 'sureforms' ), | |
| 2396 | + 'pages' => [ 'all' ], | |
| 2397 | + ] | |
| 2398 | + ); | |
| 2399 | + return; | |
| 2400 | + } | |
| 2401 | + | |
| 2402 | + if ( 'failed' === $result ) { | |
| 2403 | + Notice_Manager::register_notice( | |
| 2404 | + [ | |
| 2405 | + 'id' => 'srfm-database-repair-failed', | |
| 2406 | + // Still a warning, not an error: a host that does not allow | |
| 2407 | + // SureForms to create tables is not the user's mistake. | |
| 2408 | + 'variant' => 'warning', | |
| 2409 | + 'message' => __( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ), | |
| 2410 | + 'actions' => [ | |
| 2411 | + [ | |
| 2412 | + 'label' => __( 'Contact support', 'sureforms' ), | |
| 2413 | + 'url' => 'https://sureforms.com/contact/', | |
| 2414 | + ], | |
| 2415 | + ], | |
| 2416 | + 'pages' => [ 'all' ], | |
| 2417 | + ] | |
| 2418 | + ); | |
| 2419 | + return; | |
| 2420 | + } | |
| 2421 | + | |
| 2422 | + if ( ! Register::is_entries_table_missing() ) { | |
| 2423 | + return; | |
| 2424 | + } | |
| 2425 | + | |
| 2426 | + $this->track_database_notice_impression(); | |
| 2427 | + | |
| 2428 | + Notice_Manager::register_notice( | |
| 2429 | + [ | |
| 2430 | + 'id' => 'srfm-database-maintenance', | |
| 2431 | + 'variant' => 'warning', | |
| 2432 | + 'title' => __( 'Database update needed', 'sureforms' ), | |
| 2433 | + // Plain text only. AdminNotice.js renders this as a React child, so | |
| 2434 | + // any markup here would show up as literal characters. | |
| 2435 | + 'message' => $this->get_database_notice_message(), | |
| 2436 | + 'actions' => [ | |
| 2437 | + [ | |
| 2438 | + 'label' => __( 'Fix now', 'sureforms' ), | |
| 2439 | + // Opaque identifier, resolved to a handler in AdminNotice.js. | |
| 2440 | + // Deliberately not a URL or endpoint: the server never tells | |
| 2441 | + // the browser which address to call. | |
| 2442 | + 'action' => 'repair-entries-table', | |
| 2443 | + 'url' => $this->get_database_repair_url(), | |
| 2444 | + ], | |
| 2445 | + ], | |
| 2446 | + 'pages' => [ 'all' ], | |
| 2447 | + ] | |
| 2448 | + ); | |
| 2449 | + } | |
| 2450 | + | |
| 714 | 2451 | /** |
| 715 | - * Handle entry actions. | |
| 2452 | + * Render the classic warning on the WordPress dashboard. | |
| 716 | 2453 | * |
| 717 | - * @since 0.0.13 | |
| 2454 | + * Hooked - admin_notices. | |
| 2455 | + * | |
| 2456 | + * Scoped to index.php on purpose. The React notice already covers the SureForms | |
| 2457 | + * screens, so leaving this one admin-wide would stack two warnings on the same | |
| 2458 | + * page and nag on every screen in wp-admin. | |
| 2459 | + * | |
| 2460 | + * Registered as [ $this, 'method' ] rather than a closure because | |
| 2461 | + * suppress_foreign_admin_notices() strips any callback it cannot attribute to a | |
| 2462 | + * SureForms class — a closure here would be silently removed. | |
| 2463 | + * | |
| 2464 | + * @since 2.12.6 | |
| 718 | 2465 | * @return void |
| 719 | 2466 | */ |
| 720 | - public function handle_entry_actions() { | |
| 721 | - if ( isset( $_GET['entry'] ) && isset( $_GET['action'] ) ) { | |
| 722 | - Entries_List_Table::process_bulk_actions(); | |
| 2467 | + public function render_database_repair_notice() { | |
| 2468 | + if ( ! Helper::current_user_can() ) { | |
| 723 | 2469 | return; |
| 724 | 2470 | } |
| 725 | - if ( ! isset( $_GET['page'] ) || 'sureforms_entries' !== $_GET['page'] ) { | |
| 2471 | + | |
| 2472 | + $screen = get_current_screen(); | |
| 2473 | + | |
| 2474 | + if ( ! $screen || 'dashboard' !== $screen->base ) { | |
| 726 | 2475 | return; |
| 727 | 2476 | } |
| 728 | - if ( ! isset( $_GET['entry_id'] ) || ! isset( $_GET['action'] ) ) { | |
| 2477 | + | |
| 2478 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only display flag; the repair itself is nonce-checked in handle_database_repair(). | |
| 2479 | + $result = isset( $_GET['srfm_db_repair'] ) ? sanitize_key( wp_unslash( $_GET['srfm_db_repair'] ) ) : ''; | |
| 2480 | + | |
| 2481 | + if ( 'done' === $result ) { | |
| 2482 | + ?> | |
| 2483 | + <div class="notice notice-success is-dismissible"> | |
| 2484 | + <p><?php esc_html_e( 'Your SureForms database is up to date. New form entries will be saved as usual.', 'sureforms' ); ?></p> | |
| 2485 | + </div> | |
| 2486 | + <?php | |
| 729 | 2487 | return; |
| 730 | 2488 | } |
| 731 | - if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ), 'srfm_entries_action' ) ) { | |
| 732 | - wp_die( esc_html__( 'Nonce verification failed.', 'sureforms' ) ); | |
| 2489 | + | |
| 2490 | + if ( 'failed' === $result ) { | |
| 2491 | + ?> | |
| 2492 | + <div class="notice notice-warning is-dismissible"> | |
| 2493 | + <p><?php esc_html_e( 'SureForms could not finish updating the database. Your hosting may not allow SureForms to create database tables — please contact your hosting provider or SureForms support.', 'sureforms' ); ?></p> | |
| 2494 | + </div> | |
| 2495 | + <?php | |
| 2496 | + return; | |
| 733 | 2497 | } |
| 734 | - $action = isset( $_GET['action'] ) ? sanitize_text_field( wp_unslash( $_GET['action'] ) ) : ''; | |
| 735 | - $entry_id = Helper::get_integer_value( sanitize_text_field( wp_unslash( $_GET['entry_id'] ) ) ); | |
| 736 | - $view = isset( $_GET['view'] ) ? sanitize_text_field( wp_unslash( $_GET['view'] ) ) : ''; | |
| 737 | - if ( $entry_id > 0 ) { | |
| 738 | - if ( 'read' === $action && 'details' === $view ) { | |
| 739 | - $entry_status = Entries::get( $entry_id )['status']; | |
| 740 | - if ( 'trash' === $entry_status ) { | |
| 741 | - wp_die( esc_html__( 'You cannot view this entry because it is in trash.', 'sureforms' ) ); | |
| 2498 | + | |
| 2499 | + if ( ! Register::is_entries_table_missing() ) { | |
| 2500 | + return; | |
| 2501 | + } | |
| 2502 | + | |
| 2503 | + $this->track_database_notice_impression(); | |
| 2504 | + ?> | |
| 2505 | + <div class="notice notice-warning"> | |
| 2506 | + <p> | |
| 2507 | + <strong><?php esc_html_e( 'SureForms — database update needed', 'sureforms' ); ?></strong> | |
| 2508 | + </p> | |
| 2509 | + <p><?php echo esc_html( $this->get_database_notice_message() ); ?></p> | |
| 2510 | + <p> | |
| 2511 | + <a href="<?php echo esc_url( $this->get_database_repair_url() ); ?>" class="button button-primary"> | |
| 2512 | + <?php esc_html_e( 'Update database', 'sureforms' ); ?> | |
| 2513 | + </a> | |
| 2514 | + </p> | |
| 2515 | + </div> | |
| 2516 | + <?php | |
| 2517 | + } | |
| 2518 | + | |
| 2519 | + /** | |
| 2520 | + * Repair the entries table, then redirect back with the outcome. | |
| 2521 | + * | |
| 2522 | + * Hooked - admin_post_srfm_repair_entries_table. | |
| 2523 | + * | |
| 2524 | + * A nonce-protected GET that changes state matches how core's own plugin | |
| 2525 | + * activate / deactivate / delete links work. | |
| 2526 | + * | |
| 2527 | + * @since 2.12.6 | |
| 2528 | + * @return void | |
| 2529 | + */ | |
| 2530 | + public function handle_database_repair() { | |
| 2531 | + if ( ! Helper::current_user_can() ) { | |
| 2532 | + wp_die( esc_html__( 'You do not have permission to update the database.', 'sureforms' ), 403 ); | |
| 2533 | + } | |
| 2534 | + | |
| 2535 | + check_admin_referer( 'srfm_repair_entries_table' ); | |
| 2536 | + | |
| 2537 | + $repaired = $this->do_database_repair(); | |
| 2538 | + $referer = wp_get_referer(); | |
| 2539 | + | |
| 2540 | + wp_safe_redirect( | |
| 2541 | + add_query_arg( | |
| 2542 | + 'srfm_db_repair', | |
| 2543 | + $repaired ? 'done' : 'failed', | |
| 2544 | + $referer ? $referer : admin_url() | |
| 2545 | + ) | |
| 2546 | + ); | |
| 2547 | + exit; | |
| 2548 | + } | |
| 2549 | + | |
| 2550 | + /** | |
| 2551 | + * Repair the entries table and record what happened. | |
| 2552 | + * | |
| 2553 | + * The single place the repair is performed and counted, shared by the | |
| 2554 | + * admin-post handler and the REST endpoint. One user action reaches exactly one | |
| 2555 | + * of those, so the click counter cannot double-count across the two surfaces. | |
| 2556 | + * | |
| 2557 | + * @since 2.12.6 | |
| 2558 | + * @return bool True when the table exists afterwards. | |
| 2559 | + */ | |
| 2560 | + public function do_database_repair() { | |
| 2561 | + // Cumulative counter, so $force = true: each new count is a new value and is | |
| 2562 | + // re-sent, while an identical repeat short-circuits inside track(). | |
| 2563 | + $attempts = Helper::get_integer_value( Helper::get_srfm_option( 'db_repair_attempts', 0 ) ) + 1; | |
| 2564 | + Helper::update_srfm_option( 'db_repair_attempts', $attempts ); | |
| 2565 | + | |
| 2566 | + // Event name is the `database_error` => `fix_now` entry in the $valid | |
| 2567 | + // allowlist in handle_notice_response(). Kept in sync by hand; that array is | |
| 2568 | + // where the team looks notice event names up. | |
| 2569 | + Analytics::events()->track( 'database_error_notice_cta', (string) $attempts, [], true ); | |
| 2570 | + | |
| 2571 | + $repaired = Register::repair_entries_table(); | |
| 2572 | + | |
| 2573 | + // The failure case is the more valuable signal: it means the host refuses to | |
| 2574 | + // let SureForms create tables, which no amount of retrying will fix. | |
| 2575 | + Analytics::events()->track( | |
| 2576 | + 'database_repair_result', | |
| 2577 | + $repaired ? 'success' : 'failed', | |
| 2578 | + [], | |
| 2579 | + true | |
| 2580 | + ); | |
| 2581 | + | |
| 2582 | + return $repaired; | |
| 2583 | + } | |
| 2584 | + | |
| 2585 | + /** | |
| 2586 | + * Admin Notice Callback if sureforms pro is out of date. | |
| 2587 | + * | |
| 2588 | + * Hooked - admin_notices | |
| 2589 | + * | |
| 2590 | + * @return void | |
| 2591 | + * @since 1.0.4 | |
| 2592 | + */ | |
| 2593 | + public function srfm_pro_version_compatibility() { | |
| 2594 | + if ( ! Helper::has_pro() ) { | |
| 2595 | + return; | |
| 2596 | + } | |
| 2597 | + | |
| 2598 | + if ( empty( get_current_screen() ) ) { | |
| 2599 | + return; | |
| 2600 | + } | |
| 2601 | + | |
| 2602 | + if ( ! Helper::current_user_can() ) { | |
| 2603 | + return; | |
| 2604 | + } | |
| 2605 | + | |
| 2606 | + $srfm_pro_license_status = get_option( 'srfm_pro_license_status', '' ); | |
| 2607 | + /** | |
| 2608 | + * If the license status is not set then get the license status and update the option accordingly. | |
| 2609 | + * This will be executed only once. Subsequently, the option status is updated by the licensing class on license activation or deactivation. | |
| 2610 | + */ | |
| 2611 | + if ( empty( $srfm_pro_license_status ) && class_exists( 'SRFM_PRO\Admin\Licensing' ) ) { | |
| 2612 | + $srfm_pro_license_status = \SRFM_PRO\Admin\Licensing::is_license_active() ? 'licensed' : 'unlicensed'; | |
| 2613 | + update_option( 'srfm_pro_license_status', $srfm_pro_license_status ); | |
| 2614 | + } | |
| 2615 | + | |
| 2616 | + $pro_plugin_name = defined( 'SRFM_PRO_PRODUCT' ) ? SRFM_PRO_PRODUCT : 'SureForms Pro'; | |
| 2617 | + $message = ''; | |
| 2618 | + $url = admin_url( 'admin.php?page=sureforms_form_settings&tab=account-settings' ); | |
| 2619 | + if ( 'unlicensed' === $srfm_pro_license_status ) { | |
| 2620 | + ob_start(); | |
| 2621 | + ?> | |
| 2622 | + <p> | |
| 2623 | + <?php | |
| 2624 | + printf( | |
| 2625 | + // translators: %1$s: Opening anchor tag with URL, %2$s: Closing anchor tag, %3$s: SureForms Pro Plugin Name. | |
| 2626 | + esc_html__( 'Please %1$sactivate%2$s your copy of %3$s to get new features, access support, receive update notifications, and more.', 'sureforms' ), | |
| 2627 | + '<a href="' . esc_url( $url ) . '">', | |
| 2628 | + '</a>', | |
| 2629 | + '<i>' . esc_html( $pro_plugin_name ) . '</i>' | |
| 2630 | + ); | |
| 2631 | + ?> | |
| 2632 | + </p> | |
| 2633 | + <?php | |
| 2634 | + $message = ob_get_clean(); | |
| 2635 | + } | |
| 2636 | + | |
| 2637 | + if ( ! version_compare( SRFM_PRO_VER, SRFM_PRO_RECOMMENDED_VER, '>=' ) ) { | |
| 2638 | + ob_start(); | |
| 2639 | + ?> | |
| 2640 | + <p> | |
| 2641 | + <?php | |
| 2642 | + printf( | |
| 2643 | + // translators: %1$s: SureForms version, %2$s: SureForms Pro Plugin Name, %3$s: SureForms Pro Version, %4$s: Anchor tag open, %5$s: Closing anchor tag. | |
| 2644 | + esc_html__( 'SureForms %1$s requires minimum %2$s %3$s to work properly. Please update to the latest version from %4$shere%5$s.', 'sureforms' ), | |
| 2645 | + esc_html( SRFM_VER ), | |
| 2646 | + esc_html( $pro_plugin_name ), | |
| 2647 | + esc_html( SRFM_PRO_RECOMMENDED_VER ), | |
| 2648 | + '<a href="' . esc_url( admin_url( 'update-core.php' ) ) . '">', | |
| 2649 | + '</a>' | |
| 2650 | + ); | |
| 2651 | + ?> | |
| 2652 | + </p> | |
| 2653 | + <?php | |
| 2654 | + $message .= ob_get_clean(); | |
| 2655 | + } | |
| 2656 | + | |
| 2657 | + if ( ! empty( $message ) ) { | |
| 2658 | + // Phpcs ignore comment is required as $message variable is already escaped. | |
| 2659 | + ?> | |
| 2660 | + <div class="notice notice-warning"><?php echo wp_kses_post( $message ); ?></div> | |
| 2661 | + <?php | |
| 2662 | + } | |
| 2663 | + } | |
| 2664 | + | |
| 2665 | + /** | |
| 2666 | + * Display a notice to the user about providing a review. | |
| 2667 | + * | |
| 2668 | + * @since 2.5.2 | |
| 2669 | + * @return void | |
| 2670 | + */ | |
| 2671 | + public function display_srfm_rating_notice() { | |
| 2672 | + // Only show to admins. | |
| 2673 | + if ( ! Helper::current_user_can() ) { | |
| 2674 | + return; | |
| 2675 | + } | |
| 2676 | + | |
| 2677 | + // Allow the notice to be disabled. | |
| 2678 | + if ( ! apply_filters( 'srfm_show_rating_notice', true ) ) { | |
| 2679 | + return; | |
| 2680 | + } | |
| 2681 | + | |
| 2682 | + $notice_id = 'srfm-plugin-review-notice'; | |
| 2683 | + | |
| 2684 | + Astra_Notices::add_notice( | |
| 2685 | + [ | |
| 2686 | + 'id' => $notice_id, | |
| 2687 | + 'type' => '', | |
| 2688 | + 'message' => self::build_srfm_notice_markup( | |
| 2689 | + __( 'Amazing! SureForms is powering your forms and submissions - let\'s keep growing together!', 'sureforms' ), | |
| 2690 | + __( 'If SureForms has been helpful, would you mind taking a moment to leave a 5-star review on WordPress.org?', 'sureforms' ), | |
| 2691 | + [ | |
| 2692 | + [ | |
| 2693 | + 'text' => __( 'Rate SureForms', 'sureforms' ), | |
| 2694 | + 'url' => esc_url( 'https://wordpress.org/support/plugin/sureforms/reviews/' ), | |
| 2695 | + 'primary' => true, | |
| 2696 | + // Leaves wp-admin, so it also dismisses on the way out. | |
| 2697 | + 'dismiss' => true, | |
| 2698 | + 'external' => true, | |
| 2699 | + ], | |
| 2700 | + [ | |
| 2701 | + 'text' => __( 'Maybe later', 'sureforms' ), | |
| 2702 | + 'url' => '#', | |
| 2703 | + 'dismiss' => true, | |
| 2704 | + 'snooze' => WEEK_IN_SECONDS, | |
| 2705 | + ], | |
| 2706 | + [ | |
| 2707 | + 'text' => __( 'I already did', 'sureforms' ), | |
| 2708 | + 'url' => '#', | |
| 2709 | + 'dismiss' => true, | |
| 2710 | + ], | |
| 2711 | + ] | |
| 2712 | + ), | |
| 2713 | + 'class' => 'srfm-notice srfm-rating-notice', | |
| 2714 | + 'repeat-notice-after' => WEEK_IN_SECONDS, | |
| 2715 | + // Yields to the Thank You prompt for the same reason the Getting Started | |
| 2716 | + // notice does: a specific form to finish beats a recurring review ask, | |
| 2717 | + // and a user with three forms who then imports a template would | |
| 2718 | + // otherwise see both at once. | |
| 2719 | + 'show_if' => $this->maybe_display_rating_notice() && null === $this->get_displayable_thankyou_prompt() && ! $this->has_action_item_warnings(), | |
| 2720 | + 'display-with-other-notices' => true, | |
| 2721 | + ] | |
| 2722 | + ); | |
| 2723 | + | |
| 2724 | + add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] ); | |
| 2725 | + add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_notice_response_script' ] ); | |
| 2726 | + } | |
| 2727 | + | |
| 2728 | + /** | |
| 2729 | + * Display a "Getting Started" admin notice for new users who haven't yet | |
| 2730 | + * reached the rating-notice milestone (3+ forms or 3+ entries). | |
| 2731 | + * | |
| 2732 | + * The Astra Notices library handles the 7-day delay via the | |
| 2733 | + * `display-notice-after` parameter. | |
| 2734 | + * | |
| 2735 | + * @since 2.5.2 | |
| 2736 | + * @return void | |
| 2737 | + */ | |
| 2738 | + public function display_srfm_getting_started_notice() { | |
| 2739 | + // Only show to admins. | |
| 2740 | + if ( ! Helper::current_user_can() ) { | |
| 2741 | + return; | |
| 2742 | + } | |
| 2743 | + | |
| 2744 | + // Allow the notice to be disabled programmatically. | |
| 2745 | + if ( ! apply_filters( 'srfm_show_getting_started_notice', true ) ) { | |
| 2746 | + return; | |
| 2747 | + } | |
| 2748 | + | |
| 2749 | + $notice_id = 'srfm-getting-started-notice'; | |
| 2750 | + | |
| 2751 | + Astra_Notices::add_notice( | |
| 2752 | + [ | |
| 2753 | + 'id' => $notice_id, | |
| 2754 | + 'type' => '', | |
| 2755 | + 'message' => self::build_srfm_notice_markup( | |
| 2756 | + __( 'SureForms is ready to power your forms — explore what\'s possible!', 'sureforms' ), | |
| 2757 | + __( 'Manage your forms, track submissions, and discover features like AI Form Builder, payment integrations, and more from the SureForms dashboard.', 'sureforms' ), | |
| 2758 | + [ | |
| 2759 | + [ | |
| 2760 | + 'text' => __( 'Go to Dashboard', 'sureforms' ), | |
| 2761 | + 'url' => esc_url( admin_url( 'admin.php?page=sureforms_menu' ) ), | |
| 2762 | + 'primary' => true, | |
| 2763 | + ], | |
| 2764 | + [ | |
| 2765 | + 'text' => __( 'Maybe later', 'sureforms' ), | |
| 2766 | + 'url' => '#', | |
| 2767 | + 'dismiss' => true, | |
| 2768 | + 'snooze' => WEEK_IN_SECONDS, | |
| 2769 | + ], | |
| 2770 | + [ | |
| 2771 | + 'text' => __( 'I already know', 'sureforms' ), | |
| 2772 | + 'url' => '#', | |
| 2773 | + 'dismiss' => true, | |
| 2774 | + ], | |
| 2775 | + ] | |
| 2776 | + ), | |
| 2777 | + 'class' => 'srfm-notice srfm-getting-started-notice', | |
| 2778 | + 'repeat-notice-after' => WEEK_IN_SECONDS, | |
| 2779 | + // Yields to both of the other SureForms notices, so only one of ours is | |
| 2780 | + // ever on screen. The rating notice supersedes it once the user has real | |
| 2781 | + // usage; the Thank You prompt supersedes it because "finish this specific | |
| 2782 | + // form" is a concrete next step and this is a generic tour invitation. | |
| 2783 | + 'show_if' => ! $this->maybe_display_rating_notice() && null === $this->get_displayable_thankyou_prompt() && ! $this->has_action_item_warnings(), | |
| 2784 | + 'display-notice-after' => WEEK_IN_SECONDS, | |
| 2785 | + 'display-with-other-notices' => true, | |
| 2786 | + ] | |
| 2787 | + ); | |
| 2788 | + | |
| 2789 | + // Same pre-markup hook the Thank You prompt uses, so both notices are painted | |
| 2790 | + // by one stylesheet instead of two that drift apart. | |
| 2791 | + add_action( 'astra_notice_before_markup_' . $notice_id, [ $this, 'print_srfm_notice_styles' ] ); | |
| 2792 | + add_action( 'astra_notice_after_markup_' . $notice_id, [ $this, 'enqueue_notice_response_script' ] ); | |
| 2793 | + } | |
| 2794 | + | |
| 2795 | + /** | |
| 2796 | + * Enqueue the notice response analytics script. | |
| 2797 | + * | |
| 2798 | + * Called via the astra_notice_after_markup_{id} hook so the script | |
| 2799 | + * only loads when a SureForms notice is actually rendered. | |
| 2800 | + * | |
| 2801 | + * @since 2.5.2 | |
| 2802 | + * @return void | |
| 2803 | + */ | |
| 2804 | + public function enqueue_notice_response_script() { | |
| 2805 | + if ( wp_script_is( 'srfm-notice-response', 'enqueued' ) ) { | |
| 2806 | + return; | |
| 2807 | + } | |
| 2808 | + | |
| 2809 | + wp_enqueue_script( | |
| 2810 | + 'srfm-notice-response', | |
| 2811 | + SRFM_URL . 'admin/assets/js/notice-response.js', | |
| 2812 | + [], | |
| 2813 | + SRFM_VER, | |
| 2814 | + true | |
| 2815 | + ); | |
| 2816 | + | |
| 2817 | + wp_localize_script( | |
| 2818 | + 'srfm-notice-response', | |
| 2819 | + 'srfmNoticeResponse', | |
| 2820 | + [ | |
| 2821 | + 'ajaxurl' => admin_url( 'admin-ajax.php' ), | |
| 2822 | + 'nonce' => wp_create_nonce( 'srfm_notice_response' ), | |
| 2823 | + // The diagnostics are fetched when the dialog opens rather than | |
| 2824 | + // shipped with every page, so the dialog needs its own nonce. | |
| 2825 | + 'detailsNonce' => wp_create_nonce( 'srfm_action_item_details' ), | |
| 2826 | + // Carousel chrome. Built in the browser rather than printed here so | |
| 2827 | + // that with JavaScript off every notice simply stays visible, which | |
| 2828 | + // is the behaviour this replaced -- controls that cannot work must | |
| 2829 | + // not be what hides a warning. | |
| 2830 | + 'carousel' => [ | |
| 2831 | + 'previous' => __( 'Previous notice', 'sureforms' ), | |
| 2832 | + 'next' => __( 'Next notice', 'sureforms' ), | |
| 2833 | + /* translators: 1: current position, 2: total notices. */ | |
| 2834 | + 'counter' => __( '%1$d of %2$d', 'sureforms' ), | |
| 2835 | + ], | |
| 2836 | + // Details modal chrome, translated here so the script carries no | |
| 2837 | + // user-facing English of its own. | |
| 2838 | + 'details' => $this->get_details_dialog_labels(), | |
| 2839 | + // Where Contact Support goes when the fetch fails and there is no | |
| 2840 | + // category-tagged URL to use. Untagged, because at that point we do | |
| 2841 | + // not know which check sent them -- but still a way out: these | |
| 2842 | + // notices are not dismissible and Contact Support is the only action | |
| 2843 | + // that retires them. | |
| 2844 | + 'supportUrl' => $this->get_support_contact_url( '' ), | |
| 2845 | + ] | |
| 2846 | + ); | |
| 2847 | + } | |
| 2848 | + | |
| 2849 | + /** | |
| 2850 | + * Serve one failure category's diagnostics, on demand. | |
| 2851 | + * | |
| 2852 | + * Hooked - wp_ajax_srfm_action_item_details. | |
| 2853 | + * | |
| 2854 | + * The report is built here rather than shipped with the page. Its content | |
| 2855 | + * comes from the client error log, and that log is filled through a public | |
| 2856 | + * REST route gated on a submit token any visitor can obtain from a form page | |
| 2857 | + * rather than on a capability -- so the text is attacker-authored, and putting | |
| 2858 | + * it in the localisation JSON and a hidden div on every admin screen exposed | |
| 2859 | + * it far beyond the one admin who opens the dialog. | |
| 2860 | + * | |
| 2861 | + * Capability first, then nonce, then the category, matching the ordering of | |
| 2862 | + * the sibling handlers in this class. | |
| 2863 | + * | |
| 2864 | + * @since 2.12.7 | |
| 2865 | + * @return void | |
| 2866 | + */ | |
| 2867 | + public function handle_action_item_details() { | |
| 2868 | + if ( ! Helper::current_user_can() ) { | |
| 2869 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 2870 | + return; | |
| 2871 | + } | |
| 2872 | + | |
| 2873 | + if ( ! check_ajax_referer( 'srfm_action_item_details', 'nonce', false ) ) { | |
| 2874 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 2875 | + return; | |
| 2876 | + } | |
| 2877 | + | |
| 2878 | + // sanitize_key() returns '' for anything non-scalar (formatting.php:2194), so | |
| 2879 | + // a category[]= in the body arrives here as the empty string and falls into | |
| 2880 | + // the refusal below rather than needing a type branch of its own. | |
| 2881 | + $category = isset( $_POST['category'] ) ? sanitize_key( wp_unslash( $_POST['category'] ) ) : ''; | |
| 2882 | + | |
| 2883 | + // The only check the category needs, and the reason there is no separate | |
| 2884 | + // allowlist above it: get_open_failures() returns nothing but keys in | |
| 2885 | + // Client_Logger::CATEGORIES, so an absent category, an unrecognised one and | |
| 2886 | + // a recognised one with nothing wrong all land here. Asking for a category | |
| 2887 | + // with no fault must not mint a report describing one. | |
| 2888 | + $open = Client_Logger::get_open_failures(); | |
| 2889 | + | |
| 2890 | + if ( ! isset( $open[ $category ] ) ) { | |
| 2891 | + wp_send_json_error( [ 'message' => __( 'Nothing to report.', 'sureforms' ) ], 404 ); | |
| 2892 | + return; | |
| 2893 | + } | |
| 2894 | + | |
| 2895 | + $form_title = Helper::get_string_value( $open[ $category ]['form_title'] ?? '' ); | |
| 2896 | + | |
| 2897 | + wp_send_json_success( | |
| 2898 | + [ | |
| 2899 | + 'details' => $this->get_support_message( $category, $form_title ) | |
| 2900 | + . "\n\n" . $this->get_support_log_block( 8000 ), | |
| 2901 | + 'support_url' => $this->get_support_contact_url( $category ), | |
| 2902 | + ] | |
| 2903 | + ); | |
| 2904 | + } | |
| 2905 | + | |
| 2906 | + /** | |
| 2907 | + * Handle the notice response AJAX request. | |
| 2908 | + * | |
| 2909 | + * Validates the request and records the analytics event | |
| 2910 | + * for the notice button that was clicked. | |
| 2911 | + * | |
| 2912 | + * @since 2.5.2 | |
| 2913 | + * @return void | |
| 2914 | + */ | |
| 2915 | + public function handle_notice_response() { | |
| 2916 | + if ( ! check_ajax_referer( 'srfm_notice_response', 'nonce', false ) ) { | |
| 2917 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 2918 | + return; | |
| 2919 | + } | |
| 2920 | + | |
| 2921 | + if ( ! Helper::current_user_can() ) { | |
| 2922 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 2923 | + return; | |
| 2924 | + } | |
| 2925 | + | |
| 2926 | + $notice_id = isset( $_POST['notice_id'] ) ? sanitize_text_field( wp_unslash( $_POST['notice_id'] ) ) : ''; | |
| 2927 | + $button = isset( $_POST['button'] ) ? sanitize_text_field( wp_unslash( $_POST['button'] ) ) : ''; | |
| 2928 | + | |
| 2929 | + $valid = [ | |
| 2930 | + 'srfm-getting-started-notice' => [ | |
| 2931 | + 'go_to_dashboard' => 'getting_started_notice_cta', | |
| 2932 | + 'maybe_later' => 'getting_started_notice_snooze', | |
| 2933 | + 'dismissed' => 'getting_started_notice_dismiss', | |
| 2934 | + ], | |
| 2935 | + 'srfm-plugin-review-notice' => [ | |
| 2936 | + 'rate_sureforms' => 'rating_notice_cta', | |
| 2937 | + 'maybe_later' => 'rating_notice_snooze', | |
| 2938 | + 'dismissed' => 'rating_notice_dismiss', | |
| 2939 | + ], | |
| 2940 | + // Database maintenance notice. Keyed `database_error` for the warehouse; | |
| 2941 | + // the user-facing copy deliberately reads as a routine update, not an | |
| 2942 | + // error. `dismissed` is registered but unreachable today — a missing | |
| 2943 | + // entries table is not something we let people dismiss. | |
| 2944 | + 'database_error' => [ | |
| 2945 | + 'fix_now' => 'database_error_notice_cta', | |
| 2946 | + 'dismissed' => 'database_error_notice_dismiss', | |
| 2947 | + ], | |
| 2948 | + // The "Finish setting up" prompt (#3030): three CTAs, plus the ✕. | |
| 2949 | + 'form_submission_error' => [ | |
| 2950 | + 'view_details' => 'submission_failure_notice_view', | |
| 2951 | + 'copy_details' => 'submission_failure_notice_copy', | |
| 2952 | + 'contact_support' => 'submission_failure_notice_cta', | |
| 2953 | + 'dismissed' => 'submission_failure_notice_dismiss', | |
| 2954 | + ], | |
| 2955 | + 'notification_error' => [ | |
| 2956 | + 'view_details' => 'notification_failure_notice_view', | |
| 2957 | + 'copy_details' => 'notification_failure_notice_copy', | |
| 2958 | + 'contact_support' => 'notification_failure_notice_cta', | |
| 2959 | + 'help_me_fix' => 'notification_failure_notice_guide', | |
| 2960 | + 'dismissed' => 'notification_failure_notice_dismiss', | |
| 2961 | + ], | |
| 2962 | + 'integration_error' => [ | |
| 2963 | + 'view_details' => 'integration_failure_notice_view', | |
| 2964 | + 'copy_details' => 'integration_failure_notice_copy', | |
| 2965 | + 'contact_support' => 'integration_failure_notice_cta', | |
| 2966 | + 'dismissed' => 'integration_failure_notice_dismiss', | |
| 2967 | + ], | |
| 2968 | + 'caching_plugin' => [ | |
| 2969 | + 'help_me_fix' => 'caching_plugin_notice_cta', | |
| 2970 | + 'dismissed' => 'caching_plugin_notice_dismiss', | |
| 2971 | + ], | |
| 2972 | + 'srfm-thankyou-prompt' => [ | |
| 2973 | + 'edit_form' => 'thankyou_notice_edit_form', | |
| 2974 | + 'set_replies' => 'thankyou_notice_set_replies', | |
| 2975 | + 'edit_thankyou' => 'thankyou_notice_edit_thankyou', | |
| 2976 | + 'dismissed' => 'thankyou_notice_dismiss', | |
| 2977 | + ], | |
| 2978 | + ]; | |
| 2979 | + | |
| 2980 | + if ( ! isset( $valid[ $notice_id ][ $button ] ) ) { | |
| 2981 | + wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 ); | |
| 2982 | + // wp_send_json_error() ends the request in production. The explicit return | |
| 2983 | + // keeps the guard a guard rather than something that only works because of | |
| 2984 | + // a side effect in a function elsewhere. | |
| 2985 | + return; | |
| 2986 | + } | |
| 2987 | + | |
| 2988 | + $this->track_notice_event( $valid[ $notice_id ][ $button ] ); | |
| 2989 | + | |
| 2990 | + // Reporting the failures retires the notice until something new fails. | |
| 2991 | + // Handled here rather than in the browser so it holds for the classic | |
| 2992 | + // wp-admin notice too, which is a plain link with no JavaScript. | |
| 2993 | + $categories = [ | |
| 2994 | + 'form_submission_error' => 'submission', | |
| 2995 | + 'notification_error' => 'notification', | |
| 2996 | + 'integration_error' => 'integration', | |
| 2997 | + ]; | |
| 2998 | + | |
| 2999 | + if ( 'contact_support' === $button && isset( $categories[ $notice_id ] ) ) { | |
| 3000 | + Client_Logger::acknowledge_category( $categories[ $notice_id ] ); | |
| 3001 | + | |
| 3002 | + if ( 'form_submission_error' === $notice_id ) { | |
| 3003 | + Client_Logger::acknowledge_failures(); | |
| 3004 | + } | |
| 3005 | + } | |
| 3006 | + | |
| 3007 | + wp_send_json_success(); | |
| 3008 | + } | |
| 3009 | + | |
| 3010 | + /** | |
| 3011 | + * Disables the capabilities for WPForms to avoid conflicts when enqueueing | |
| 3012 | + * scripts and styles for WPForms. | |
| 3013 | + * | |
| 3014 | + * This function is intended to prevent any potential conflicts that may arise | |
| 3015 | + * when WPForms scripts and styles are enqueued. By disabling certain capabilities, | |
| 3016 | + * it ensures that WPForms does not interfere with other functionalities. | |
| 3017 | + * | |
| 3018 | + * @param bool $user_can A boolean indicating whether the user has the capability. | |
| 3019 | + * @return bool Returns true if the capabilities are successfully disabled, false otherwise. | |
| 3020 | + * @since 1.4.2 | |
| 3021 | + */ | |
| 3022 | + public function disable_wpforms_capabilities( $user_can ) { | |
| 3023 | + // Note: Nonce verification is intentionally omitted here as no database operations are performed. | |
| 3024 | + // The values of the $_REQUEST variables are strictly validated, ensuring security without the need for nonce verification. | |
| 3025 | + | |
| 3026 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 3027 | + $post_id = ! empty( $_REQUEST['post'] ) && ! empty( $_REQUEST['action'] ) ? absint( $_REQUEST['post'] ) : 0; | |
| 3028 | + | |
| 3029 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended | |
| 3030 | + $post_type = $post_id ? get_post_type( $post_id ) : sanitize_text_field( wp_unslash( $_REQUEST['post_type'] ?? '' ) ); | |
| 3031 | + return SRFM_FORMS_POST_TYPE === $post_type ? false : $user_can; | |
| 3032 | + } | |
| 3033 | + | |
| 3034 | + /** | |
| 3035 | + * Enqueueus the admin pointer script and styles. | |
| 3036 | + * | |
| 3037 | + * @return void | |
| 3038 | + * @since 1.8.0 | |
| 3039 | + */ | |
| 3040 | + public function enqueue_admin_pointer() { | |
| 3041 | + if ( ! $this->is_admin_pointer_visible() ) { | |
| 3042 | + return; | |
| 3043 | + } | |
| 3044 | + wp_enqueue_style( 'wp-pointer' ); | |
| 3045 | + wp_enqueue_script( 'wp-pointer' ); | |
| 3046 | + wp_enqueue_script( | |
| 3047 | + 'sureforms-admin-pointer', | |
| 3048 | + plugins_url( 'admin/assets/js/sureforms-pointer.js', SRFM_FILE ), | |
| 3049 | + [ 'wp-pointer', 'jquery' ], | |
| 3050 | + SRFM_VER, | |
| 3051 | + true | |
| 3052 | + ); | |
| 3053 | + wp_localize_script( | |
| 3054 | + 'sureforms-admin-pointer', | |
| 3055 | + 'sureformsPointerData', | |
| 3056 | + [ | |
| 3057 | + 'ajaxurl' => admin_url( 'admin-ajax.php' ), | |
| 3058 | + 'pointer_nonce' => wp_create_nonce( 'sureforms_pointer_action' ), | |
| 3059 | + ] | |
| 3060 | + ); | |
| 3061 | + } | |
| 3062 | + | |
| 3063 | + /** | |
| 3064 | + * Ajax handler for pointer popup visibility. | |
| 3065 | + * | |
| 3066 | + * @return void | |
| 3067 | + * @since 1.8.0 | |
| 3068 | + */ | |
| 3069 | + public function pointer_should_show() { | |
| 3070 | + // Security: Check user capability. | |
| 3071 | + if ( ! Helper::current_user_can() ) { | |
| 3072 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 3073 | + } | |
| 3074 | + // Security: Nonce check. | |
| 3075 | + if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) { | |
| 3076 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 3077 | + } | |
| 3078 | + | |
| 3079 | + $content_markup = sprintf( | |
| 3080 | + /* translators: 1: opening span, 2: opening strong (inline), 3: closing strong, 4: closing span, 5: opening strong (block), 6: closing strong */ | |
| 3081 | + __( '%1$sGet started by %2$sbuilding your first form%3$s.%4$s%5$sExperience the power of our intuitive AI Form Builder%6$s', 'sureforms' ), | |
| 3082 | + '<span>', | |
| 3083 | + '<strong>', | |
| 3084 | + '</strong>', | |
| 3085 | + '</span><br/>', | |
| 3086 | + '<strong style="font-size:1.1em;">', | |
| 3087 | + '</strong>' | |
| 3088 | + ); | |
| 3089 | + wp_send_json( | |
| 3090 | + [ | |
| 3091 | + 'show' => true, | |
| 3092 | + 'title' => esc_html( __( 'SureForms is waiting for you!', 'sureforms' ) ), | |
| 3093 | + 'content' => wp_kses_post( $content_markup ), | |
| 3094 | + 'button_text' => esc_html( __( 'Build My First Form', 'sureforms' ) ), | |
| 3095 | + 'dismiss' => esc_html( __( 'Dismiss', 'sureforms' ) ), | |
| 3096 | + 'button_url' => admin_url( 'admin.php?page=add-new-form' ), | |
| 3097 | + ] | |
| 3098 | + ); | |
| 3099 | + } | |
| 3100 | + | |
| 3101 | + /** | |
| 3102 | + * Ajax callback for pointer popup dismissed action. | |
| 3103 | + * | |
| 3104 | + * @return void | |
| 3105 | + * @since 1.8.0 | |
| 3106 | + */ | |
| 3107 | + public function pointer_dismissed() { | |
| 3108 | + // Security: Check user capability. | |
| 3109 | + if ( ! Helper::current_user_can() ) { | |
| 3110 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 3111 | + } | |
| 3112 | + // Security: Nonce check. | |
| 3113 | + if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) { | |
| 3114 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 3115 | + } | |
| 3116 | + // Use Helper to update srfm_options key. | |
| 3117 | + Helper::update_srfm_option( 'pointer_popup_dismissed', time() ); | |
| 3118 | + | |
| 3119 | + wp_send_json_success(); | |
| 3120 | + } | |
| 3121 | + | |
| 3122 | + /** | |
| 3123 | + * Ajax pointer accepted CTA callback. | |
| 3124 | + * | |
| 3125 | + * @return void | |
| 3126 | + * @since 1.8.0 | |
| 3127 | + */ | |
| 3128 | + public function pointer_accepted_cta() { | |
| 3129 | + // Security: Check user capability. | |
| 3130 | + if ( ! Helper::current_user_can() ) { | |
| 3131 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 3132 | + } | |
| 3133 | + // Security: Nonce check. | |
| 3134 | + if ( empty( $_POST['pointer_nonce'] ) || ! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['pointer_nonce'] ) ), 'sureforms_pointer_action' ) ) { | |
| 3135 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 3136 | + } | |
| 3137 | + // Use Helper to update srfm_options key. | |
| 3138 | + Helper::update_srfm_option( 'pointer_popup_accepted', time() ); | |
| 3139 | + | |
| 3140 | + wp_send_json_success(); | |
| 3141 | + } | |
| 3142 | + | |
| 3143 | + /** | |
| 3144 | + * Maybe register the dashboard widget based on entries. | |
| 3145 | + * | |
| 3146 | + * @return void | |
| 3147 | + * @since 1.9.1 | |
| 3148 | + */ | |
| 3149 | + public function maybe_register_dashboard_widget() { | |
| 3150 | + | |
| 3151 | + // Only for users with manage_options capability. | |
| 3152 | + if ( ! Helper::current_user_can() ) { | |
| 3153 | + return; | |
| 3154 | + } | |
| 3155 | + | |
| 3156 | + // Register the AI quick draft widget for capable users (the capability gate above applies); unlike the recent-entries widget below, it is not conditional on having entries. | |
| 3157 | + add_action( 'wp_dashboard_setup', [ $this, 'register_ai_dashboard_widget' ] ); | |
| 3158 | + | |
| 3159 | + // Quick check if there are any entries in the last 7 days. | |
| 3160 | + $seven_days_ago = strtotime( '-7 days' ); | |
| 3161 | + $total_entries = Entries::get_entries_count_after( $seven_days_ago ); | |
| 3162 | + | |
| 3163 | + // Only add the dashboard setup hook if there are entries. | |
| 3164 | + if ( $total_entries > 0 ) { | |
| 3165 | + // Get forms with entries (limit 4 for dashboard widget). | |
| 3166 | + $this->dashboard_widget_data = Helper::get_forms_with_entry_counts( $seven_days_ago, 4 ); | |
| 3167 | + | |
| 3168 | + // Only show dashboard widget if there are forms with entries. | |
| 3169 | + if ( ! empty( $this->dashboard_widget_data ) ) { | |
| 3170 | + add_action( 'wp_dashboard_setup', [ $this, 'register_dashboard_widget' ] ); | |
| 3171 | + } | |
| 3172 | + } | |
| 3173 | + } | |
| 3174 | + | |
| 3175 | + /** | |
| 3176 | + * Register the dashboard widget. | |
| 3177 | + * | |
| 3178 | + * @return void | |
| 3179 | + * @since 1.9.1 | |
| 3180 | + */ | |
| 3181 | + public function register_dashboard_widget() { | |
| 3182 | + // Add the widget with high priority to position it at the top. | |
| 3183 | + wp_add_dashboard_widget( | |
| 3184 | + 'sureforms_recent_entries', | |
| 3185 | + __( 'SureForms', 'sureforms' ), | |
| 3186 | + [ $this, 'render_dashboard_widget' ], | |
| 3187 | + null, | |
| 3188 | + null, | |
| 3189 | + 'normal', | |
| 3190 | + 'high' | |
| 3191 | + ); | |
| 3192 | + } | |
| 3193 | + | |
| 3194 | + /** | |
| 3195 | + * Register the AI quick draft dashboard widget. | |
| 3196 | + * | |
| 3197 | + * @return void | |
| 3198 | + * @since 2.12.1 | |
| 3199 | + */ | |
| 3200 | + public function register_ai_dashboard_widget() { | |
| 3201 | + wp_add_dashboard_widget( | |
| 3202 | + 'sureforms_ai_quick_draft', | |
| 3203 | + __( 'SureForms AI Quick Draft', 'sureforms' ), | |
| 3204 | + [ $this, 'render_ai_dashboard_widget' ], | |
| 3205 | + null, | |
| 3206 | + null, | |
| 3207 | + 'normal', | |
| 3208 | + 'high' | |
| 3209 | + ); | |
| 3210 | + } | |
| 3211 | + | |
| 3212 | + /** | |
| 3213 | + * Render AI quick draft dashboard widget content. | |
| 3214 | + * | |
| 3215 | + * @return void | |
| 3216 | + * @since 2.12.1 | |
| 3217 | + */ | |
| 3218 | + public function render_ai_dashboard_widget() { | |
| 3219 | + ?> | |
| 3220 | + <div class="srfm-ai-dashboard-widget"> | |
| 3221 | + <p> | |
| 3222 | + <?php esc_html_e( 'Describe the form and let SureForms AI generate it for you.', 'sureforms' ); ?> | |
| 3223 | + </p> | |
| 3224 | + <label for="srfm-ai-dashboard-prompt" class="screen-reader-text"> | |
| 3225 | + <?php esc_html_e( 'Describe your form', 'sureforms' ); ?> | |
| 3226 | + </label> | |
| 3227 | + <textarea | |
| 3228 | + id="srfm-ai-dashboard-prompt" | |
| 3229 | + class="widefat" | |
| 3230 | + rows="5" | |
| 3231 | + maxlength="2000" | |
| 3232 | + placeholder="<?php esc_attr_e( 'Example: Create a contact form with name, email, phone, and message fields.', 'sureforms' ); ?>" | |
| 3233 | + ></textarea> | |
| 3234 | + <p style="margin-top:10px;margin-bottom:0;display:flex;align-items:center;gap:10px;"> | |
| 3235 | + <button type="button" class="button button-primary" id="srfm-ai-dashboard-generate" disabled> | |
| 3236 | + <?php esc_html_e( 'Create New Form', 'sureforms' ); ?> | |
| 3237 | + </button> | |
| 3238 | + <span id="srfm-ai-dashboard-char-count" style="color:#646970;">0/2000</span> | |
| 3239 | + </p> | |
| 3240 | + </div> | |
| 3241 | + <?php | |
| 3242 | + } | |
| 3243 | + | |
| 3244 | + /** | |
| 3245 | + * Enqueue the AI quick draft dashboard widget script on the dashboard screen. | |
| 3246 | + * | |
| 3247 | + * The widget's behavior lives here (attached via wp_add_inline_script) rather than as an | |
| 3248 | + * inline <script> in the render callback, so it passes Plugin Check and keeps server values | |
| 3249 | + * out of the markup. Server values are passed through wp_localize_script. | |
| 3250 | + * | |
| 3251 | + * @param string $hook_suffix The current admin page hook suffix. | |
| 3252 | + * @return void | |
| 3253 | + * @since 2.12.1 | |
| 3254 | + */ | |
| 3255 | + public function enqueue_ai_dashboard_widget_assets( $hook_suffix ) { | |
| 3256 | + // Only on the main dashboard, and only for capable users (matches the widget gate). | |
| 3257 | + if ( 'index.php' !== $hook_suffix || ! Helper::current_user_can() ) { | |
| 3258 | + return; | |
| 3259 | + } | |
| 3260 | + | |
| 3261 | + // Register an inline-only handle (empty src) — the WordPress-core pattern for attaching | |
| 3262 | + // localized data plus an inline script without shipping a separate asset file. | |
| 3263 | + wp_register_script( 'srfm-ai-dashboard-widget', '', [], SRFM_VER, true ); | |
| 3264 | + wp_enqueue_script( 'srfm-ai-dashboard-widget' ); | |
| 3265 | + | |
| 3266 | + wp_localize_script( | |
| 3267 | + 'srfm-ai-dashboard-widget', | |
| 3268 | + 'srfmAiDashboardWidget', | |
| 3269 | + [ | |
| 3270 | + 'redirectUrl' => admin_url( 'admin.php?page=add-new-form' ), | |
| 3271 | + 'ajaxUrl' => admin_url( 'admin-ajax.php' ), | |
| 3272 | + 'nonce' => wp_create_nonce( 'srfm_ai_widget_usage' ), | |
| 3273 | + 'redirectingTxt' => __( 'Redirecting...', 'sureforms' ), | |
| 3274 | + ] | |
| 3275 | + ); | |
| 3276 | + | |
| 3277 | + $inline_script = <<<'JS' | |
| 3278 | +( function () { | |
| 3279 | + const config = window.srfmAiDashboardWidget || {}; | |
| 3280 | + const generateButton = document.getElementById( 'srfm-ai-dashboard-generate' ); | |
| 3281 | + const promptField = document.getElementById( 'srfm-ai-dashboard-prompt' ); | |
| 3282 | + const charCount = document.getElementById( 'srfm-ai-dashboard-char-count' ); | |
| 3283 | + if ( ! generateButton || ! promptField ) { | |
| 3284 | + return; | |
| 3285 | + } | |
| 3286 | + | |
| 3287 | + const updateWidgetState = function () { | |
| 3288 | + const promptValue = promptField.value.trim(); | |
| 3289 | + generateButton.disabled = ! promptValue; | |
| 3290 | + if ( charCount ) { | |
| 3291 | + charCount.textContent = `${ promptField.value.length }/2000`; | |
| 3292 | + } | |
| 3293 | + }; | |
| 3294 | + | |
| 3295 | + const triggerGeneration = function () { | |
| 3296 | + const prompt = promptField.value.trim(); | |
| 3297 | + if ( ! prompt ) { | |
| 3298 | + promptField.focus(); | |
| 3299 | + return; | |
| 3300 | + } | |
| 3301 | + | |
| 3302 | + generateButton.disabled = true; | |
| 3303 | + generateButton.textContent = config.redirectingTxt; | |
| 3304 | + | |
| 3305 | + const redirectUrl = new URL( config.redirectUrl, window.location.origin ); | |
| 3306 | + redirectUrl.searchParams.set( 'srfm_ai_dashboard_prompt', prompt ); | |
| 3307 | + | |
| 3308 | + const requestBody = new URLSearchParams(); | |
| 3309 | + requestBody.append( 'action', 'srfm_ai_widget_usage' ); | |
| 3310 | + requestBody.append( 'nonce', config.nonce ); | |
| 3311 | + | |
| 3312 | + fetch( config.ajaxUrl, { | |
| 3313 | + method: 'POST', | |
| 3314 | + credentials: 'same-origin', | |
| 3315 | + headers: { | |
| 3316 | + 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', | |
| 3317 | + }, | |
| 3318 | + body: requestBody.toString(), | |
| 3319 | + } ).finally( function () { | |
| 3320 | + window.location.href = redirectUrl.toString(); | |
| 3321 | + } ); | |
| 3322 | + }; | |
| 3323 | + | |
| 3324 | + promptField.addEventListener( 'input', updateWidgetState ); | |
| 3325 | + generateButton.addEventListener( 'click', triggerGeneration ); | |
| 3326 | + promptField.addEventListener( 'keydown', function ( event ) { | |
| 3327 | + if ( event.key === 'Enter' && ( event.metaKey || event.ctrlKey ) ) { | |
| 3328 | + event.preventDefault(); | |
| 3329 | + triggerGeneration(); | |
| 3330 | + } | |
| 3331 | + } ); | |
| 3332 | + | |
| 3333 | + updateWidgetState(); | |
| 3334 | +}() ); | |
| 3335 | +JS; | |
| 3336 | + | |
| 3337 | + wp_add_inline_script( 'srfm-ai-dashboard-widget', $inline_script ); | |
| 3338 | + } | |
| 3339 | + | |
| 3340 | + /** | |
| 3341 | + * Count an editor visit that came from the front-end "Edit Form" pill. | |
| 3342 | + * | |
| 3343 | + * The pill is a plain link, so the click is attributed by the marker query arg | |
| 3344 | + * it carries rather than by a front-end click handler. That keeps the front end | |
| 3345 | + * script-free and adds no AJAX endpoint: the only thing on the page is still an | |
| 3346 | + * anchor. It also measures the outcome that matters — the editor actually | |
| 3347 | + * opening — instead of a click that may never land. | |
| 3348 | + * | |
| 3349 | + * Every decision here comes from server state. The query arg selects the code | |
| 3350 | + * path; what gets counted is derived from the resolved post and the current | |
| 3351 | + * user's capability on it. An absent, empty, misspelled or reused arg, a post | |
| 3352 | + * that is not a SureForms form, and a user without `edit_post` on that form all | |
| 3353 | + * fall through to no-op without an explicit branch. | |
| 3354 | + * | |
| 3355 | + * No nonce, deliberately: the pill is rendered into front-end HTML that may be | |
| 3356 | + * page-cached, so a nonce would either be baked into the cache or be stale on | |
| 3357 | + * arrival. The effect is a private usage counter for a user who can already edit | |
| 3358 | + * the form, and nothing attacker-controlled reaches the analytics payload — the | |
| 3359 | + * value sent is an integer read back from stored state. | |
| 3360 | + * | |
| 3361 | + * Because the marker is just a query arg, the invariant that bounds this is the | |
| 3362 | + * dedup transient below, not the arg: a given editor moves the counter at most | |
| 3363 | + * once per form per hour, no matter how many times the URL is requested. That is | |
| 3364 | + * also what keeps the metric honest — without it a refresh or a back-navigation | |
| 3365 | + * would count again, and each count is a read-modify-write of the whole | |
| 3366 | + * `srfm_options` row, which holds unrelated settings. | |
| 3367 | + * | |
| 3368 | + * @return void | |
| 3369 | + * @since 2.12.6 | |
| 3370 | + */ | |
| 3371 | + public function maybe_track_edit_form_button_click() { | |
| 3372 | + // is_string() before sanitize_key(): `?srfm_edit_src[]=x` satisfies isset(), | |
| 3373 | + // and wp_unslash() hands the array straight through. sanitize_key() only grew | |
| 3374 | + // its is_scalar() guard after this plugin's minimum WordPress, so on the older | |
| 3375 | + // supported versions that reaches strtolower( array ) — a TypeError on PHP 8, | |
| 3376 | + // i.e. the one input shape that ended in a fatal rather than in the no-op the | |
| 3377 | + // rest of this method guarantees. | |
| 3378 | + $arg = Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG; | |
| 3379 | + | |
| 3380 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only attribution marker; see docblock for why a nonce is neither possible nor needed. | |
| 3381 | + $source = isset( $_GET[ $arg ] ) && is_string( $_GET[ $arg ] ) ? sanitize_key( wp_unslash( $_GET[ $arg ] ) ) : ''; | |
| 3382 | + | |
| 3383 | + if ( 'embed' !== $source ) { | |
| 3384 | + return; | |
| 3385 | + } | |
| 3386 | + | |
| 3387 | + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Same read-only path as above. | |
| 3388 | + $post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0; | |
| 3389 | + | |
| 3390 | + // Resolve the post type from the stored post, never from the request. | |
| 3391 | + // | |
| 3392 | + // The capability below reads as per-post but is not: sureforms_form is | |
| 3393 | + // registered with an explicit capabilities map and no `map_meta_cap` | |
| 3394 | + // (inc/post-types.php), so core short-circuits `edit_post` to the post type's | |
| 3395 | + // `edit_post` capability — `manage_options` — without ever consulting $post_id. | |
| 3396 | + // The real gate is therefore "site administrator", which is stricter than a | |
| 3397 | + // per-form check, not weaker. Written down because a later `map_meta_cap` on | |
| 3398 | + // the CPT would silently change what this line means with no diff here. | |
| 3399 | + if ( 0 === $post_id || SRFM_FORMS_POST_TYPE !== get_post_type( $post_id ) ) { | |
| 3400 | + return; | |
| 3401 | + } | |
| 3402 | + | |
| 3403 | + if ( ! current_user_can( 'edit_post', $post_id ) ) { | |
| 3404 | + return; | |
| 3405 | + } | |
| 3406 | + | |
| 3407 | + // One count per editor per form per hour. Without this the metric measures | |
| 3408 | + // "editor loads carrying the marker" rather than pill clicks — a refresh or a | |
| 3409 | + // back-navigation re-counts — and a forged page could drive the counter, and | |
| 3410 | + // the writes behind it, without bound. | |
| 3411 | + $dedup_key = 'srfm_pill_click_' . get_current_user_id() . '_' . $post_id; | |
| 3412 | + | |
| 3413 | + if ( false !== get_transient( $dedup_key ) ) { | |
| 3414 | + return; | |
| 3415 | + } | |
| 3416 | + | |
| 3417 | + set_transient( $dedup_key, 1, HOUR_IN_SECONDS ); | |
| 3418 | + | |
| 3419 | + $count = Helper::get_integer_value( Helper::get_srfm_option( 'edit_form_button_clicks', 0 ) ) + 1; | |
| 3420 | + Helper::update_srfm_option( 'edit_form_button_clicks', $count ); | |
| 3421 | + | |
| 3422 | + // $force = true because this is a cumulative counter, not a one-time event — | |
| 3423 | + // it must re-send the latest count each cycle (bypasses one-time dedup). | |
| 3424 | + Analytics::events()->track( 'edit_form_button_clicked', (string) $count, [], true ); | |
| 3425 | + } | |
| 3426 | + | |
| 3427 | + /** | |
| 3428 | + * Let core strip the edit-attribution marker from the admin URL. | |
| 3429 | + * | |
| 3430 | + * Core's wp_admin_canonical_url() rewrites the address bar via replaceState() on | |
| 3431 | + * admin_head, which runs after load-post.php — so the marker has already been | |
| 3432 | + * counted by the time it is removed and no attribution is lost. Without this it | |
| 3433 | + * lingers in the address bar, in bookmarks, and in the Referer header sent to | |
| 3434 | + * every subresource the editor loads. | |
| 3435 | + * | |
| 3436 | + * @param array<string> $args Query args core already removes. | |
| 3437 | + * @since 2.12.6 | |
| 3438 | + * @return array<string> Args with the marker appended. | |
| 3439 | + */ | |
| 3440 | + public function add_removable_query_args( $args ) { | |
| 3441 | + if ( ! is_array( $args ) ) { | |
| 3442 | + return [ Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG ]; | |
| 3443 | + } | |
| 3444 | + | |
| 3445 | + $args[] = Generate_Form_Markup::EDIT_FORM_BUTTON_SOURCE_ARG; | |
| 3446 | + | |
| 3447 | + return $args; | |
| 3448 | + } | |
| 3449 | + | |
| 3450 | + /** | |
| 3451 | + * Track AI dashboard widget usage. | |
| 3452 | + * | |
| 3453 | + * @return void | |
| 3454 | + * @since 2.12.1 | |
| 3455 | + */ | |
| 3456 | + public function track_ai_widget_usage() { | |
| 3457 | + if ( ! check_ajax_referer( 'srfm_ai_widget_usage', 'nonce', false ) ) { | |
| 3458 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 3459 | + } | |
| 3460 | + | |
| 3461 | + if ( ! Helper::current_user_can() ) { | |
| 3462 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 3463 | + } | |
| 3464 | + | |
| 3465 | + $current_count = (int) Helper::get_srfm_option( 'ai_dashboard_widget_uses', 0 ) + 1; | |
| 3466 | + Helper::update_srfm_option( 'ai_dashboard_widget_uses', $current_count ); | |
| 3467 | + | |
| 3468 | + // Emit an analytics event so usage lands in the warehouse via events_record. | |
| 3469 | + // $force = true because this is a cumulative counter, not a one-time event — | |
| 3470 | + // it must re-send the latest count each cycle (bypasses one-time dedup). | |
| 3471 | + Analytics::events()->track( 'ai_dashboard_widget_used', (string) $current_count, [], true ); | |
| 3472 | + | |
| 3473 | + wp_send_json_success(); | |
| 3474 | + } | |
| 3475 | + | |
| 3476 | + /** | |
| 3477 | + * Render the dashboard widget content. | |
| 3478 | + * | |
| 3479 | + * @return void | |
| 3480 | + * @since 1.9.1 | |
| 3481 | + */ | |
| 3482 | + public function render_dashboard_widget() { | |
| 3483 | + // Use the pre-fetched data to avoid duplicate queries. | |
| 3484 | + $entries_data = $this->dashboard_widget_data; | |
| 3485 | + | |
| 3486 | + // Display the widget content. | |
| 3487 | + ?> | |
| 3488 | + <div class="srfm-dashboard-widget"> | |
| 3489 | + <div class="srfm-widget-header"> | |
| 3490 | + <h3 class="srfm-widget-title"> | |
| 3491 | + <?php esc_html_e( 'Recent Entries', 'sureforms' ); ?> | |
| 3492 | + <span class="srfm-widget-subtitle"><?php esc_html_e( '( Last 7 days )', 'sureforms' ); ?></span> | |
| 3493 | + </h3> | |
| 3494 | + <a href="<?php echo esc_url( admin_url( 'admin.php?page=sureforms_entries' ) ); ?>" class="srfm-widget-view-link"> | |
| 3495 | + <?php esc_html_e( 'View', 'sureforms' ); ?> | |
| 3496 | + </a> | |
| 3497 | + </div> | |
| 3498 | + | |
| 3499 | + <div class="srfm-table-wrapper"> | |
| 3500 | + <table class="srfm-entries-table"> | |
| 3501 | + <thead> | |
| 3502 | + <tr> | |
| 3503 | + <th><?php esc_html_e( 'Form Name', 'sureforms' ); ?></th> | |
| 3504 | + <th><?php esc_html_e( 'Entries', 'sureforms' ); ?></th> | |
| 3505 | + </tr> | |
| 3506 | + </thead> | |
| 3507 | + <tbody> | |
| 3508 | + <?php foreach ( $entries_data as $form_data ) { ?> | |
| 3509 | + <tr> | |
| 3510 | + <td class="form-name"><?php echo esc_html( $form_data['title'] ); ?></td> | |
| 3511 | + <td class="entry-count"><?php echo esc_html( $form_data['count'] ); ?></td> | |
| 3512 | + </tr> | |
| 3513 | + <?php } ?> | |
| 3514 | + </tbody> | |
| 3515 | + </table> | |
| 3516 | + </div> | |
| 3517 | + | |
| 3518 | + <?php | |
| 3519 | + // Render footer if applicable. | |
| 3520 | + $this->render_dashboard_widget_footer( $entries_data ); | |
| 3521 | + ?> | |
| 3522 | + </div> | |
| 3523 | + <?php | |
| 3524 | + } | |
| 3525 | + | |
| 3526 | + /** | |
| 3527 | + * Classic dashboard notice when submissions keep failing. | |
| 3528 | + * | |
| 3529 | + * Hooked - admin_notices. | |
| 3530 | + * | |
| 3531 | + * Gated to the WP dashboard. The React notice already covers SureForms' own | |
| 3532 | + * screens, so leaving this admin-wide would stack two warnings on one page. | |
| 3533 | + * | |
| 3534 | + * Registered as [ $this, 'method' ] rather than a closure because | |
| 3535 | + * suppress_foreign_admin_notices() strips any callback it cannot attribute to | |
| 3536 | + * a SureForms class -- a closure here would be silently removed. | |
| 3537 | + * | |
| 3538 | + * @since 2.12.6 | |
| 3539 | + * @return void | |
| 3540 | + */ | |
| 3541 | + public function render_action_item_notices() { | |
| 3542 | + // Shown across wp-admin, because someone whose forms are silently failing | |
| 3543 | + // may not open the WP dashboard or SureForms for days. | |
| 3544 | + // | |
| 3545 | + // The one exclusion is SureForms' own dashboard: the Form Checks panel in | |
| 3546 | + // its sidebar already lists these, and a banner above it would say the same | |
| 3547 | + // thing twice on one screen. | |
| 3548 | + if ( Helper::validate_request_context( 'sureforms_menu', 'page' ) ) { | |
| 3549 | + return; | |
| 3550 | + } | |
| 3551 | + | |
| 3552 | + $items = $this->get_action_items(); | |
| 3553 | + | |
| 3554 | + // Only the faults reach this surface, so count those before deciding | |
| 3555 | + // whether the carousel stylesheet is worth printing. | |
| 3556 | + $rendered = 0; | |
| 3557 | + | |
| 3558 | + foreach ( $items as $item ) { | |
| 3559 | + $status = Helper::get_string_value( $item['status'] ?? '' ); | |
| 3560 | + | |
| 3561 | + if ( 'success' !== $status && '' !== $status ) { | |
| 3562 | + $rendered++; | |
| 3563 | + } | |
| 3564 | + } | |
| 3565 | + | |
| 3566 | + if ( 0 === $rendered ) { | |
| 3567 | + return; | |
| 3568 | + } | |
| 3569 | + | |
| 3570 | + $this->enqueue_notice_response_script(); | |
| 3571 | + | |
| 3572 | + foreach ( $items as $item ) { | |
| 3573 | + $status = Helper::get_string_value( $item['status'] ?? '' ); | |
| 3574 | + | |
| 3575 | + // Passing checks belong in the SureForms panel, not in wp-admin. A | |
| 3576 | + // notice that says nothing is wrong is noise on every page load. | |
| 3577 | + if ( 'success' === $status || '' === $status ) { | |
| 3578 | + continue; | |
| 3579 | + } | |
| 3580 | + | |
| 3581 | + // A fault reads as an error; advice reads as a warning. Both are shown, | |
| 3582 | + // but they are not the same kind of message and should not look alike. | |
| 3583 | + $class = 'error' === $status ? 'notice-error' : 'notice-warning'; | |
| 3584 | + ?> | |
| 3585 | + <div class="notice srfm-action-item-notice <?php echo esc_attr( $class ); ?>"> | |
| 3586 | + <?php | |
| 3587 | + /* | |
| 3588 | + * Guarded like every sibling key. A filter item carrying only | |
| 3589 | + * id/status/cta_* is a shape this surface designs for, and reading | |
| 3590 | + * these unguarded is two PHP 8 undefined-key warnings plus an | |
| 3591 | + * esc_html( null ) deprecation on 8.1+. React tolerates the absence, | |
| 3592 | + * so leaving it would keep the two renderers disagreeing. | |
| 3593 | + */ | |
| 3594 | + ?> | |
| 3595 | + <p><strong><?php echo esc_html( Helper::get_string_value( $item['title'] ?? '' ) ); ?></strong></p> | |
| 3596 | + <p><?php echo esc_html( Helper::get_string_value( $item['message'] ?? '' ) ); ?></p> | |
| 3597 | + <?php | |
| 3598 | + // Self-serve first, so the emphasis follows the order rather than the | |
| 3599 | + // identity: whichever action leads is the primary button, and an item | |
| 3600 | + // with no guide still leads with Contact Support. | |
| 3601 | + $has_guide = ! empty( $item['guide_label'] ) && ! empty( $item['guide_url'] ); | |
| 3602 | + | |
| 3603 | + // Both keys, not either. An item contributed through | |
| 3604 | + // srfm_action_items may carry only guide_* keys -- reading cta_url | |
| 3605 | + // unguarded emits two PHP 8 undefined-key warnings and renders | |
| 3606 | + // href="" -- and a label without a URL renders an anchor that is not | |
| 3607 | + // keyboard focusable. React gates on the same pair. | |
| 3608 | + $has_cta = ! empty( $item['cta_label'] ) && ! empty( $item['cta_url'] ); | |
| 3609 | + ?> | |
| 3610 | + <p> | |
| 3611 | + <?php if ( $has_guide ) { ?> | |
| 3612 | + <a | |
| 3613 | + href="<?php echo esc_url( Helper::get_string_value( $item['guide_url'] ) ); ?>" | |
| 3614 | + class="button button-primary" | |
| 3615 | + data-srfm-notice-id="<?php echo esc_attr( Helper::get_string_value( $item['id'] ) ); ?>" | |
| 3616 | + data-srfm-button="<?php echo esc_attr( Helper::get_string_value( $item['guide_action'] ?? '' ) ); ?>" | |
| 3617 | + target="_blank" | |
| 3618 | + rel="noopener noreferrer" | |
| 3619 | + > | |
| 3620 | + <?php echo esc_html( $item['guide_label'] ); ?> | |
| 3621 | + </a> | |
| 3622 | + <?php } ?> | |
| 3623 | + <?php if ( $has_cta ) { ?> | |
| 3624 | + <a | |
| 3625 | + href="<?php echo esc_url( Helper::get_string_value( $item['cta_url'] ) ); ?>" | |
| 3626 | + class="<?php echo $has_guide ? 'button' : 'button button-primary'; ?>" | |
| 3627 | + data-srfm-notice-id="<?php echo esc_attr( Helper::get_string_value( $item['id'] ) ); ?>" | |
| 3628 | + data-srfm-button="<?php echo esc_attr( Helper::get_string_value( $item['cta_action'] ?? '' ) ); ?>" | |
| 3629 | + <?php | |
| 3630 | + // With details to fetch, the click opens them here instead | |
| 3631 | + // of following the href. The href stays as the no-JS | |
| 3632 | + // path: it goes to the dashboard, where the same details | |
| 3633 | + // are readable. | |
| 3634 | + if ( ! empty( $item['has_details'] ) ) { | |
| 3635 | + printf( | |
| 3636 | + 'data-srfm-details-for="%1$s" data-srfm-category="%2$s"', | |
| 3637 | + esc_attr( Helper::get_string_value( $item['id'] ) ), | |
| 3638 | + esc_attr( Helper::get_string_value( $item['category'] ?? '' ) ) | |
| 3639 | + ); | |
| 3640 | + } elseif ( 0 !== strpos( Helper::get_string_value( $item['cta_url'] ), 'mailto:' ) ) { | |
| 3641 | + echo 'target="_blank" rel="noopener noreferrer"'; | |
| 3642 | + } | |
| 3643 | + ?> | |
| 3644 | + > | |
| 3645 | + <?php echo esc_html( $item['cta_label'] ); ?> | |
| 3646 | + </a> | |
| 3647 | + <?php } ?> | |
| 3648 | + <?php if ( ! empty( $item['dismissible'] ) ) { ?> | |
| 3649 | + <a href="<?php echo esc_url( $this->get_dismiss_action_item_url( Helper::get_string_value( $item['id'] ) ) ); ?>" class="button"> | |
| 3650 | + <?php esc_html_e( 'Dismiss', 'sureforms' ); ?> | |
| 3651 | + </a> | |
| 3652 | + <?php } ?> | |
| 3653 | + </p> | |
| 3654 | + </div> | |
| 3655 | + <?php | |
| 3656 | + } | |
| 3657 | + } | |
| 3658 | + | |
| 3659 | + /** | |
| 3660 | + * Dismiss an action item from the classic notice's link. | |
| 3661 | + * | |
| 3662 | + * Hooked - admin_post_srfm_dismiss_action_item_link. | |
| 3663 | + * | |
| 3664 | + * @since 2.12.6 | |
| 3665 | + * @return void | |
| 3666 | + */ | |
| 3667 | + public function handle_dismiss_action_item_link() { | |
| 3668 | + if ( ! Helper::current_user_can() ) { | |
| 3669 | + wp_die( esc_html__( 'You do not have permission to do this.', 'sureforms' ), 403 ); | |
| 3670 | + } | |
| 3671 | + | |
| 3672 | + check_admin_referer( 'srfm_dismiss_action_item' ); | |
| 3673 | + | |
| 3674 | + $item_id = isset( $_GET['item'] ) ? sanitize_key( wp_unslash( $_GET['item'] ) ) : ''; | |
| 3675 | + | |
| 3676 | + $this->dismiss_action_item( $item_id ); | |
| 3677 | + | |
| 3678 | + $referer = wp_get_referer(); | |
| 3679 | + | |
| 3680 | + wp_safe_redirect( $referer ? $referer : admin_url() ); | |
| 3681 | + exit; | |
| 3682 | + } | |
| 3683 | + | |
| 3684 | + /** | |
| 3685 | + * Whether a first-party warning is currently on screen. | |
| 3686 | + * | |
| 3687 | + * Asked from the show_if of the rating, Getting Started and Thank You notices, | |
| 3688 | + * all of which are gated on nothing being wrong. "Wrong" has to mean the same | |
| 3689 | + * thing here as it does to the person looking at the screen. | |
| 3690 | + * | |
| 3691 | + * It used to re-state the conditions instead of reading them, and the | |
| 3692 | + * restatement was narrower than the display: has_persistent_failures() reads | |
| 3693 | + * the `submission` counter alone, while the notices and the Form Checks panel | |
| 3694 | + * warn on any open failure in any of the three categories. So an open | |
| 3695 | + * notification or integration failure left this false, and the review ask | |
| 3696 | + * appeared directly beneath "We noticed a notification failure on Contact | |
| 3697 | + * Form". Submission was covered only incidentally, by FAULT_THRESHOLD being 1 -- | |
| 3698 | + * raise that and it would have joined them. | |
| 3699 | + * | |
| 3700 | + * Derived from get_first_party_action_items() now, which is the thing that | |
| 3701 | + * builds those warnings, so the gate cannot drift from the display again. | |
| 3702 | + * | |
| 3703 | + * Two constraints kept from the previous version. It must not call | |
| 3704 | + * get_action_items(): that records an impression as a side effect and must | |
| 3705 | + * never run from a show_if. And it reads the first-party set specifically, so | |
| 3706 | + * an item contributed through `srfm_action_items` cannot suppress notices that | |
| 3707 | + * have nothing to do with it. | |
| 3708 | + * | |
| 3709 | + * Returns false with logging disabled, which is what makes those notices | |
| 3710 | + * eligible again on a site that has turned this surface off. Intended: with the | |
| 3711 | + * surface off there is nothing being reported. | |
| 3712 | + * | |
| 3713 | + * @since 2.12.6 | |
| 3714 | + * @return bool | |
| 3715 | + */ | |
| 3716 | + public function has_action_item_warnings() { | |
| 3717 | + if ( ! Client_Logger::is_enabled() ) { | |
| 3718 | + return false; | |
| 3719 | + } | |
| 3720 | + | |
| 3721 | + foreach ( $this->get_first_party_action_items() as $item ) { | |
| 3722 | + if ( ! is_array( $item ) ) { | |
| 3723 | + continue; | |
| 3724 | + } | |
| 3725 | + | |
| 3726 | + $status = Helper::get_string_value( $item['status'] ?? '' ); | |
| 3727 | + | |
| 3728 | + // Matches the renderers: 'success' is a passing check and an empty | |
| 3729 | + // status is not a warning either, so neither suppresses anything. | |
| 3730 | + if ( 'success' !== $status && '' !== $status ) { | |
| 3731 | + return true; | |
| 3732 | + } | |
| 3733 | + } | |
| 3734 | + | |
| 3735 | + return false; | |
| 3736 | + } | |
| 3737 | + | |
| 3738 | + /** | |
| 3739 | + * Things on this site that need the owner's attention, newest concern first. | |
| 3740 | + * | |
| 3741 | + * Fed to the dashboard sidebar carousel. Each entry is self-describing so the | |
| 3742 | + * front end has no rules of its own to keep in sync -- adding a new item here | |
| 3743 | + * makes it appear with no JavaScript change. | |
| 3744 | + * | |
| 3745 | + * `dismissible` separates a fault from advice. A run of failed submissions is | |
| 3746 | + * not something to wave away, and clears itself when a submission succeeds. A | |
| 3747 | + * caching plugin being present is information, so it can be dismissed. | |
| 3748 | + * | |
| 3749 | + * @since 2.12.6 | |
| 3750 | + * @return array<int,array<string,mixed>> | |
| 3751 | + */ | |
| 3752 | + public function get_action_items() { | |
| 3753 | + if ( ! Helper::current_user_can() ) { | |
| 3754 | + return []; | |
| 3755 | + } | |
| 3756 | + | |
| 3757 | + // Memoised for the request. This runs twice on every admin page -- once | |
| 3758 | + // building the localisation payload and once in the classic renderer -- and | |
| 3759 | + // each open category reads a log excerpt. It also records an impression, so | |
| 3760 | + // running twice counted twice. Matches the $thankyou_prompt_cache and | |
| 3761 | + // $setup_card_cache pattern already in this class. | |
| 3762 | + if ( null !== self::$action_items_cache ) { | |
| 3763 | + return self::$action_items_cache; | |
| 3764 | + } | |
| 3765 | + | |
| 3766 | + // Logging off is the opt-out for this surface. Not because the counters go | |
| 3767 | + // stale -- Client_Logger::record_failure() has no enabled check, and the | |
| 3768 | + // notification and integration categories are written by direct calls in | |
| 3769 | + // inc/form-submit.php that keep counting accurately with logging off. It is | |
| 3770 | + // simply the switch a site owner has to turn these notices off, and it | |
| 3771 | + // covers our own items only: the filter below still runs, because a third | |
| 3772 | + // party's advisory has nothing to do with SureForms' logging toggle. | |
| 3773 | + $warnings = []; | |
| 3774 | + | |
| 3775 | + if ( Client_Logger::is_enabled() ) { | |
| 3776 | + $warnings = $this->get_first_party_action_items(); | |
| 3777 | + } | |
| 3778 | + | |
| 3779 | + $this->track_action_item_impressions( $warnings ); | |
| 3780 | + | |
| 3781 | + /** | |
| 3782 | + * Filter the dashboard action items. | |
| 3783 | + * | |
| 3784 | + * Each entry needs id, status ('warning' or 'success'), title, message, | |
| 3785 | + * cta_label, cta_url and dismissible. Only ids in | |
| 3786 | + * handle_dismiss_action_item()'s allowlist can actually be dismissed, so | |
| 3787 | + * adding a dismissible item here also needs a line there. | |
| 3788 | + * | |
| 3789 | + * The details dialog is not available here: it is served by | |
| 3790 | + * handle_action_item_details(), which reads SureForms' own client error log | |
| 3791 | + * and knows nothing about a third-party item. Such an item's cta_url is | |
| 3792 | + * followed as a link, which is what it does with JavaScript off anyway. | |
| 3793 | + * | |
| 3794 | + * @since 2.12.6 | |
| 3795 | + * | |
| 3796 | + * @param array<int,array<string,mixed>> $items Action items. | |
| 3797 | + */ | |
| 3798 | + $items = Helper::apply_filters_as_array( 'srfm_action_items', $warnings ); | |
| 3799 | + | |
| 3800 | + // Both URLs normalised once, here, rather than trusting each renderer to do | |
| 3801 | + // it. Two things are being fixed at once. | |
| 3802 | + // | |
| 3803 | + // The scheme: the classic notice runs esc_url() and drops anything outside | |
| 3804 | + // the allowlist, while React assigns href directly and react-dom 18 leaves | |
| 3805 | + // a javascript: URL intact -- its sanitizeURL() only warns, and the warning | |
| 3806 | + // is compiled out of the production build. esc_url_raw() with the same | |
| 3807 | + // allowlist closes both. | |
| 3808 | + // | |
| 3809 | + // The ampersands: Helper::get_sureforms_website_url() returns an esc_url()'d | |
| 3810 | + // string, so a URL with UTM parameters arrives with & in it. In an HTML | |
| 3811 | + // href the browser decodes that; React sets the property directly, so the | |
| 3812 | + // entity would be sent to the server verbatim. Decoded to one raw form here, | |
| 3813 | + // and each renderer escapes it for its own context. | |
| 3814 | + foreach ( $items as $index => $item ) { | |
| 3815 | + // A filter may hand back an object. isset() on it returns false, which | |
| 3816 | + // would slip the item past both the URL normalisation and the | |
| 3817 | + // sanitize_key() below without any sign that it had. | |
| 3818 | + if ( ! is_array( $item ) ) { | |
| 3819 | + continue; | |
| 3820 | + } | |
| 3821 | + | |
| 3822 | + foreach ( [ 'cta_url', 'guide_url' ] as $key ) { | |
| 3823 | + if ( ! isset( $item[ $key ] ) ) { | |
| 3824 | + continue; | |
| 742 | 3825 | } |
| 3826 | + | |
| 3827 | + $items[ $index ][ $key ] = esc_url_raw( | |
| 3828 | + wp_specialchars_decode( Helper::get_string_value( $item[ $key ] ), ENT_QUOTES ), | |
| 3829 | + [ 'http', 'https', 'mailto' ] | |
| 3830 | + ); | |
| 743 | 3831 | } |
| 744 | - Entries_List_Table::handle_entry_status( $entry_id, $action, $view ); | |
| 3832 | + | |
| 3833 | + // The id ends up in a data attribute the dialog matches on with an | |
| 3834 | + // attribute selector, and in the dismiss allowlist. sanitize_key() is | |
| 3835 | + // what both dismiss paths already apply, so applying it once here means | |
| 3836 | + // the value that renders is the value they compare against -- and a | |
| 3837 | + // filter-contributed id carrying a quote cannot break the selector. | |
| 3838 | + if ( isset( $item['id'] ) ) { | |
| 3839 | + $items[ $index ]['id'] = sanitize_key( Helper::get_string_value( $item['id'] ) ); | |
| 3840 | + } | |
| 745 | 3841 | } |
| 3842 | + | |
| 3843 | + self::$action_items_cache = $items; | |
| 3844 | + | |
| 3845 | + return $items; | |
| 746 | 3846 | } |
| 747 | 3847 | |
| 3848 | + /** | |
| 3849 | + * Dismiss one action item. | |
| 3850 | + * | |
| 3851 | + * Hooked - wp_ajax_srfm_dismiss_action_item. | |
| 3852 | + * | |
| 3853 | + * Only items get_action_items() marks dismissible can be dismissed, so a | |
| 3854 | + * crafted request cannot silence a genuine fault. | |
| 3855 | + * | |
| 3856 | + * @since 2.12.6 | |
| 3857 | + * @return void | |
| 3858 | + */ | |
| 3859 | + public function handle_dismiss_action_item() { | |
| 3860 | + if ( ! Helper::current_user_can() ) { | |
| 3861 | + wp_send_json_error( [ 'message' => __( 'Unauthorized user.', 'sureforms' ) ], 403 ); | |
| 3862 | + return; | |
| 3863 | + } | |
| 3864 | + | |
| 3865 | + if ( ! check_ajax_referer( 'srfm_dismiss_action_item', 'nonce', false ) ) { | |
| 3866 | + wp_send_json_error( [ 'message' => __( 'Invalid nonce.', 'sureforms' ) ], 403 ); | |
| 3867 | + return; | |
| 3868 | + } | |
| 3869 | + | |
| 3870 | + $item_id = isset( $_POST['item_id'] ) ? sanitize_key( wp_unslash( $_POST['item_id'] ) ) : ''; | |
| 3871 | + | |
| 3872 | + if ( ! $this->dismiss_action_item( $item_id ) ) { | |
| 3873 | + wp_send_json_error( [ 'message' => __( 'Invalid parameters.', 'sureforms' ) ], 400 ); | |
| 3874 | + return; | |
| 3875 | + } | |
| 3876 | + | |
| 3877 | + wp_send_json_success(); | |
| 3878 | + } | |
| 3879 | + | |
| 3880 | + /** | |
| 3881 | + * The stylesheet for the notice carousel and the details dialog. | |
| 3882 | + * | |
| 3883 | + * In a stylesheet rather than inline style assignments in | |
| 3884 | + * notice-response.js, so the rules use logical properties, an RTL sheet can | |
| 3885 | + * override them, and a site can restyle the dialog without patching a script. | |
| 3886 | + * | |
| 3887 | + * Only the classic wp-admin surface needs these. The SureForms dashboard's | |
| 3888 | + * dialog is force-ui's, styled by the Tailwind build, so nothing here reaches | |
| 3889 | + * it -- the two surfaces share their strings, not their markup. | |
| 3890 | + * | |
| 3891 | + * Attached to a registered handle with no file of its own, which is the WP way | |
| 3892 | + * to ship CSS tied to one script. | |
| 3893 | + * | |
| 3894 | + * Hooked to admin_enqueue_scripts rather than called from the renderer. | |
| 3895 | + * admin_notices fires from admin-header.php after admin_print_styles has | |
| 3896 | + * flushed the head, so enqueuing there reached the page only through core's | |
| 3897 | + * late-styles pass in the footer -- and until that parsed, every stacked notice | |
| 3898 | + * rendered expanded before collapsing to one, the carousel controls overlapped | |
| 3899 | + * the notice text, and the defensive `display: none` on the hidden payload was | |
| 3900 | + * inert, which is the exact window that rule exists for. | |
| 3901 | + * | |
| 3902 | + * The buttons are painted explicitly. They carry core's `button` classes for | |
| 3903 | + * their shape and focus behaviour, and core paints those with | |
| 3904 | + * `var(--wp-admin-theme-color)` -- so without this the dialog renders in | |
| 3905 | + * whichever admin colour scheme the user picked, which on a default install is | |
| 3906 | + * blue, on a SureForms panel that is otherwise entirely brand orange. Same | |
| 3907 | + * approach and same values as print_srfm_notice_styles(). | |
| 3908 | + * | |
| 3909 | + * @since 2.12.7 | |
| 3910 | + * @return void | |
| 3911 | + */ | |
| 3912 | + public function enqueue_action_item_styles() { | |
| 3913 | + if ( wp_style_is( 'srfm-action-items', 'enqueued' ) ) { | |
| 3914 | + return; | |
| 3915 | + } | |
| 3916 | + | |
| 3917 | + if ( ! Helper::current_user_can() ) { | |
| 3918 | + return; | |
| 3919 | + } | |
| 3920 | + | |
| 3921 | + // Nothing to style unless the carousel is actually going to build. Cheap to | |
| 3922 | + // ask: get_action_items() is memoised for the request. | |
| 3923 | + // | |
| 3924 | + // Two, not one: notice-response.js bails below two cards, so these rules | |
| 3925 | + // have no consumer on a site with a single open fault. | |
| 3926 | + $notices = 0; | |
| 3927 | + | |
| 3928 | + foreach ( $this->get_action_items() as $item ) { | |
| 3929 | + $status = Helper::get_string_value( is_array( $item ) ? $item['status'] ?? '' : '' ); | |
| 3930 | + | |
| 3931 | + if ( 'success' !== $status && '' !== $status ) { | |
| 3932 | + $notices++; | |
| 3933 | + } | |
| 3934 | + } | |
| 3935 | + | |
| 3936 | + if ( $notices < 2 ) { | |
| 3937 | + return; | |
| 3938 | + } | |
| 3939 | + | |
| 3940 | + wp_register_style( 'srfm-action-items', false, [], SRFM_VER ); | |
| 3941 | + wp_enqueue_style( 'srfm-action-items' ); | |
| 3942 | + | |
| 3943 | + $css = <<<'CSS' | |
| 3944 | +.srfm-action-item-carousel { position: relative; } | |
| 3945 | +.srfm-action-item-carousel .srfm-action-item-notice { padding-inline-end: var(--srfm-carousel-reserve, 130px); } | |
| 3946 | +/* [hidden] is only a UA rule, and WordPress sets display on .notice, so a | |
| 3947 | + third-party admin sheet can otherwise put a notice the carousel has hidden back | |
| 3948 | + on screen. */ | |
| 3949 | +.srfm-action-item-carousel .srfm-action-item-notice[hidden] { display: none; } | |
| 3950 | +.srfm-action-item-carousel-nav { | |
| 3951 | + position: absolute; | |
| 3952 | + top: 8px; | |
| 3953 | + inset-inline-end: 12px; | |
| 3954 | + margin: 0; | |
| 3955 | + display: flex; | |
| 3956 | + align-items: center; | |
| 3957 | + gap: 8px; | |
| 3958 | +} | |
| 3959 | +.srfm-details-overlay { | |
| 3960 | + position: fixed; | |
| 3961 | + inset: 0; | |
| 3962 | + z-index: 999999; | |
| 3963 | + display: flex; | |
| 3964 | + align-items: center; | |
| 3965 | + justify-content: center; | |
| 3966 | + background: rgba(0, 0, 0, .5); | |
| 3967 | + padding: 16px; | |
| 3968 | +} | |
| 3969 | +.srfm-details-panel { | |
| 3970 | + background: #fff; | |
| 3971 | + border-radius: 8px; | |
| 3972 | + padding: 16px; | |
| 3973 | + width: 100%; | |
| 3974 | + max-width: 800px; | |
| 3975 | + box-shadow: 0 10px 30px rgba(0, 0, 0, .2); | |
| 3976 | +} | |
| 3977 | +.srfm-details-panel h2 { margin: 0 0 4px; font-size: 14px; } | |
| 3978 | +.srfm-details-panel .srfm-details-description { margin: 0 0 12px; color: #50575e; } | |
| 3979 | +.srfm-details-panel pre { | |
| 3980 | + margin: 0; | |
| 3981 | + max-height: 320px; | |
| 3982 | + overflow: auto; | |
| 3983 | + white-space: pre-wrap; | |
| 3984 | + word-break: break-word; | |
| 3985 | + background: #f6f7f7; | |
| 3986 | + padding: 12px; | |
| 3987 | + border-radius: 6px; | |
| 3988 | + font-size: 12px; | |
| 3989 | +} | |
| 3990 | +.srfm-details-actions { | |
| 3991 | + display: flex; | |
| 3992 | + gap: 8px; | |
| 3993 | + align-items: center; | |
| 3994 | + flex-wrap: wrap; | |
| 3995 | + justify-content: flex-end; | |
| 3996 | + margin: 12px 0 0; | |
| 3997 | +} | |
| 3998 | +.srfm-details-hint { | |
| 3999 | + margin-inline-end: auto; | |
| 4000 | + font-size: 12px; | |
| 4001 | + color: #4b5563; | |
| 4002 | +} | |
| 4003 | +/* Core paints .button with the admin colour scheme, so these say what they are | |
| 4004 | + rather than inheriting whichever scheme the user picked. */ | |
| 4005 | +.srfm-details-panel .srfm-details-close.button-link { | |
| 4006 | + color: #50575e; | |
| 4007 | + text-decoration: none; | |
| 4008 | +} | |
| 4009 | +.srfm-details-panel .srfm-details-close.button-link:hover, | |
| 4010 | +.srfm-details-panel .srfm-details-close.button-link:focus { | |
| 4011 | + color: #1e1e1e; | |
| 4012 | +} | |
| 4013 | +.srfm-details-panel .srfm-details-copy.button { | |
| 4014 | + background: #fff; | |
| 4015 | + border-color: #c3c4c7; | |
| 4016 | + color: #1e1e1e; | |
| 4017 | +} | |
| 4018 | +.srfm-details-panel .srfm-details-copy.button:hover, | |
| 4019 | +.srfm-details-panel .srfm-details-copy.button:focus { | |
| 4020 | + background: #f6f7f7; | |
| 4021 | + border-color: #8c8f94; | |
| 4022 | + color: #1e1e1e; | |
| 4023 | +} | |
| 4024 | +.srfm-details-panel .srfm-details-contact.button-primary, | |
| 4025 | +.srfm-details-panel .srfm-details-contact.button-primary:hover, | |
| 4026 | +.srfm-details-panel .srfm-details-contact.button-primary:focus { | |
| 4027 | + background: #D54407; | |
| 4028 | + border-color: #D54407; | |
| 4029 | + color: #fff; | |
| 4030 | + box-shadow: none; | |
| 4031 | + text-shadow: none; | |
| 4032 | + text-decoration: none; | |
| 4033 | +} | |
| 4034 | +.srfm-details-panel .srfm-details-contact.button-primary:hover, | |
| 4035 | +.srfm-details-panel .srfm-details-contact.button-primary:focus { | |
| 4036 | + background: #C83B00; | |
| 4037 | + border-color: #C83B00; | |
| 4038 | +} | |
| 4039 | +/* Grey rather than a dimmed orange fill. Core sets the disabled text colour with | |
| 4040 | + !important, so an orange background here leaves grey on orange at 1.31:1 -- | |
| 4041 | + and a control that cannot be used should not wear the primary colour anyway. | |
| 4042 | + This is what core gives every other disabled button, and what force-ui renders | |
| 4043 | + for the same state on the dashboard, so the two surfaces agree. */ | |
| 4044 | +.srfm-details-panel .srfm-details-contact.button-primary[aria-disabled="true"], | |
| 4045 | +.srfm-details-panel .srfm-details-contact.button-primary[aria-disabled="true"]:hover, | |
| 4046 | +.srfm-details-panel .srfm-details-contact.button-primary[aria-disabled="true"]:focus { | |
| 4047 | + background: #f6f7f7; | |
| 4048 | + border-color: #dcdcde; | |
| 4049 | + pointer-events: none; | |
| 4050 | + box-shadow: none; | |
| 4051 | +} | |
| 4052 | +.srfm-details-panel .button:focus { | |
| 4053 | + outline: 2px solid #D54407; | |
| 4054 | + outline-offset: 1px; | |
| 4055 | + box-shadow: none; | |
| 4056 | +} | |
| 4057 | +CSS; | |
| 4058 | + | |
| 4059 | + wp_add_inline_style( 'srfm-action-items', $css ); | |
| 4060 | + } | |
| 4061 | + | |
| 4062 | + /** | |
| 4063 | + * The details dialog's strings. | |
| 4064 | + * | |
| 4065 | + * One array, two consumers: the classic wp-admin dialog in | |
| 4066 | + * notice-response.js, and the dashboard's force-ui one. Declared here rather | |
| 4067 | + * than inline in each, because the same sentence written as `__()` in PHP and | |
| 4068 | + * again in JSX looks identical to translators until the first edit to either, | |
| 4069 | + * after which one surface silently reverts to English. | |
| 4070 | + * | |
| 4071 | + * @since 2.12.7 | |
| 4072 | + * @return array<string,string> | |
| 4073 | + */ | |
| 4074 | + private function get_details_dialog_labels() { | |
| 4075 | + return [ | |
| 4076 | + 'title' => __( 'Details', 'sureforms' ), | |
| 4077 | + 'description' => __( 'What we recorded about this problem. Copy it into your support request so we can start from the cause rather than a description of it.', 'sureforms' ), | |
| 4078 | + 'copy' => __( 'Copy details', 'sureforms' ), | |
| 4079 | + 'copied' => __( 'Copied', 'sureforms' ), | |
| 4080 | + 'contact' => __( 'Contact Support', 'sureforms' ), | |
| 4081 | + 'close' => __( 'Close', 'sureforms' ), | |
| 4082 | + // Shown beside the buttons rather than as a title attribute: | |
| 4083 | + // pointer-events:none suppresses the native tooltip, a title | |
| 4084 | + // never fires on keyboard focus, and screen readers commonly | |
| 4085 | + // drop it on an unavailable control -- so the sentence saying | |
| 4086 | + // why the button is inert could not be read by anyone. | |
| 4087 | + 'copyFirst' => __( 'Copy the details first, so you have them to paste.', 'sureforms' ), | |
| 4088 | + // The unlock changes the label, the icon and whether Contact | |
| 4089 | + // Support works, none of which was announced. This goes in a | |
| 4090 | + // role="status" node so it is. | |
| 4091 | + 'unlocked' => __( 'Copied. Contact Support is now available.', 'sureforms' ), | |
| 4092 | + 'copyFailed' => __( 'Your browser would not let us copy. Select the text above and copy it by hand.', 'sureforms' ), | |
| 4093 | + // The scrollable diagnostics block is focusable, so it needs a name of | |
| 4094 | + // its own. | |
| 4095 | + 'logRegion' => __( 'Recorded diagnostics', 'sureforms' ), | |
| 4096 | + // The dialog opens before its payload arrives -- see | |
| 4097 | + // handle_action_item_details() for why the report is not shipped with | |
| 4098 | + // the page. | |
| 4099 | + 'loading' => __( 'Collecting the details…', 'sureforms' ), | |
| 4100 | + 'unavailable' => __( 'We could not collect the details. Contact Support and describe what happened, and we will take it from there.', 'sureforms' ), | |
| 4101 | + ]; | |
| 4102 | + } | |
| 4103 | + | |
| 4104 | + /** | |
| 4105 | + * SureForms' own action items, before the filter. | |
| 4106 | + * | |
| 4107 | + * Split out so the Enable Logs gate in get_action_items() can sit above this | |
| 4108 | + * rather than above `srfm_action_items`. An item contributed through that | |
| 4109 | + * filter has nothing to do with SureForms' logging toggle, and was being | |
| 4110 | + * silenced by it. | |
| 4111 | + * | |
| 4112 | + * @since 2.12.7 | |
| 4113 | + * @return array<int,array<string,mixed>> | |
| 4114 | + */ | |
| 4115 | + private function get_first_party_action_items() { | |
| 4116 | + $warnings = []; | |
| 4117 | + $open = Client_Logger::get_open_failures(); | |
| 4118 | + | |
| 4119 | + // One item per category. They read differently to a site owner and must not | |
| 4120 | + // be collapsed: submissions failing means visitors cannot reach you, a | |
| 4121 | + // notification failing means you are not hearing about entries that did | |
| 4122 | + // save, an integration failing means a third party is not receiving them. | |
| 4123 | + $categories = [ | |
| 4124 | + 'submission' => [ | |
| 4125 | + 'id' => 'form_submission_error', | |
| 4126 | + /* translators: %s: form title. */ | |
| 4127 | + 'title' => __( 'We noticed a form submission failure on %s.', 'sureforms' ), | |
| 4128 | + 'generic' => __( 'We noticed a form submission failure.', 'sureforms' ), | |
| 4129 | + 'message' => __( 'Visitors may not be able to reach you, and their entries were not saved.', 'sureforms' ), | |
| 4130 | + ], | |
| 4131 | + 'notification' => [ | |
| 4132 | + 'id' => 'notification_error', | |
| 4133 | + /* translators: %s: form title. */ | |
| 4134 | + 'title' => __( 'We noticed a notification failure on %s.', 'sureforms' ), | |
| 4135 | + 'generic' => __( 'We noticed a notification failure.', 'sureforms' ), | |
| 4136 | + 'message' => __( 'The entry was saved, but we could not send the email about it. New entries may be coming in without you knowing.', 'sureforms' ), | |
| 4137 | + // Email is the one failure here a site owner can usually fix without | |
| 4138 | + // us: it is almost always SMTP not being configured. Offer the guide | |
| 4139 | + // alongside support rather than making them wait for a reply. | |
| 4140 | + 'guide' => Helper::get_sureforms_website_url( | |
| 4141 | + 'docs/troubleshooting-email-sending-in-sureforms/', | |
| 4142 | + [ | |
| 4143 | + 'utm_medium' => 'form_checks_notice', | |
| 4144 | + 'utm_content' => 'notification_error', | |
| 4145 | + ] | |
| 4146 | + ), | |
| 4147 | + ], | |
| 4148 | + 'integration' => [ | |
| 4149 | + 'id' => 'integration_error', | |
| 4150 | + /* translators: %s: form title. */ | |
| 4151 | + 'title' => __( 'We noticed an integration failure on %s.', 'sureforms' ), | |
| 4152 | + 'generic' => __( 'We noticed an integration failure.', 'sureforms' ), | |
| 4153 | + 'message' => __( 'The entry was saved, but we could not send it to a connected service.', 'sureforms' ), | |
| 4154 | + ], | |
| 4155 | + ]; | |
| 4156 | + | |
| 4157 | + foreach ( $categories as $category => $copy ) { | |
| 4158 | + if ( ! isset( $open[ $category ] ) ) { | |
| 4159 | + continue; | |
| 4160 | + } | |
| 4161 | + | |
| 4162 | + // Name the form. "A form is failing" is not actionable on a site with | |
| 4163 | + // twenty of them, and the title is the first thing anyone asks for. | |
| 4164 | + $form_title = Helper::get_string_value( $open[ $category ]['form_title'] ?? '' ); | |
| 4165 | + | |
| 4166 | + $warning = [ | |
| 4167 | + 'id' => $copy['id'], | |
| 4168 | + 'status' => 'error', | |
| 4169 | + 'title' => '' !== $form_title | |
| 4170 | + ? sprintf( $copy['title'], $form_title ) | |
| 4171 | + : $copy['generic'], | |
| 4172 | + 'message' => $copy['message'], | |
| 4173 | + // Shows what would be sent before anything is sent. Someone reporting | |
| 4174 | + // a fault on their own site is entitled to read the diagnostics and | |
| 4175 | + // the log first, and a support agent gets a cleaner paste than a | |
| 4176 | + // screenshot of a notice. | |
| 4177 | + 'cta_label' => __( 'View details', 'sureforms' ), | |
| 4178 | + // Where the classic wp-admin notice sends people, since it cannot open | |
| 4179 | + // the panel's dialog. The dashboard is where the details are readable. | |
| 4180 | + 'cta_url' => admin_url( 'admin.php?page=sureforms_menu' ), | |
| 4181 | + 'cta_action' => 'view_details', | |
| 4182 | + // Not the payload itself, only that one exists. The diagnostics are | |
| 4183 | + // fetched when the dialog opens -- see handle_action_item_details(). | |
| 4184 | + // | |
| 4185 | + // They used to ride along in the localisation JSON and in a hidden | |
| 4186 | + // div on every admin page. The content is authored by whoever | |
| 4187 | + // triggered the failure, and the client-error-log route is a public | |
| 4188 | + // endpoint gated on a submit token rather than a capability, so an | |
| 4189 | + // anonymous visitor can fill that excerpt. Broadcasting it to every | |
| 4190 | + // admin screen -- read or not -- put attacker-authored text in page | |
| 4191 | + // source site-wide and made any future escaping slip a | |
| 4192 | + // manage_options-context problem. On demand, it reaches only the | |
| 4193 | + // admin who asked for it. | |
| 4194 | + 'has_details' => true, | |
| 4195 | + // Which record to fetch. Not the payload, just the key. | |
| 4196 | + 'category' => $category, | |
| 4197 | + 'dismissible' => false, | |
| 4198 | + ]; | |
| 4199 | + | |
| 4200 | + // A second, optional action. Absent keys render nothing, so a category | |
| 4201 | + // without a guide needs no branch in either renderer, and neither does | |
| 4202 | + // an item contributed through srfm_action_items. | |
| 4203 | + if ( ! empty( $copy['guide'] ) ) { | |
| 4204 | + $warning['guide_label'] = __( 'Help Me Fix', 'sureforms' ); | |
| 4205 | + $warning['guide_url'] = $copy['guide']; | |
| 4206 | + $warning['guide_action'] = 'help_me_fix'; | |
| 4207 | + } | |
| 4208 | + | |
| 4209 | + $warnings[] = $warning; | |
| 4210 | + } | |
| 4211 | + | |
| 4212 | + $caching_plugin = Helper::get_active_caching_plugin(); | |
| 4213 | + | |
| 4214 | + if ( '' === $caching_plugin ) { | |
| 4215 | + return $warnings; | |
| 4216 | + } | |
| 4217 | + | |
| 4218 | + // Read here rather than at the top: with no caching plugin active nothing | |
| 4219 | + // consults it, and this is the only dismissible item. | |
| 4220 | + $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); | |
| 4221 | + | |
| 4222 | + if ( ! in_array( 'caching_plugin', $dismissed, true ) ) { | |
| 4223 | + $warnings[] = [ | |
| 4224 | + 'id' => 'caching_plugin', | |
| 4225 | + 'status' => 'warning', | |
| 4226 | + 'title' => sprintf( | |
| 4227 | + /* translators: %s: caching plugin name. */ | |
| 4228 | + __( '%s may interfere with your forms.', 'sureforms' ), | |
| 4229 | + $caching_plugin | |
| 4230 | + ), | |
| 4231 | + 'message' => __( 'Caching can show visitors an old copy of your form, or load its scripts in the wrong order.', 'sureforms' ), | |
| 4232 | + 'cta_label' => __( 'Help Me Fix', 'sureforms' ), | |
| 4233 | + 'cta_url' => Helper::get_caching_plugin_doc_url(), | |
| 4234 | + 'cta_action' => 'help_me_fix', | |
| 4235 | + 'dismissible' => true, | |
| 4236 | + ]; | |
| 4237 | + } | |
| 4238 | + | |
| 4239 | + return $warnings; | |
| 4240 | + } | |
| 4241 | + | |
| 4242 | + /** | |
| 4243 | + * Nonce-protected URL that repairs the entries table. | |
| 4244 | + * | |
| 4245 | + * Shared by both notice surfaces so there is one repair route, one nonce and one | |
| 4246 | + * place that counts the click. Private, so it stays off the public API and out of | |
| 4247 | + * the test-coverage gate. | |
| 4248 | + * | |
| 4249 | + * @since 2.12.6 | |
| 4250 | + * @return string | |
| 4251 | + */ | |
| 4252 | + private function get_database_repair_url() { | |
| 4253 | + return wp_nonce_url( | |
| 4254 | + admin_url( 'admin-post.php?action=srfm_repair_entries_table' ), | |
| 4255 | + 'srfm_repair_entries_table' | |
| 4256 | + ); | |
| 4257 | + } | |
| 4258 | + | |
| 4259 | + /** | |
| 4260 | + * The database notice body, which differs by what the repair will actually do. | |
| 4261 | + * | |
| 4262 | + * Two outcomes are possible and they are not equivalent to the person clicking: | |
| 4263 | + * when the entries table exists under a different prefix — a changed | |
| 4264 | + * `$table_prefix`, a restored dump, a security plugin that renamed tables and | |
| 4265 | + * skipped ours — the repair renames it back and every stored entry comes with | |
| 4266 | + * it. When there is nothing to adopt, the repair creates an empty table and the | |
| 4267 | + * old submissions are not recoverable from here. | |
| 4268 | + * | |
| 4269 | + * Promising the wrong one is how a maintenance prompt turns into a complaint, so | |
| 4270 | + * the copy states which is about to happen. | |
| 4271 | + * | |
| 4272 | + * @since 2.12.6 | |
| 4273 | + * @return string | |
| 4274 | + */ | |
| 4275 | + private function get_database_notice_message() { | |
| 4276 | + if ( '' !== Register::get_adoptable_entries_table() ) { | |
| 4277 | + return __( 'SureForms found your form entries stored under a different database table prefix. Reconnecting them takes a moment, and your existing entries will be kept.', 'sureforms' ); | |
| 4278 | + } | |
| 4279 | + | |
| 4280 | + return __( 'SureForms needs to update your database before it can save new form entries. This only takes a moment and will not change your forms or existing content. Entries submitted before now cannot be recovered from here.', 'sureforms' ); | |
| 4281 | + } | |
| 4282 | + | |
| 4283 | + /** | |
| 4284 | + * Count one sighting of the database notice, at most once per user per day. | |
| 4285 | + * | |
| 4286 | + * While the table is missing the notice renders on every admin page load, on two | |
| 4287 | + * surfaces. Counting each render would rewrite the autoloaded `srfm_options` blob | |
| 4288 | + * on every pageview of a site that is already broken, and one site left unfixed | |
| 4289 | + * would dominate the aggregate. Throttling to a day per user answers the question | |
| 4290 | + * that matters — how many people are seeing this — for one write. | |
| 4291 | + * | |
| 4292 | + * @since 2.12.6 | |
| 4293 | + * @return void | |
| 4294 | + */ | |
| 4295 | + private function track_database_notice_impression() { | |
| 4296 | + $user_id = get_current_user_id(); | |
| 4297 | + | |
| 4298 | + if ( ! $user_id ) { | |
| 4299 | + return; | |
| 4300 | + } | |
| 4301 | + | |
| 4302 | + $key = 'srfm_db_notice_seen_' . $user_id; | |
| 4303 | + | |
| 4304 | + if ( get_transient( $key ) ) { | |
| 4305 | + return; | |
| 4306 | + } | |
| 4307 | + | |
| 4308 | + set_transient( $key, 1, DAY_IN_SECONDS ); | |
| 4309 | + | |
| 4310 | + Analytics::events()->track( 'database_error_notice_shown', 'entries' ); | |
| 4311 | + } | |
| 4312 | + | |
| 4313 | + /** | |
| 4314 | + * Build the setup-card payload (uncached). See get_form_setup_card(). | |
| 4315 | + * | |
| 4316 | + * @since 2.12.4 | |
| 4317 | + * @return array<string,mixed>|null Card payload, or null when there is no candidate. | |
| 4318 | + */ | |
| 4319 | + private static function compute_form_setup_card() { | |
| 4320 | + if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) { | |
| 4321 | + return null; | |
| 4322 | + } | |
| 4323 | + | |
| 4324 | + // Negative cache. Deliberately not a `defined( 'ASTRA_SITES_VER' )` check: | |
| 4325 | + // Starter Templates defines that constant in its main plugin file, so it only | |
| 4326 | + // exists while the plugin is active, yet neither its uninstall.php nor its | |
| 4327 | + // deactivation hook removes the import marker. Gating on the constant would | |
| 4328 | + // silently switch this feature off for the very people it targets — anyone who | |
| 4329 | + // imported a starter template and then removed the one-shot import plugin. | |
| 4330 | + if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) { | |
| 4331 | + return null; | |
| 4332 | + } | |
| 4333 | + | |
| 4334 | + // Only forms created from an Astra Sites starter template — those carry the | |
| 4335 | + // marker Starter Templates stamps on imported posts (self::ASTRA_SITES_IMPORT_META). | |
| 4336 | + // Prime post + meta caches (the loop reads title, permalink and edit link | |
| 4337 | + // per candidate) so this is a single query, not a follow-up per form. | |
| 4338 | + $query = new \WP_Query( | |
| 4339 | + [ | |
| 4340 | + 'post_type' => SRFM_FORMS_POST_TYPE, | |
| 4341 | + 'post_status' => [ 'publish', 'draft', 'pending' ], | |
| 4342 | + 'posts_per_page' => 10, | |
| 4343 | + // ID breaks the tie: a starter-template import creates several forms | |
| 4344 | + // within the same second, so post_date alone leaves "the newest form" | |
| 4345 | + // up to MySQL and it can differ between page loads. | |
| 4346 | + 'orderby' => [ | |
| 4347 | + 'date' => 'DESC', | |
| 4348 | + 'ID' => 'DESC', | |
| 4349 | + ], | |
| 4350 | + 'no_found_rows' => true, | |
| 4351 | + 'update_post_meta_cache' => true, | |
| 4352 | + 'update_post_term_cache' => false, | |
| 4353 | + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; dashboard-only. | |
| 4354 | + [ | |
| 4355 | + 'key' => self::ASTRA_SITES_IMPORT_META, | |
| 4356 | + 'compare' => 'EXISTS', | |
| 4357 | + ], | |
| 4358 | + ], | |
| 4359 | + ] | |
| 4360 | + ); | |
| 4361 | + | |
| 4362 | + // Nothing on this site carries the marker — remember that, so the query does | |
| 4363 | + // not repeat on every load. Keyed on the query result rather than on anything | |
| 4364 | + // user-specific, so it is safe to share, and invalidated the moment a post is | |
| 4365 | + // stamped (see invalidate_starter_template_cache()). | |
| 4366 | + if ( empty( $query->posts ) ) { | |
| 4367 | + set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS ); | |
| 4368 | + } | |
| 4369 | + | |
| 4370 | + foreach ( $query->posts as $post ) { | |
| 4371 | + $form_id = (int) $post->ID; | |
| 4372 | + | |
| 4373 | + if ( ! current_user_can( 'edit_post', $form_id ) ) { | |
| 4374 | + continue; | |
| 4375 | + } | |
| 4376 | + | |
| 4377 | + $edit_link = get_edit_post_link( $form_id, 'raw' ); | |
| 4378 | + | |
| 4379 | + if ( empty( $edit_link ) ) { | |
| 4380 | + continue; | |
| 4381 | + } | |
| 4382 | + | |
| 4383 | + // The steps are shown as optional next-steps — their completion is not | |
| 4384 | + // computed, so the widget simply lists the actions the owner can take. | |
| 4385 | + return [ | |
| 4386 | + 'id' => $form_id, | |
| 4387 | + 'title' => get_the_title( $form_id ), | |
| 4388 | + 'edit_url' => $edit_link, | |
| 4389 | + // Deep-links to the email-notification panel where supported; falls | |
| 4390 | + // back to opening the editor when the focus handler isn't present. | |
| 4391 | + 'email_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ), | |
| 4392 | + // Deep-links to the Form Confirmation panel (the Thank You message). | |
| 4393 | + 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ), | |
| 4394 | + // Front-end instant-form page. get_permalink() only yields a working | |
| 4395 | + // URL for published forms; a draft/pending form has no public URL, so | |
| 4396 | + // omit the view link there (the empty() guard hides the icon). | |
| 4397 | + 'view_url' => 'publish' === $post->post_status ? (string) get_permalink( $form_id ) : '', | |
| 4398 | + ]; | |
| 4399 | + } | |
| 4400 | + | |
| 4401 | + return null; | |
| 4402 | + } | |
| 4403 | + | |
| 4404 | + /** | |
| 4405 | + * Build the Thank You prompt payload (uncached). See get_thankyou_prompt_forms(). | |
| 4406 | + * | |
| 4407 | + * @since 2.12.4 | |
| 4408 | + * @return array<int,array<string,mixed>> One entry, or none. | |
| 4409 | + */ | |
| 4410 | + private static function compute_thankyou_prompt_forms() { | |
| 4411 | + if ( ! defined( 'SRFM_FORMS_POST_TYPE' ) || ! post_type_exists( SRFM_FORMS_POST_TYPE ) ) { | |
| 4412 | + return []; | |
| 4413 | + } | |
| 4414 | + | |
| 4415 | + // Negative cache — this notice renders on every admin screen, so keeping the | |
| 4416 | + // query off installs that can never match is what matters here. See | |
| 4417 | + // self::NO_IMPORTED_FORMS_TRANSIENT for why this is not gated on whether | |
| 4418 | + // Starter Templates is still active: the marker outlives the plugin. | |
| 4419 | + if ( 'no' === get_transient( self::NO_IMPORTED_FORMS_TRANSIENT ) ) { | |
| 4420 | + return []; | |
| 4421 | + } | |
| 4422 | + | |
| 4423 | + // Only forms imported from a Starter Templates (Astra Sites) starter | |
| 4424 | + // template — see self::ASTRA_SITES_IMPORT_META. Prime post + meta caches | |
| 4425 | + // (the loop reads meta, title and creation time per candidate) so this is a | |
| 4426 | + // single query rather than the main query plus a follow-up per form. | |
| 4427 | + $query = new \WP_Query( | |
| 4428 | + [ | |
| 4429 | + 'post_type' => SRFM_FORMS_POST_TYPE, | |
| 4430 | + 'post_status' => 'publish', | |
| 4431 | + 'posts_per_page' => 10, | |
| 4432 | + // ID breaks the tie — an import creates several forms in the same | |
| 4433 | + // second, so post_date alone makes "newest" MySQL-dependent. | |
| 4434 | + 'orderby' => [ | |
| 4435 | + 'date' => 'DESC', | |
| 4436 | + 'ID' => 'DESC', | |
| 4437 | + ], | |
| 4438 | + 'no_found_rows' => true, | |
| 4439 | + 'update_post_meta_cache' => true, | |
| 4440 | + 'update_post_term_cache' => false, | |
| 4441 | + 'meta_query' => [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Bounded to 10 recent forms; admin-notice only. | |
| 4442 | + [ | |
| 4443 | + 'key' => self::ASTRA_SITES_IMPORT_META, | |
| 4444 | + 'compare' => 'EXISTS', | |
| 4445 | + ], | |
| 4446 | + ], | |
| 4447 | + ] | |
| 4448 | + ); | |
| 4449 | + | |
| 4450 | + // Nothing on this site carries the marker — remember that, so the query does | |
| 4451 | + // not repeat on every load. Keyed on the query result rather than on anything | |
| 4452 | + // user-specific, so it is safe to share, and invalidated the moment a post is | |
| 4453 | + // stamped (see invalidate_starter_template_cache()). | |
| 4454 | + if ( empty( $query->posts ) ) { | |
| 4455 | + set_transient( self::NO_IMPORTED_FORMS_TRANSIENT, 'no', WEEK_IN_SECONDS ); | |
| 4456 | + } | |
| 4457 | + | |
| 4458 | + $prompts = []; | |
| 4459 | + $now = time(); | |
| 4460 | + | |
| 4461 | + foreach ( $query->posts as $post ) { | |
| 4462 | + $form_id = (int) $post->ID; | |
| 4463 | + | |
| 4464 | + if ( ! current_user_can( 'edit_post', $form_id ) ) { | |
| 4465 | + continue; | |
| 4466 | + } | |
| 4467 | + | |
| 4468 | + $steps = [ | |
| 4469 | + // A destination for replies: an enabled notification with a recipient. | |
| 4470 | + 'replies' => ! self::form_has_reply_destination( $form_id ), | |
| 4471 | + // The thank-you message is still the shipped default. | |
| 4472 | + 'thankyou' => self::is_default_confirmation_message( $form_id ), | |
| 4473 | + ]; | |
| 4474 | + | |
| 4475 | + // Nothing left to finish — no card for this form. | |
| 4476 | + if ( ! $steps['replies'] && ! $steps['thankyou'] ) { | |
| 4477 | + continue; | |
| 4478 | + } | |
| 4479 | + | |
| 4480 | + $edit_link = get_edit_post_link( $form_id, 'raw' ); | |
| 4481 | + | |
| 4482 | + if ( empty( $edit_link ) ) { | |
| 4483 | + continue; | |
| 4484 | + } | |
| 4485 | + | |
| 4486 | + $created = get_post_time( 'U', true, $form_id ); | |
| 4487 | + $days_ago = is_int( $created ) ? (int) floor( ( $now - $created ) / DAY_IN_SECONDS ) : 0; | |
| 4488 | + | |
| 4489 | + $prompts[] = [ | |
| 4490 | + 'id' => $form_id, | |
| 4491 | + 'title' => get_the_title( $form_id ), | |
| 4492 | + 'days_ago' => max( 0, $days_ago ), | |
| 4493 | + 'steps' => $steps, | |
| 4494 | + 'edit_url' => $edit_link, | |
| 4495 | + // The editor reads srfm_focus to open the matching settings tab: | |
| 4496 | + // "notifications" lands on Email Notification (where the reply | |
| 4497 | + // destination is set, so the CTA can actually clear that step) and | |
| 4498 | + // "thankyou" on Form Confirmation. | |
| 4499 | + 'replies_url' => add_query_arg( 'srfm_focus', 'notifications', $edit_link ), | |
| 4500 | + 'thankyou_url' => add_query_arg( 'srfm_focus', 'thankyou', $edit_link ), | |
| 4501 | + ]; | |
| 4502 | + | |
| 4503 | + // One card is enough — surface only the latest form needing setup. | |
| 4504 | + break; | |
| 4505 | + } | |
| 4506 | + | |
| 4507 | + return $prompts; | |
| 4508 | + } | |
| 4509 | + | |
| 4510 | + /** | |
| 4511 | + * Build the Thank You notice's inner markup (title, sentence, action buttons). | |
| 4512 | + * | |
| 4513 | + * @param array<string,mixed> $form Prompt payload from get_thankyou_prompt_forms(). | |
| 4514 | + * | |
| 4515 | + * @since 2.12.4 | |
| 4516 | + * @return string | |
| 4517 | + */ | |
| 4518 | + private static function build_thankyou_notice_markup( $form ) { | |
| 4519 | + // The prompt only surfaces starter-template imports (see the meta gate), so | |
| 4520 | + // the form was created for the user rather than by them. Kept generic — no | |
| 4521 | + // per-step claim — so it is always accurate whatever the user has since | |
| 4522 | + // changed, while the action buttons point to the specific things to finish. | |
| 4523 | + $sentence = __( 'We’ve already created this form for you. Finish customising it so it’s ready to collect real submissions.', 'sureforms' ); | |
| 4524 | + | |
| 4525 | + return self::build_srfm_notice_markup( | |
| 4526 | + sprintf( | |
| 4527 | + /* translators: %s: form name. */ | |
| 4528 | + __( 'Finish setting up “%s”', 'sureforms' ), | |
| 4529 | + $form['title'] | |
| 4530 | + ), | |
| 4531 | + $sentence, | |
| 4532 | + [ | |
| 4533 | + [ | |
| 4534 | + 'text' => __( 'Edit form', 'sureforms' ), | |
| 4535 | + 'url' => $form['edit_url'], | |
| 4536 | + 'primary' => true, | |
| 4537 | + 'class' => 'srfm-ty-edit-form', | |
| 4538 | + 'external' => true, | |
| 4539 | + ], | |
| 4540 | + [ | |
| 4541 | + 'text' => __( 'Edit the Thank You message', 'sureforms' ), | |
| 4542 | + 'url' => $form['thankyou_url'], | |
| 4543 | + 'class' => 'srfm-ty-edit-thankyou', | |
| 4544 | + 'external' => true, | |
| 4545 | + ], | |
| 4546 | + [ | |
| 4547 | + 'text' => __( 'Set where replies go', 'sureforms' ), | |
| 4548 | + 'url' => $form['replies_url'], | |
| 4549 | + 'class' => 'srfm-ty-set-replies', | |
| 4550 | + 'external' => true, | |
| 4551 | + ], | |
| 4552 | + ] | |
| 4553 | + ); | |
| 4554 | + } | |
| 4555 | + | |
| 4556 | + /** | |
| 4557 | + * Build the shared SureForms admin-notice body: title, sentence, action row. | |
| 4558 | + * | |
| 4559 | + * One builder for every SureForms notice so they cannot drift into looking like | |
| 4560 | + * two different plugins. Everything is escaped here rather than by the caller — | |
| 4561 | + * the notices library runs the result through wp_kses_post(), which would strip | |
| 4562 | + * anything richer anyway. | |
| 4563 | + * | |
| 4564 | + * @param string $title Notice heading. | |
| 4565 | + * @param string $text Supporting sentence. | |
| 4566 | + * @param array<int,array<string,mixed>> $actions Action links. Each accepts | |
| 4567 | + * text, url, and optionally | |
| 4568 | + * primary, class, external, | |
| 4569 | + * dismiss and snooze (seconds). | |
| 4570 | + * @since 2.12.6 | |
| 4571 | + * @return string | |
| 4572 | + */ | |
| 4573 | + private static function build_srfm_notice_markup( $title, $text, $actions ) { | |
| 4574 | + ob_start(); | |
| 4575 | + ?> | |
| 4576 | + <p class="srfm-notice__title"><?php echo esc_html( $title ); ?></p> | |
| 4577 | + <p class="srfm-notice__text"><?php echo esc_html( $text ); ?></p> | |
| 4578 | + <p class="srfm-notice__actions"> | |
| 4579 | + <?php | |
| 4580 | + foreach ( $actions as $action ) { | |
| 4581 | + if ( empty( $action['text'] ) || ! isset( $action['url'] ) ) { | |
| 4582 | + continue; | |
| 4583 | + } | |
| 4584 | + | |
| 4585 | + $classes = [ 'button' ]; | |
| 4586 | + | |
| 4587 | + if ( ! empty( $action['primary'] ) ) { | |
| 4588 | + $classes[] = 'button-primary'; | |
| 4589 | + } | |
| 4590 | + | |
| 4591 | + // astra-notice-close is what the library binds its dismiss handler to. | |
| 4592 | + if ( ! empty( $action['dismiss'] ) ) { | |
| 4593 | + $classes[] = 'astra-notice-close'; | |
| 4594 | + } | |
| 4595 | + | |
| 4596 | + if ( ! empty( $action['class'] ) ) { | |
| 4597 | + $classes[] = $action['class']; | |
| 4598 | + } | |
| 4599 | + ?> | |
| 4600 | + <a | |
| 4601 | + class="<?php echo esc_attr( implode( ' ', $classes ) ); ?>" | |
| 4602 | + href="<?php echo esc_url( $action['url'] ); ?>" | |
| 4603 | + <?php echo empty( $action['snooze'] ) ? '' : ' data-repeat-notice-after="' . esc_attr( (string) $action['snooze'] ) . '"'; ?> | |
| 4604 | + <?php echo empty( $action['external'] ) ? '' : ' target="_blank" rel="noopener noreferrer"'; ?> | |
| 4605 | + ><?php echo esc_html( $action['text'] ); ?></a> | |
| 4606 | + <?php | |
| 4607 | + } | |
| 4608 | + ?> | |
| 4609 | + </p> | |
| 4610 | + <?php | |
| 4611 | + return (string) ob_get_clean(); | |
| 4612 | + } | |
| 4613 | + | |
| 4614 | + /** | |
| 4615 | + * Determine whether a notice callback is owned by SureForms. | |
| 4616 | + * | |
| 4617 | + * Recognises object methods on classes in the `SRFM` / `SRFM_PRO` namespaces | |
| 4618 | + * as well as the bundled notices libraries (`BSF_Admin_Notices` and the | |
| 4619 | + * legacy `Astra_Notices` alias). Everything else is treated as foreign. | |
| 4620 | + * | |
| 4621 | + * @param callable|array|string|null $function The registered callback function. | |
| 4622 | + * @since 2.10.0 | |
| 4623 | + * @return bool True when the callback belongs to SureForms, false otherwise. | |
| 4624 | + */ | |
| 4625 | + private function is_sureforms_owned_notice_callback( $function ) { | |
| 4626 | + $class_name = ''; | |
| 4627 | + | |
| 4628 | + if ( is_array( $function ) && isset( $function[0] ) ) { | |
| 4629 | + // Object or static method callback represented as an array. The first | |
| 4630 | + // element is either the object instance or the fully qualified class name. | |
| 4631 | + $class_name = is_object( $function[0] ) ? get_class( $function[0] ) : (string) $function[0]; | |
| 4632 | + } elseif ( is_string( $function ) && false !== strpos( $function, '::' ) ) { | |
| 4633 | + // Static method passed as "Class::method". | |
| 4634 | + $class_name = strstr( $function, '::', true ); | |
| 4635 | + } | |
| 4636 | + | |
| 4637 | + if ( '' === $class_name ) { | |
| 4638 | + // Plain function callbacks are never owned by SureForms. | |
| 4639 | + return false; | |
| 4640 | + } | |
| 4641 | + | |
| 4642 | + // SureForms (free and pro) namespaced classes. Pro's real namespace is | |
| 4643 | + // `SRFM_Pro\` (case-sensitive) — not the all-caps `SRFM_PRO_` constant | |
| 4644 | + // prefix — so match it case-insensitively to be safe. | |
| 4645 | + if ( 0 === strpos( $class_name, 'SRFM\\' ) || 0 === stripos( $class_name, 'SRFM_Pro\\' ) ) { | |
| 4646 | + return true; | |
| 4647 | + } | |
| 4648 | + | |
| 4649 | + // Bundled notices library shipped with SureForms. | |
| 4650 | + return in_array( $class_name, [ 'BSF_Admin_Notices', 'Astra_Notices' ], true ); | |
| 4651 | + } | |
| 4652 | + | |
| 4653 | + /** | |
| 4654 | + * Whether the current admin page is a SureForms-owned screen identified by a | |
| 4655 | + * `sureforms_*` / `srfm_*` `page` query slug. Complements | |
| 4656 | + * {@see Helper::is_sureforms_admin_page()} so foreign-notice suppression also | |
| 4657 | + * covers the payments / quiz / survey / learn / SMTP / partial-entries screens | |
| 4658 | + * that the core helper does not enumerate. Read-only screen check. | |
| 4659 | + * | |
| 4660 | + * @since 2.10.0 | |
| 4661 | + * @return bool | |
| 4662 | + */ | |
| 4663 | + private function is_sureforms_owned_admin_page() { | |
| 4664 | + if ( ! is_admin() || empty( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change. | |
| 4665 | + return false; | |
| 4666 | + } | |
| 4667 | + $page = sanitize_key( wp_unslash( $_GET['page'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only screen detection, no state change. | |
| 4668 | + return 0 === strpos( $page, 'sureforms' ) || 0 === strpos( $page, 'srfm' ); | |
| 4669 | + } | |
| 4670 | + | |
| 4671 | + /** | |
| 4672 | + * Callback for displaying the rating notice conditionally. | |
| 4673 | + * | |
| 4674 | + * Returns true if the user has 3 or more published forms or 3 or more form entries. | |
| 4675 | + * | |
| 4676 | + * @since 2.5.2 | |
| 4677 | + * @return bool | |
| 4678 | + */ | |
| 4679 | + private function maybe_display_rating_notice() { | |
| 4680 | + if ( null === $this->should_show_rating ) { | |
| 4681 | + $entries_count = Entries::get_total_entries_by_status( 'all' ); | |
| 4682 | + $form_count = wp_count_posts( SRFM_FORMS_POST_TYPE ); | |
| 4683 | + $this->should_show_rating = $entries_count >= self::RATING_NOTICE_THRESHOLD || Helper::get_integer_value( $form_count->publish ?? 0 ) >= self::RATING_NOTICE_THRESHOLD; | |
| 4684 | + } | |
| 4685 | + | |
| 4686 | + return $this->should_show_rating; | |
| 4687 | + } | |
| 4688 | + | |
| 4689 | + /** | |
| 4690 | + * Get random premium feature text. | |
| 4691 | + * | |
| 4692 | + * @return string Random feature text. | |
| 4693 | + * @since 1.9.1 | |
| 4694 | + */ | |
| 4695 | + private function get_random_premium_feature_text() { | |
| 4696 | + $features = [ | |
| 4697 | + __( 'Use Conditional Logic to show only what matters', 'sureforms' ), | |
| 4698 | + __( 'Split your form into steps to keep it easy', 'sureforms' ), | |
| 4699 | + __( 'Let people upload files directly to your form', 'sureforms' ), | |
| 4700 | + __( 'Turn responses into downloadable PDFs automatically', 'sureforms' ), | |
| 4701 | + __( 'Let users sign with a simple signature field', 'sureforms' ), | |
| 4702 | + __( 'Connect your form to other tools using webhooks', 'sureforms' ), | |
| 4703 | + __( 'Use Conversational Forms for a chat-like experience', 'sureforms' ), | |
| 4704 | + __( 'Let users register or log in through your form', 'sureforms' ), | |
| 4705 | + __( 'Build forms that create WordPress user accounts', 'sureforms' ), | |
| 4706 | + __( 'Add calculations to auto-total scores or prices', 'sureforms' ), | |
| 4707 | + ]; | |
| 4708 | + | |
| 4709 | + // Get a random feature. | |
| 4710 | + $random_key = array_rand( $features ); | |
| 4711 | + return $features[ $random_key ]; | |
| 4712 | + } | |
| 4713 | + | |
| 4714 | + /** | |
| 4715 | + * Render the dashboard widget footer for upsell. | |
| 4716 | + * | |
| 4717 | + * @param array $entries_data The entries data array. | |
| 4718 | + * @return void | |
| 4719 | + * @since 1.9.1 | |
| 4720 | + */ | |
| 4721 | + private function render_dashboard_widget_footer( $entries_data ) { | |
| 4722 | + // Only show footer if Pro is not active. | |
| 4723 | + if ( Helper::has_pro() ) { | |
| 4724 | + return; | |
| 4725 | + } | |
| 4726 | + | |
| 4727 | + // Count total entries in last 7 days. | |
| 4728 | + $total_entries = 0; | |
| 4729 | + foreach ( $entries_data as $form_data ) { | |
| 4730 | + $total_entries += $form_data['count']; | |
| 4731 | + } | |
| 4732 | + | |
| 4733 | + // Count total published forms. | |
| 4734 | + $published_forms_count = wp_count_posts( SRFM_FORMS_POST_TYPE )->publish; | |
| 4735 | + | |
| 4736 | + // Show footer only if 3+ entries received OR 3+ forms published. | |
| 4737 | + if ( $total_entries >= 3 || $published_forms_count >= 3 ) { | |
| 4738 | + ?> | |
| 4739 | + <div class="srfm-widget-footer"> | |
| 4740 | + <div class="srfm-upgrade-content"> | |
| 4741 | + <svg class="srfm-logo-icon" width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| 4742 | + <rect width="20" height="20" fill="#D54407"/> | |
| 4743 | + <path d="M5.7139 4.2854H14.2853V7.1425H7.1424L5.7139 8.5711V7.1425V4.2854Z" fill="white"/> | |
| 4744 | + <path d="M5.7139 4.2854H14.2853V7.1425H7.1424L5.7139 8.5711V7.1425V4.2854Z" fill="white"/> | |
| 4745 | + <path d="M5.7148 8.5713H12.8577V11.4284H7.1434L5.7148 12.857V11.4284V8.5713Z" fill="white"/> | |
| 4746 | + <path d="M5.7148 8.5713H12.8577V11.4284H7.1434L5.7148 12.857V11.4284V8.5713Z" fill="white"/> | |
| 4747 | + <path d="M5.7148 12.8569H10.0006V15.7141H5.7148V12.8569Z" fill="white"/> | |
| 4748 | + <path d="M5.7148 12.8569H10.0006V15.7141H5.7148V12.8569Z" fill="white"/> | |
| 4749 | + </svg> | |
| 4750 | + <span><?php echo esc_html( $this->get_random_premium_feature_text() ); ?></span> | |
| 4751 | + </div> | |
| 4752 | + <?php | |
| 4753 | + $upgrade_url = Helper::get_sureforms_website_url( 'pricing', [ 'utm_medium' => 'dashboard-widget' ] ); | |
| 4754 | + ?> | |
| 4755 | + <a href="<?php echo esc_url( $upgrade_url ); ?>" class="srfm-upgrade-link" target="_blank"> | |
| 4756 | + <?php esc_html_e( 'Upgrade', 'sureforms' ); ?> | |
| 4757 | + </a> | |
| 4758 | + </div> | |
| 4759 | + <?php | |
| 4760 | + } | |
| 4761 | + } | |
| 4762 | + | |
| 4763 | + /** | |
| 4764 | + * Determine if the admin pointer should be visible on this page. | |
| 4765 | + * | |
| 4766 | + * @since 1.8.0 | |
| 4767 | + * @return bool | |
| 4768 | + */ | |
| 4769 | + private function is_admin_pointer_visible() { | |
| 4770 | + global $pagenow; | |
| 4771 | + $allowed_pages = [ 'index.php', 'options-general.php' ]; | |
| 4772 | + | |
| 4773 | + // Do not show if pointer dismissed, accepted, or more than 1 form exists. | |
| 4774 | + if ( | |
| 4775 | + ! empty( Helper::get_srfm_option( 'pointer_popup_dismissed' ) ) | |
| 4776 | + || ! empty( Helper::get_srfm_option( 'pointer_popup_accepted' ) ) | |
| 4777 | + || (int) ( wp_count_posts( SRFM_FORMS_POST_TYPE )->publish ?? 0 ) > 1 | |
| 4778 | + ) { | |
| 4779 | + return false; | |
| 4780 | + } | |
| 4781 | + | |
| 4782 | + if ( in_array( $pagenow, $allowed_pages, true ) ) { | |
| 4783 | + return true; | |
| 4784 | + } | |
| 4785 | + | |
| 4786 | + return false; | |
| 4787 | + } | |
| 4788 | + | |
| 4789 | + /** | |
| 4790 | + * Nonced URL that dismisses one action item without JavaScript. | |
| 4791 | + * | |
| 4792 | + * The classic notice cannot use the AJAX dismissal the carousel uses, and | |
| 4793 | + * WordPress's own `is-dismissible` only hides the notice for that pageview. | |
| 4794 | + * | |
| 4795 | + * @param string $item_id Item to dismiss. | |
| 4796 | + * @since 2.12.6 | |
| 4797 | + * @return string | |
| 4798 | + */ | |
| 4799 | + private function get_dismiss_action_item_url( $item_id ) { | |
| 4800 | + return wp_nonce_url( | |
| 4801 | + add_query_arg( | |
| 4802 | + [ | |
| 4803 | + 'action' => 'srfm_dismiss_action_item_link', | |
| 4804 | + 'item' => $item_id, | |
| 4805 | + ], | |
| 4806 | + admin_url( 'admin-post.php' ) | |
| 4807 | + ), | |
| 4808 | + 'srfm_dismiss_action_item' | |
| 4809 | + ); | |
| 4810 | + } | |
| 4811 | + | |
| 4812 | + /** | |
| 4813 | + * Count one sighting of each warning, at most once per user per day. | |
| 4814 | + * | |
| 4815 | + * Throttled because the classic notice renders on every admin page: counting | |
| 4816 | + * each render would measure how much wp-admin someone browses, not how many | |
| 4817 | + * sites are affected. A day per user answers the question that matters -- how | |
| 4818 | + * many people are seeing this -- for one option write. | |
| 4819 | + * | |
| 4820 | + * Counts SureForms' own items only. It runs before `srfm_action_items`, so a | |
| 4821 | + * third party's contribution is not counted here -- SureForms has no name for | |
| 4822 | + * it and no analytics key that would mean anything. | |
| 4823 | + * | |
| 4824 | + * @param array<int,array<string,mixed>> $warnings SureForms' own items. | |
| 4825 | + * @since 2.12.6 | |
| 4826 | + * @return void | |
| 4827 | + */ | |
| 4828 | + private function track_action_item_impressions( $warnings ) { | |
| 4829 | + if ( empty( $warnings ) || wp_doing_ajax() ) { | |
| 4830 | + return; | |
| 4831 | + } | |
| 4832 | + | |
| 4833 | + $user_id = get_current_user_id(); | |
| 4834 | + | |
| 4835 | + if ( ! $user_id ) { | |
| 4836 | + return; | |
| 4837 | + } | |
| 4838 | + | |
| 4839 | + $counts = Helper::get_array_value( Helper::get_srfm_option( 'action_item_impressions', [] ) ); | |
| 4840 | + $changed = false; | |
| 4841 | + | |
| 4842 | + foreach ( $warnings as $warning ) { | |
| 4843 | + $item_id = Helper::get_string_value( $warning['id'] ?? '' ); | |
| 4844 | + | |
| 4845 | + if ( '' === $item_id ) { | |
| 4846 | + continue; | |
| 4847 | + } | |
| 4848 | + | |
| 4849 | + $seen_key = 'srfm_action_item_seen_' . $item_id . '_' . $user_id; | |
| 4850 | + | |
| 4851 | + if ( get_transient( $seen_key ) ) { | |
| 4852 | + continue; | |
| 4853 | + } | |
| 4854 | + | |
| 4855 | + set_transient( $seen_key, 1, DAY_IN_SECONDS ); | |
| 4856 | + | |
| 4857 | + $counts[ $item_id ] = Helper::get_integer_value( $counts[ $item_id ] ?? 0 ) + 1; | |
| 4858 | + $changed = true; | |
| 4859 | + | |
| 4860 | + // Cumulative, so $force = true: each new count is a new value and is | |
| 4861 | + // re-sent, while an identical repeat short-circuits inside track(). | |
| 4862 | + Analytics::events()->track( | |
| 4863 | + $item_id . '_notice_shown', | |
| 4864 | + (string) $counts[ $item_id ], | |
| 4865 | + [], | |
| 4866 | + true | |
| 4867 | + ); | |
| 4868 | + } | |
| 4869 | + | |
| 4870 | + if ( $changed ) { | |
| 4871 | + Helper::update_srfm_option( 'action_item_impressions', $counts ); | |
| 4872 | + } | |
| 4873 | + } | |
| 4874 | + | |
| 4875 | + /** | |
| 4876 | + * Record one interaction with a Form Checks notice, cumulatively. | |
| 4877 | + * | |
| 4878 | + * Both the value and `$force` matter. Analytics_Events::track() returns early | |
| 4879 | + * when the event name is already in `usage_events_pushed`, so a call with | |
| 4880 | + * `$force` omitted records each name at most once per site, ever -- the report | |
| 4881 | + * could then say whether a button had ever been clicked but not how often, and | |
| 4882 | + * these events exist to answer the second question. Sending a running total | |
| 4883 | + * with `$force = true` re-sends each new value while an identical repeat still | |
| 4884 | + * short-circuits inside track(). Same reasoning as | |
| 4885 | + * track_action_item_impressions(). | |
| 4886 | + * | |
| 4887 | + * @param string $event_name Analytics key from the allowlist. | |
| 4888 | + * @since 2.12.7 | |
| 4889 | + * @return void | |
| 4890 | + */ | |
| 4891 | + private function track_notice_event( $event_name ) { | |
| 4892 | + $counts = Helper::get_array_value( Helper::get_srfm_option( 'action_item_events', [] ) ); | |
| 4893 | + | |
| 4894 | + $counts[ $event_name ] = Helper::get_integer_value( $counts[ $event_name ] ?? 0 ) + 1; | |
| 4895 | + | |
| 4896 | + Helper::update_srfm_option( 'action_item_events', $counts ); | |
| 4897 | + | |
| 4898 | + Analytics::events()->track( $event_name, (string) $counts[ $event_name ], [], true ); | |
| 4899 | + } | |
| 4900 | + | |
| 4901 | + /** | |
| 4902 | + * The contact form's address, tagged with where the click came from. | |
| 4903 | + * | |
| 4904 | + * One campaign, tagged per failure, so the report answers which check actually | |
| 4905 | + * sends people to support rather than only how many arrive. A submission | |
| 4906 | + * failure and a caching advisory are different problems and it is worth knowing | |
| 4907 | + * which one drives the tickets. | |
| 4908 | + * | |
| 4909 | + * Prefilled with what SureForms already knows -- the admin's address, which | |
| 4910 | + * failure it is, and the site host -- so the person reporting a fault does not | |
| 4911 | + * retype it. Worth knowing that the address travels in the query string, so it | |
| 4912 | + * reaches browser history and any referrer along the way; it is the site | |
| 4913 | + * owner's own address going to SureForms' own form, which is the flow this | |
| 4914 | + * button exists for. | |
| 4915 | + * | |
| 4916 | + * Built with add_query_arg rather than string concatenation, so it stays | |
| 4917 | + * correct if the constant ever gains a query string of its own. | |
| 4918 | + * | |
| 4919 | + * @param string $category One of Client_Logger::CATEGORIES, naming the failure | |
| 4920 | + * the visitor is reporting. | |
| 4921 | + * @since 2.12.7 | |
| 4922 | + * @return string | |
| 4923 | + */ | |
| 4924 | + private function get_support_contact_url( $category ) { | |
| 4925 | + // Deliberately not translated. These are matched against the options on the | |
| 4926 | + // troubleshooting form, so they are machine values, not copy -- a German | |
| 4927 | + // site sending "E-Mail-Benachrichtigungsfehler" would arrive as an | |
| 4928 | + // unrecognised subject and land in the wrong queue. | |
| 4929 | + $subjects = [ | |
| 4930 | + 'submission' => 'Form submission failure', | |
| 4931 | + 'notification' => 'Email notification failure', | |
| 4932 | + 'integration' => 'Integration failure', | |
| 4933 | + ]; | |
| 4934 | + | |
| 4935 | + $user = wp_get_current_user(); | |
| 4936 | + | |
| 4937 | + $url = add_query_arg( | |
| 4938 | + [ | |
| 4939 | + // Prefills the form, so the person reporting a fault does not retype | |
| 4940 | + // what SureForms already knows. Empty rather than absent when the | |
| 4941 | + // address is unusable, so the form still opens. | |
| 4942 | + 'mail' => is_email( $user->user_email ) ? $user->user_email : '', | |
| 4943 | + // Falls back to "Other" for a category SureForms does not define -- | |
| 4944 | + // srfm_action_items is public, so an item can carry any category or | |
| 4945 | + // none. | |
| 4946 | + 'subject' => $subjects[ $category ] ?? 'Other', | |
| 4947 | + 'site_url' => Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ), | |
| 4948 | + 'utm_source' => 'sureforms', | |
| 4949 | + 'utm_medium' => 'form_checks', | |
| 4950 | + 'utm_campaign' => 'contact_support', | |
| 4951 | + // Which check sent them. The one part that differs per button, and | |
| 4952 | + // the reason for tagging at all. | |
| 4953 | + 'utm_content' => $category, | |
| 4954 | + ], | |
| 4955 | + self::SUPPORT_CONTACT_URL | |
| 4956 | + ); | |
| 4957 | + | |
| 4958 | + /** | |
| 4959 | + * Filter where the Contact Support action sends people. | |
| 4960 | + * | |
| 4961 | + * Replaces the `srfm_support_email_address` filter, which pointed at an | |
| 4962 | + * inbox and has no destination left to change now that the action opens a | |
| 4963 | + * form. A white-label install wants to point this at its own support page. | |
| 4964 | + * | |
| 4965 | + * @since 2.12.7 | |
| 4966 | + * | |
| 4967 | + * @param string $url Contact form URL, already UTM-tagged. | |
| 4968 | + * @param string $category The failure being reported. | |
| 4969 | + */ | |
| 4970 | + $filtered = Helper::get_string_value( apply_filters( 'srfm_support_contact_url', $url, $category ) ); | |
| 4971 | + | |
| 4972 | + // Escaped after the filter, not before: the point of escaping here is that | |
| 4973 | + // neither renderer has to trust what comes back. mailto: is allowed because | |
| 4974 | + // an inbox is a legitimate destination for a white-label support contact, | |
| 4975 | + // and get_action_items() already allows it on the sibling item URLs. | |
| 4976 | + $safe = esc_url_raw( $filtered, [ 'http', 'https', 'mailto' ] ); | |
| 4977 | + | |
| 4978 | + // Never empty. Contact Support is the only action that retires these | |
| 4979 | + // notices and they are dismissible => false, so returning '' for a filter | |
| 4980 | + // value that cannot survive escaping leaves an undismissable notice with | |
| 4981 | + // nothing on it that works. Falling back to SureForms' own form is worse | |
| 4982 | + // for a white-label than their own URL and better than a dead end, and the | |
| 4983 | + // unfiltered URL is built here rather than supplied, so it always escapes. | |
| 4984 | + return '' !== $safe ? $safe : esc_url_raw( $url, [ 'http', 'https' ] ); | |
| 4985 | + } | |
| 4986 | + | |
| 4987 | + /** | |
| 4988 | + * The log tail, formatted for pasting. | |
| 4989 | + * | |
| 4990 | + * One builder, so the text someone reads before sending is the text that gets | |
| 4991 | + * sent. They used to be built separately, which is how a "details" view drifts | |
| 4992 | + * from what it claims to show. | |
| 4993 | + * | |
| 4994 | + * The budget is a parameter because nothing here is going into a URL any more. | |
| 4995 | + * Client_Logger::get_tail()'s 1200-character default existed to fit a compose | |
| 4996 | + * URL; a clipboard and a <pre> have no such limit, so the dialog asks for more | |
| 4997 | + * and the note below describes the real constraint rather than a mail client | |
| 4998 | + * that is not in this flow. | |
| 4999 | + * | |
| 5000 | + * @param int $max_chars Characters of log to include. | |
| 5001 | + * @since 2.12.7 | |
| 5002 | + * @return string | |
| 5003 | + */ | |
| 5004 | + private function get_support_log_block( $max_chars = 1200 ) { | |
| 5005 | + $log = Client_Logger::get_tail( $max_chars ); | |
| 5006 | + $block = '---' . "\n"; | |
| 5007 | + | |
| 5008 | + if ( '' === $log['text'] ) { | |
| 5009 | + return $block . __( 'Debug log: no entries recorded.', 'sureforms' ); | |
| 5010 | + } | |
| 5011 | + | |
| 5012 | + $block .= sprintf( | |
| 5013 | + /* translators: 1: entries shown, 2: entries recorded. */ | |
| 5014 | + __( 'Debug log (most recent %1$d of %2$d entries)', 'sureforms' ), | |
| 5015 | + $log['shown'], | |
| 5016 | + $log['total'] | |
| 5017 | + ) . "\n"; | |
| 5018 | + | |
| 5019 | + // Fenced so it survives a reply and reads as data rather than prose wherever | |
| 5020 | + // Markdown is rendered. | |
| 5021 | + $block .= '```' . "\n" . $log['text'] . "\n" . '```'; | |
| 5022 | + | |
| 5023 | + if ( $log['shown'] < $log['total'] ) { | |
| 5024 | + $block .= "\n\n" . __( 'Older entries were left out to keep this excerpt readable. The full log can be downloaded from SureForms → Settings → General.', 'sureforms' ); | |
| 5025 | + } | |
| 5026 | + | |
| 5027 | + return $block; | |
| 5028 | + } | |
| 5029 | + | |
| 5030 | + /** | |
| 5031 | + * Subject and countless opening line for one kind of failure. | |
| 5032 | + * | |
| 5033 | + * Both come from here so they cannot drift apart: a subject naming one problem | |
| 5034 | + * over a body describing another is worse than either alone. The counted form | |
| 5035 | + * of the opening line lives in get_support_count_sentence(), which needs | |
| 5036 | + * `_n()`'s literals and so cannot be an array lookup. | |
| 5037 | + * | |
| 5038 | + * An unknown or absent category gets deliberately neutral wording. The | |
| 5039 | + * alternative -- defaulting to the submission copy -- states something specific | |
| 5040 | + * that may not be true, and an item contributed through srfm_action_items has no | |
| 5041 | + * category at all. | |
| 5042 | + * | |
| 5043 | + * @param string $category One of Client_Logger::CATEGORIES. | |
| 5044 | + * @since 2.12.7 | |
| 5045 | + * @return array{subject:string,anon:string} | |
| 5046 | + */ | |
| 5047 | + private function get_support_copy( $category ) { | |
| 5048 | + $copy = [ | |
| 5049 | + 'submission' => [ | |
| 5050 | + /* translators: %s: site host. */ | |
| 5051 | + 'subject' => __( 'SureForms: form submissions are failing on %s', 'sureforms' ), | |
| 5052 | + /* translators: %s: site host. */ | |
| 5053 | + 'anon' => __( 'SureForms has recorded form submissions on %s that could not be completed.', 'sureforms' ), | |
| 5054 | + ], | |
| 5055 | + 'notification' => [ | |
| 5056 | + /* translators: %s: site host. */ | |
| 5057 | + 'subject' => __( 'SureForms: notification emails are not being sent on %s', 'sureforms' ), | |
| 5058 | + /* translators: %s: site host. */ | |
| 5059 | + 'anon' => __( 'SureForms saved entries on %s but could not send the notification emails for them.', 'sureforms' ), | |
| 5060 | + ], | |
| 5061 | + 'integration' => [ | |
| 5062 | + /* translators: %s: site host. */ | |
| 5063 | + 'subject' => __( 'SureForms: an integration is not receiving entries on %s', 'sureforms' ), | |
| 5064 | + /* translators: %s: site host. */ | |
| 5065 | + 'anon' => __( 'SureForms saved entries on %s but could not pass them to a connected service.', 'sureforms' ), | |
| 5066 | + ], | |
| 5067 | + ]; | |
| 5068 | + | |
| 5069 | + if ( isset( $copy[ $category ] ) ) { | |
| 5070 | + return $copy[ $category ]; | |
| 5071 | + } | |
| 5072 | + | |
| 5073 | + return [ | |
| 5074 | + /* translators: %s: site host. */ | |
| 5075 | + 'subject' => __( 'SureForms: a problem with the forms on %s', 'sureforms' ), | |
| 5076 | + /* translators: %s: site host. */ | |
| 5077 | + 'anon' => __( 'SureForms has recorded a problem with the forms on %s.', 'sureforms' ), | |
| 5078 | + ]; | |
| 5079 | + } | |
| 5080 | + | |
| 5081 | + /** | |
| 5082 | + * The sentence that opens the support email, with the failure count in it. | |
| 5083 | + * | |
| 5084 | + * A switch with literal `_n()` calls rather than a singular/plural pair looked | |
| 5085 | + * up from an array. `_n()` has to see its two literals at extraction time to | |
| 5086 | + * emit an `msgid_plural`, and only that lets a locale supply the number of | |
| 5087 | + * forms it actually uses -- Polish and Russian need three, Arabic six, | |
| 5088 | + * Japanese one. Choosing on `1 === $count` in PHP is correct for English and | |
| 5089 | + * wrong everywhere with a different plural rule. | |
| 5090 | + * | |
| 5091 | + * @param string $category One of Client_Logger::CATEGORIES. Unknown or absent | |
| 5092 | + * gets neutral wording rather than a specific claim. | |
| 5093 | + * @param int $count Failures recorded for that category. | |
| 5094 | + * @since 2.12.7 | |
| 5095 | + * @return string | |
| 5096 | + */ | |
| 5097 | + private function get_support_count_sentence( $category, $count ) { | |
| 5098 | + switch ( $category ) { | |
| 5099 | + case 'submission': | |
| 5100 | + return sprintf( | |
| 5101 | + /* translators: %d: number of failed submissions. */ | |
| 5102 | + _n( | |
| 5103 | + 'SureForms has recorded %d form submission that could not be completed.', | |
| 5104 | + 'SureForms has recorded %d form submissions that could not be completed.', | |
| 5105 | + $count, | |
| 5106 | + 'sureforms' | |
| 5107 | + ), | |
| 5108 | + $count | |
| 5109 | + ); | |
| 5110 | + | |
| 5111 | + case 'notification': | |
| 5112 | + return sprintf( | |
| 5113 | + /* translators: %d: number of failed notifications. */ | |
| 5114 | + _n( | |
| 5115 | + 'SureForms saved %d entry but could not send the notification email for it.', | |
| 5116 | + 'SureForms saved %d entries but could not send the notification emails for them.', | |
| 5117 | + $count, | |
| 5118 | + 'sureforms' | |
| 5119 | + ), | |
| 5120 | + $count | |
| 5121 | + ); | |
| 5122 | + | |
| 5123 | + case 'integration': | |
| 5124 | + return sprintf( | |
| 5125 | + /* translators: %d: number of failed integration hand-offs. */ | |
| 5126 | + _n( | |
| 5127 | + 'SureForms saved %d entry but could not pass it to a connected service.', | |
| 5128 | + 'SureForms saved %d entries but could not pass them to a connected service.', | |
| 5129 | + $count, | |
| 5130 | + 'sureforms' | |
| 5131 | + ), | |
| 5132 | + $count | |
| 5133 | + ); | |
| 5134 | + | |
| 5135 | + default: | |
| 5136 | + return sprintf( | |
| 5137 | + /* translators: %d: number of recorded problems. */ | |
| 5138 | + _n( | |
| 5139 | + 'SureForms has recorded %d problem with the forms on this site.', | |
| 5140 | + 'SureForms has recorded %d problems with the forms on this site.', | |
| 5141 | + $count, | |
| 5142 | + 'sureforms' | |
| 5143 | + ), | |
| 5144 | + $count | |
| 5145 | + ); | |
| 5146 | + } | |
| 5147 | + } | |
| 5148 | + | |
| 5149 | + /** | |
| 5150 | + * Diagnostics block for the support report. | |
| 5151 | + * | |
| 5152 | + * Carries what support would otherwise have to ask for, so the first reply can | |
| 5153 | + * be an answer rather than a questionnaire. | |
| 5154 | + * | |
| 5155 | + * The count is the one for this category, not get_fault_streak(), which reports | |
| 5156 | + * submissions only -- so a notification failure used to quote a number from an | |
| 5157 | + * unrelated counter, often zero. | |
| 5158 | + * | |
| 5159 | + * @param string $category One of Client_Logger::CATEGORIES. | |
| 5160 | + * @param string $form_title Form the failure was recorded against, when known. | |
| 5161 | + * @since 2.12.6 | |
| 5162 | + * @return string | |
| 5163 | + */ | |
| 5164 | + private function get_support_message( $category = '', $form_title = '' ) { | |
| 5165 | + global $wp_version; | |
| 5166 | + | |
| 5167 | + $failures = Client_Logger::get_failures(); | |
| 5168 | + $count = Helper::get_integer_value( $failures[ $category ]['count'] ?? 0 ); | |
| 5169 | + $copy = $this->get_support_copy( $category ); | |
| 5170 | + | |
| 5171 | + // With nothing recorded, describe the failure without a number. The old | |
| 5172 | + // max( 1, $count ) reported "recorded 1 problem" and "Recorded failures: 1" | |
| 5173 | + // for a count nobody recorded -- a number support would then chase. | |
| 5174 | + $host = Helper::get_string_value( wp_parse_url( home_url(), PHP_URL_HOST ) ); | |
| 5175 | + | |
| 5176 | + $lines = [ | |
| 5177 | + __( 'Hello SureForms support,', 'sureforms' ), | |
| 5178 | + '', | |
| 5179 | + $count > 0 | |
| 5180 | + ? $this->get_support_count_sentence( $category, $count ) | |
| 5181 | + : sprintf( $copy['anon'], $host ), | |
| 5182 | + ]; | |
| 5183 | + | |
| 5184 | + if ( '' !== $form_title ) { | |
| 5185 | + $lines[] = ''; | |
| 5186 | + $lines[] = sprintf( | |
| 5187 | + /* translators: %s: form title. */ | |
| 5188 | + __( 'Form: %s', 'sureforms' ), | |
| 5189 | + $form_title | |
| 5190 | + ); | |
| 5191 | + } | |
| 5192 | + | |
| 5193 | + // Once: each call reads an option and a site option. | |
| 5194 | + $caching = Helper::get_active_caching_plugin(); | |
| 5195 | + | |
| 5196 | + $lines = array_merge( | |
| 5197 | + $lines, | |
| 5198 | + [ | |
| 5199 | + '', | |
| 5200 | + '---', | |
| 5201 | + __( 'Site details', 'sureforms' ), | |
| 5202 | + // Labels translated, values not. The site owner reads this on screen | |
| 5203 | + // before sending it, so the labels are copy; the values are machine | |
| 5204 | + // data -- a version, a URL, a plugin name -- and stay verbatim. The | |
| 5205 | + // debug log below is left alone entirely for the same reason. | |
| 5206 | + /* translators: %s: site address. */ | |
| 5207 | + sprintf( __( 'Site: %s', 'sureforms' ), home_url() ), | |
| 5208 | + /* translators: %s: SureForms version. */ | |
| 5209 | + sprintf( __( 'SureForms: %s', 'sureforms' ), SRFM_VER ), | |
| 5210 | + sprintf( | |
| 5211 | + /* translators: %s: SureForms Pro version, or a note that it is not active. */ | |
| 5212 | + __( 'SureForms Pro: %s', 'sureforms' ), | |
| 5213 | + Helper::has_pro() && defined( 'SRFM_PRO_VER' ) ? SRFM_PRO_VER : __( 'not active', 'sureforms' ) | |
| 5214 | + ), | |
| 5215 | + /* translators: %s: WordPress version. */ | |
| 5216 | + sprintf( __( 'WordPress: %s', 'sureforms' ), Helper::get_string_value( $wp_version ) ), | |
| 5217 | + /* translators: %s: PHP version. */ | |
| 5218 | + sprintf( __( 'PHP: %s', 'sureforms' ), PHP_VERSION ), | |
| 5219 | + sprintf( | |
| 5220 | + /* translators: %s: caching plugin name, or a note that none was detected. */ | |
| 5221 | + __( 'Caching: %s', 'sureforms' ), | |
| 5222 | + '' !== $caching ? $caching : __( 'none detected', 'sureforms' ) | |
| 5223 | + ), | |
| 5224 | + sprintf( | |
| 5225 | + /* translators: %s: number of recorded failures, or a note that none were. */ | |
| 5226 | + __( 'Recorded failures: %s', 'sureforms' ), | |
| 5227 | + $count > 0 ? Helper::get_string_value( $count ) : __( 'none recorded', 'sureforms' ) | |
| 5228 | + ), | |
| 5229 | + ] | |
| 5230 | + ); | |
| 5231 | + | |
| 5232 | + // Only when there is one. A repeat report is worth knowing about: the same | |
| 5233 | + // category having been reported before means the last answer did not hold, | |
| 5234 | + // which is a different conversation from a first report. Appended with the | |
| 5235 | + // rest of the site details rather than raised to the top, because it is | |
| 5236 | + // context for them rather than a headline. | |
| 5237 | + // | |
| 5238 | + // Survives only until the next success in that category, because | |
| 5239 | + // clear_category() unsets the whole record -- so in practice it is | |
| 5240 | + // reachable for 'integration', which has no success signal, and transient | |
| 5241 | + // for the other two. | |
| 5242 | + // | |
| 5243 | + // Stored as time(), a UTC epoch comparable with the sibling 'at', and | |
| 5244 | + // formatted here with wp_date() so it reads in the site's timezone rather | |
| 5245 | + // than the server's. | |
| 5246 | + $acked_at = Helper::get_integer_value( $failures[ $category ]['acked_at'] ?? 0 ); | |
| 5247 | + | |
| 5248 | + if ( $acked_at > 0 ) { | |
| 5249 | + $lines[] = sprintf( | |
| 5250 | + /* translators: %s: date and time of the previous report, in the site's timezone. */ | |
| 5251 | + __( 'Previously reported: %s', 'sureforms' ), | |
| 5252 | + Helper::get_string_value( wp_date( 'Y-m-d H:i T', $acked_at ) ) | |
| 5253 | + ); | |
| 5254 | + } | |
| 5255 | + | |
| 5256 | + return implode( "\n", $lines ); | |
| 5257 | + } | |
| 5258 | + | |
| 5259 | + /** | |
| 5260 | + * Record one dismissal, shared by the AJAX and no-JS entry points. | |
| 5261 | + * | |
| 5262 | + * Allowlisted, so only advisory items can be dismissed. A run of failed | |
| 5263 | + * submissions is a fault and must stay put until it actually resolves -- | |
| 5264 | + * otherwise a crafted request could silence the one message that matters. | |
| 5265 | + * | |
| 5266 | + * @param string $item_id Item to dismiss. | |
| 5267 | + * @since 2.12.6 | |
| 5268 | + * @return bool False when the id is not dismissible. | |
| 5269 | + */ | |
| 5270 | + private function dismiss_action_item( $item_id ) { | |
| 5271 | + if ( ! in_array( $item_id, [ 'caching_plugin' ], true ) ) { | |
| 5272 | + return false; | |
| 5273 | + } | |
| 5274 | + | |
| 5275 | + $dismissed = Helper::get_array_value( Helper::get_srfm_option( 'dismissed_action_items', [] ) ); | |
| 5276 | + | |
| 5277 | + if ( ! in_array( $item_id, $dismissed, true ) ) { | |
| 5278 | + $dismissed[] = $item_id; | |
| 5279 | + Helper::update_srfm_option( 'dismissed_action_items', $dismissed ); | |
| 5280 | + | |
| 5281 | + // Recorded here rather than at each caller: both the cross in the | |
| 5282 | + // dashboard panel and the no-JS link in the classic notice land here. | |
| 5283 | + $this->track_notice_event( $item_id . '_notice_dismiss' ); | |
| 5284 | + } | |
| 5285 | + | |
| 5286 | + return true; | |
| 5287 | + } | |
| 748 | 5288 | } |