templates = $templates; } /** * Registers the hooks. */ public function register_hooks() { add_action( 'add_meta_boxes_' . Post_Types::FORM, array( $this, 'add_box' ) ); add_action( 'save_post_' . Post_Types::FORM, array( $this, 'save' ) ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue' ) ); add_action( 'admin_post_' . self::POLICY_ACTION, array( $this, 'add_policy_paragraph' ) ); add_action( 'admin_post_' . self::POLICY_COVERED_ACTION, array( $this, 'policy_covered' ) ); } /** * Helps the business with the privacy policy of the site. The form * already tells visitors how their request is handled, under the consent * box: this only offers to add the same information to the privacy * policy page, in one click, never on its own. * * @param int $form_id Form post ID. */ private function privacy_notice( $form_id ) { if ( ! current_user_can( 'manage_privacy_options' ) ) { return; } $page = self::policy_page(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only flag set by add_policy_paragraph() after its own nonce check. if ( $page && isset( $_GET['primiko_policy'] ) && 'added' === $_GET['primiko_policy'] ) { ?>

post_status ) : ?>

ID || ! current_user_can( 'edit_post', $page->ID ) ) { return; } $url = wp_nonce_url( add_query_arg( array( 'action' => self::POLICY_ACTION, 'form' => $form_id, ), admin_url( 'admin-post.php' ) ), self::POLICY_ACTION ); $covered = wp_nonce_url( add_query_arg( array( 'action' => self::POLICY_COVERED_ACTION, 'form' => $form_id, ), admin_url( 'admin-post.php' ) ), self::POLICY_COVERED_ACTION ); ?>

post_type && 'trash' !== $page->post_status ? $page : null; } /** * Tells whether the privacy policy page still holds the added paragraph. * * @param \WP_Post $page Privacy policy page. * @return bool */ private static function has_policy_paragraph( $page ) { return false !== strpos( $page->post_content, self::POLICY_CLASS ); } /** * Adds the paragraph about quote requests at the end of the privacy * policy page, when the business clicks the button, once. */ public function add_policy_paragraph() { check_admin_referer( self::POLICY_ACTION ); $form_id = isset( $_GET['form'] ) ? absint( $_GET['form'] ) : 0; $page = self::policy_page(); if ( ! $page || ! current_user_can( 'manage_privacy_options' ) || ! current_user_can( 'edit_post', $page->ID ) ) { wp_die( esc_html__( 'You are not allowed to change the privacy policy page.', 'primiko' ), 403 ); } if ( ! self::has_policy_paragraph( $page ) ) { $blocks = '

' . esc_html__( 'Quote requests', 'primiko' ) . '

'; foreach ( Privacy::policy_paragraphs() as $paragraph ) { $blocks .= "\n\n

" . esc_html( $paragraph ) . '

'; } wp_update_post( array( 'ID' => $page->ID, 'post_content' => self::insert_paragraph( $page->post_content, $blocks ), ) ); } wp_safe_redirect( add_query_arg( 'primiko_policy', 'added', get_edit_post_link( $form_id, 'url' ) ) ); exit; } /** * Places the paragraph at the end of the page, but before a closing line * such as "This page was updated in October 2026.": a short last * paragraph with a year in it. * * @param string $content Content of the privacy policy page. * @param string $blocks Block markup to add. * @return string */ private static function insert_paragraph( $content, $blocks ) { $parsed = parse_blocks( $content ); $last = null; foreach ( $parsed as $index => $block ) { if ( null !== $block['blockName'] || '' !== trim( $block['innerHTML'] ) ) { $last = $index; } } if ( null !== $last && 'core/paragraph' === $parsed[ $last ]['blockName'] ) { $text = trim( wp_strip_all_tags( $parsed[ $last ]['innerHTML'] ) ); if ( strlen( $text ) < 160 && preg_match( '/\b(19|20)\d{2}\b/', $text ) ) { array_splice( $parsed, $last, 0, parse_blocks( $blocks . "\n\n" ) ); return serialize_blocks( $parsed ); } } return rtrim( $content ) . "\n\n" . $blocks; } /** * Remembers that the privacy policy page already covers quote requests, * when the business says so: the offer is not shown again for this page. */ public function policy_covered() { check_admin_referer( self::POLICY_COVERED_ACTION ); $form_id = isset( $_GET['form'] ) ? absint( $_GET['form'] ) : 0; $page = self::policy_page(); if ( ! current_user_can( 'manage_privacy_options' ) ) { wp_die( esc_html__( 'You are not allowed to change the privacy policy page.', 'primiko' ), 403 ); } if ( $page ) { update_option( self::POLICY_COVERED_OPTION, $page->ID, false ); } wp_safe_redirect( get_edit_post_link( $form_id, 'url' ) ); exit; } /** * Adds the settings box. */ public function add_box() { add_meta_box( 'primiko-form-settings', __( 'Form settings', 'primiko' ), array( $this, 'render' ), Post_Types::FORM, 'normal', 'high' ); add_meta_box( 'primiko-questions', __( 'Questions and messages', 'primiko' ), array( $this, 'render_questions' ), Post_Types::FORM, 'normal', 'default' ); } /** * Loads the questions editor on the form screens. * * @param string $hook Admin page. */ public function enqueue( $hook ) { $screen = get_current_screen(); $asset = PRIMIKO_DIR . 'build/admin/index.asset.php'; if ( ! in_array( $hook, array( 'post.php', 'post-new.php' ), true ) || ! $screen || Post_Types::FORM !== $screen->post_type || ! is_readable( $asset ) ) { return; } $asset = require $asset; wp_enqueue_script( 'primiko-admin', PRIMIKO_URL . 'build/admin/index.js', $asset['dependencies'], $asset['version'], true ); wp_set_script_translations( 'primiko-admin', 'primiko', PRIMIKO_DIR . 'languages' ); wp_enqueue_style( 'wp-components' ); if ( is_readable( PRIMIKO_DIR . 'build/admin/style-index.css' ) ) { wp_enqueue_style( 'primiko-admin', PRIMIKO_URL . 'build/admin/style-index.css', array( 'wp-components' ), $asset['version'] ); } } /** * Prints the mount point of the questions editor, with what it needs: * every template, translated, and the customizations already saved. * The editor writes its result in a hidden field saved with the form. * * @param \WP_Post $post Form post. */ public function render_questions( $post ) { $templates = array(); foreach ( array_keys( $this->templates->choices() ) as $id ) { $template = $this->templates->localized( $id ); foreach ( Customizations::MESSAGES as $key ) { if ( empty( $template['messages'][ $key ] ) ) { $template['messages'][ $key ] = Mailer::default_message( $key ); } } $templates[ $id ] = $template; } $saved = get_post_meta( $post->ID, Customizations::META, true ); $config = array( 'templates' => $templates, 'customizations' => is_array( $saved ) ? $saved : array(), ); ?>

ID, '_primiko_template', true ); $recipients = implode( "\n", (array) get_post_meta( $post->ID, '_primiko_recipients', true ) ); wp_nonce_field( self::NONCE, self::NONCE ); $this->privacy_notice( $post->ID ); ?> post_status ) : ?> templates->choices() ) as $id ) { $template = $this->templates->localized( $id ); $templates[] = array( 'id' => $id, 'title' => $template['title'], 'description' => $template['description'], 'keywords' => $template['keywords'], 'category' => $template['category'], ); } return array( 'templates' => $templates, 'categories' => Templates::categories(), ); } /** * Saves the settings. * * @param int $post_id Form post ID. */ public function save( $post_id ) { if ( ! isset( $_POST[ self::NONCE ] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST[ self::NONCE ] ) ), self::NONCE ) ) { return; } if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || wp_is_post_revision( $post_id ) || ! current_user_can( 'edit_post', $post_id ) ) { return; } $template = isset( $_POST['primiko_template'] ) ? sanitize_key( wp_unslash( $_POST['primiko_template'] ) ) : ''; if ( null !== $this->templates->get( $template ) ) { update_post_meta( $post_id, '_primiko_template', $template ); } $raw = isset( $_POST['primiko_recipients'] ) ? sanitize_textarea_field( wp_unslash( $_POST['primiko_recipients'] ) ) : ''; $recipients = array(); foreach ( preg_split( '/[\s,;]+/', $raw ) as $email ) { $email = sanitize_email( $email ); if ( is_email( $email ) ) { $recipients[] = $email; } } update_post_meta( $post_id, '_primiko_recipients', array_values( array_unique( $recipients ) ) ); $contact = isset( $_POST['primiko_contact'] ) ? sanitize_key( wp_unslash( $_POST['primiko_contact'] ) ) : ''; if ( in_array( $contact, array( 'both', 'phone', 'either' ), true ) ) { update_post_meta( $post_id, '_primiko_contact', $contact ); delete_post_meta( $post_id, '_primiko_email_optional' ); } $this->save_customizations( $post_id ); } /** * Saves what the questions editor sent. An empty field means the editor * did not load: the saved customizations stay as they are. * * @param int $post_id Form post ID. */ private function save_customizations( $post_id ) { // Decoded below, then every value is checked and sanitized in Customizations::sanitize(). $json = isset( $_POST['primiko_customizations'] ) ? wp_unslash( $_POST['primiko_customizations'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.NonceVerification.Missing -- Nonce checked in save(). if ( ! is_string( $json ) || '' === $json || strlen( $json ) > 262144 ) { return; } $data = json_decode( $json, true, 16 ); $template = is_array( $data ) && isset( $data['template'] ) && is_string( $data['template'] ) ? $this->templates->localized( sanitize_key( $data['template'] ) ) : null; if ( null === $template ) { return; } $custom = Customizations::sanitize( isset( $data['custom'] ) ? $data['custom'] : array(), $template ); Customizations::save( $post_id, $template['id'], $custom ); } }