menu, 'register' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_assets' ) ); add_filter( 'plugin_action_links_' . FORMPILOT_PLUGIN_BASENAME, array( $this, 'action_links' ) ); add_filter( 'set_screen_option_' . FormsListTable::PER_PAGE_OPTION, array( $this, 'save_screen_option' ), 10, 3 ); add_filter( 'set_screen_option_' . EntriesListTable::PER_PAGE_OPTION, array( $this, 'save_screen_option' ), 10, 3 ); add_action( 'admin_notices', array( $this, 'captcha_notice' ) ); } /** * Warns when a captcha is selected but cannot be used. * * A provider missing a key is treated as switched off, which is the right * call — a widget that cannot be verified would lock every visitor out — but * it used to happen in complete silence. A typo in the secret field turned the * site's spam protection off and nothing said so, on any screen, ever. */ public function captcha_notice(): void { // Shown only where it is relevant: the plugin's own screens and the Plugins screen. $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null; if ( ! $screen || ( 'plugins' !== $screen->id && ! $this->menu->owns_screen( (string) $screen->id ) ) ) { return; } $provider = CaptchaManager::misconfigured(); if ( '' === $provider || ! Capabilities::current_user_can( Capabilities::MANAGE_SETTINGS ) ) { return; } printf( '

%s

', esc_html( sprintf( /* translators: %s: Name of the selected captcha provider. */ __( 'FormPilot — Smart Form Builder: %s is selected as the spam check but one of its keys is missing, so no challenge is being shown and no spam check is running. Add the missing key under FormPilot → Settings → Security.', 'devplugins-custom-form-builder' ), $provider ) ) ); } /** * Persists the per page screen option. * * WordPress discards the value unless a filter claims it. * * @param mixed $status Value returned by earlier filters. * @param string $option Option being saved. * @param mixed $value Value submitted by the user. * * @return mixed The value to store. */ public function save_screen_option( mixed $status, string $option, mixed $value ): mixed { $known = array( FormsListTable::PER_PAGE_OPTION, EntriesListTable::PER_PAGE_OPTION ); if ( ! in_array( $option, $known, true ) ) { return $status; } return max( 1, (int) $value ); } /** * Returns the menu registrar. */ public function menu(): Menu { return $this->menu; } /** * Enqueues the admin assets on the plugin screens. * * @param string $hook_suffix Current admin page hook suffix. */ public function enqueue_assets( string $hook_suffix ): void { if ( ! $this->menu->owns_screen( $hook_suffix ) ) { return; } wp_enqueue_style( Assets::ADMIN_STYLE ); wp_enqueue_script( Assets::ADMIN_SCRIPT ); wp_localize_script( Assets::ADMIN_SCRIPT, 'formpilotAdmin', array( 'confirmDelete' => __( 'This form and all of its entries will be deleted permanently. Continue?', 'devplugins-custom-form-builder' ), ) ); /* * No `wp_set_script_translations()` here on purpose. Every string the * scripts show reaches them through `wp_localize_script()`, already * translated on the server, so there is no `wp.i18n` call for a JSON * translation file to satisfy. Registering one would ask translators to * produce files nothing ever loads. * * If a script starts calling `wp.i18n.__()`, this needs to come back * along with the `wp-i18n` dependency in Assets::register_admin_assets(). */ if ( $hook_suffix !== $this->menu->hook_suffix( FormEditPage::SLUG ) ) { return; } wp_enqueue_script( Assets::BUILDER_SCRIPT ); wp_enqueue_script( Assets::WEBHOOKS_SCRIPT ); wp_localize_script( Assets::WEBHOOKS_SCRIPT, 'formpilotWebhooks', array( 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'action' => Webhooks::TEST_ACTION, 'nonce' => wp_create_nonce( Webhooks::TEST_NONCE ), 'integrationAction' => Integrations::TEST_ACTION, 'integrationNonce' => wp_create_nonce( Integrations::TEST_NONCE ), 'strings' => array( 'sending' => __( 'Sending…', 'devplugins-custom-form-builder' ), 'needUrl' => __( 'Enter a URL first.', 'devplugins-custom-form-builder' ), 'error' => __( 'The test could not be completed.', 'devplugins-custom-form-builder' ), ), ) ); wp_localize_script( Assets::BUILDER_SCRIPT, 'formpilotBuilder', array( 'confirmRemoveField' => __( 'Remove this field? Answers already collected for it will be deleted when you save.', 'devplugins-custom-form-builder' ), 'untitled' => __( 'Untitled field', 'devplugins-custom-form-builder' ), /* translators: %1$s: Number of fields. */ 'fieldCount' => __( '%1$s fields', 'devplugins-custom-form-builder' ), /* translators: %1$s: Field label. */ 'fieldAdded' => __( '%1$s added.', 'devplugins-custom-form-builder' ), /* translators: %1$s: Field label. */ 'fieldDuplicated' => __( '%1$s duplicated.', 'devplugins-custom-form-builder' ), /* translators: %1$s: Field label. */ 'fieldRemoved' => __( '%1$s removed.', 'devplugins-custom-form-builder' ), /* translators: 1: Field label. 2: New position. 3: Total number of fields. */ 'fieldMoved' => __( '%1$s moved to position %2$s of %3$s.', 'devplugins-custom-form-builder' ), 'undone' => __( 'Change undone.', 'devplugins-custom-form-builder' ), 'redone' => __( 'Change redone.', 'devplugins-custom-form-builder' ), 'collapsedAll' => __( 'All field settings collapsed.', 'devplugins-custom-form-builder' ), 'expandedAll' => __( 'All field settings expanded.', 'devplugins-custom-form-builder' ), ) ); } /** * Adds a shortcut to the plugin row on the Plugins screen. * * @param array $links Existing action links. * * @return array The filtered action links. */ public function action_links( array $links ): array { if ( ! Capabilities::current_user_can( Capabilities::MANAGE_FORMS ) ) { return $links; } $shortcut = sprintf( '%2$s', esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG ) ), esc_html__( 'Forms', 'devplugins-custom-form-builder' ) ); array_unshift( $links, $shortcut ); return $links; } }