admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'dealroot_form_scan' ), 'urls' => Form_Scan::targets(), 'manual' => self::manual_selectors(), 'outdated' => Form_Scan::outdated(), /* translators: 1: number of the page being scanned, 2: number of pages */ 'updating' => __( 'Updating the list for this version of Dealroot: looking at page %1$s of %2$s…', 'dealroot' ), 'picked' => array_values( wp_list_pluck( Form_Selection::get()['custom'], 'selector' ) ), 'declined' => array_slice( Form_Selection::get()['declined'], 0, 100 ), 'home' => home_url( '/' ), 'ownPage' => __( 'Enter the address of a page on this site.', 'dealroot' ), 'done' => add_query_arg( array( 'page' => self::PAGE, 'scanned' => 1, ), admin_url( 'admin.php' ) ), /* translators: 1: number of the page being scanned, 2: number of pages */ 'progress' => __( 'Looking at page %1$s of %2$s…', 'dealroot' ), 'saving' => __( 'Saving the result…', 'dealroot' ), 'failed' => __( 'Your pages could not be opened from this browser. If the site is in maintenance mode or protected by a password, turn that off for a moment and try again.', 'dealroot' ), 'error' => __( 'The result could not be saved. Please reload the page and try again.', 'dealroot' ), ); } /** * Forms of the last scan in short: name, type, whether they are saved as leads and the * first page they are on. Embedded forms whose submissions never reach the site are left out. * * @return array */ public static function summary() { $scan = Form_Scan::last(); if ( ! $scan ) { return array(); } $selection = Form_Selection::get(); $saved = wp_list_pluck( $selection['custom'], 'name', 'selector' ); $plugins = Form_Scan::plugin_classes(); $out = array(); foreach ( ( new self() )->rows( $scan ) as $form ) { $name = (string) $form['name']; switch ( $form['kind'] ) { case 'plugin': $on = ! in_array( $form['key'], $selection['excluded'], true ); $type = isset( $plugins[ $form['plugin'] ] ) ? $plugins[ $form['plugin'] ]::label() : (string) $form['plugin']; $name = '' !== $name ? $name : '#' . $form['id']; break; case 'custom': $picked = isset( $saved[ $form['selector'] ] ); $on = $picked || ( ! in_array( $form['selector'], $selection['declined'], true ) && '' === $form['tag'] ); $name = $picked && '' !== (string) $saved[ $form['selector'] ] ? (string) $saved[ $form['selector'] ] : $name; $type = 'newsletter' === $form['tag'] ? __( 'Newsletter sign-up', 'dealroot' ) : __( 'Form without a form plugin', 'dealroot' ); break; case 'embed': if ( '' === $form['plugin'] ) { continue 2; } $on = ! in_array( $form['key'], $selection['excluded'], true ); $type = (string) $form['provider']; break; case 'auto': $on = true; $type = isset( $plugins[ $form['plugin'] ] ) ? $plugins[ $form['plugin'] ]::label() : (string) $form['plugin']; break; case 'coded': $on = true; $type = __( 'Always saved', 'dealroot' ); break; default: continue 2; } $out[] = array( 'name' => '' !== $name ? $name : __( 'Form', 'dealroot' ), 'type' => $type, 'on' => $on, 'page' => ! empty( $form['pages'][0]['url'] ) ? (string) $form['pages'][0]['url'] : '', ); } return $out; } /** * Stores the result of a scan. */ public function ajax_scan() { check_ajax_referer( 'dealroot_form_scan', 'nonce' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( null, 403 ); } $raw = isset( $_POST['results'] ) ? wp_unslash( $_POST['results'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- JSON, every value is checked in Form_Scan::store(). $data = is_string( $raw ) && strlen( $raw ) <= 1048576 ? json_decode( $raw, true ) : null; if ( ! is_array( $data ) ) { wp_send_json_error( null, 400 ); } $merge = ! empty( $_POST['merge'] ); $scan = Form_Scan::store( $data, $merge ); Form_Selection::pick_found( $scan['forms'] ); if ( $merge && isset( $_POST['page'] ) ) { Form_Scan::add_page( sanitize_url( wp_unslash( $_POST['page'] ) ) ); } wp_send_json_success( array( 'forms' => count( $scan['forms'] ) ) ); } /** * Selectors from the "Other forms" setting. * * @return string[] */ private static function manual_selectors() { return array_values( array_filter( array_map( 'trim', explode( ',', (string) Settings::get( 'capture_selector' ) ) ), 'strlen' ) ); } /** * Adds a form by its ID or selector, e.g. one in a pop-up that the scan does not find. */ public function handle_add() { check_admin_referer( 'dealroot_add_form' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $selector = Form_Selection::from_input( isset( $_POST['selector'] ) ? sanitize_text_field( wp_unslash( $_POST['selector'] ) ) : '' ); $name = isset( $_POST['name'] ) ? mb_substr( sanitize_text_field( wp_unslash( $_POST['name'] ) ), 0, 191 ) : ''; $args = array( 'page' => self::PAGE ); if ( '' === $selector ) { $args['invalid'] = 1; } else { Form_Selection::add( $selector, $name ); $args['added'] = 1; } wp_safe_redirect( add_query_arg( $args, admin_url( 'admin.php' ) ) ); exit; } /** * Moves the leads and form figures of a renamed form to its new name, so the reports go on * without a break. * * @param string $from Name before. * @param string $to New name. */ private static function rename( $from, $to ) { global $wpdb; $from = trim( $from ); $to = mb_substr( trim( $to ), 0, 191 ); if ( '' === $from || '' === $to || $from === $to ) { return; } // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching $wpdb->query( $wpdb->prepare( "UPDATE %i SET form_name = %s, form_id = %s WHERE form_plugin = 'html' AND ( form_id = %s OR form_name = %s )", \Dealroot\Installer::table(), $to, mb_substr( sanitize_title( $to ), 0, 64 ), mb_substr( sanitize_title( $from ), 0, 64 ), $from ) ); $wpdb->query( $wpdb->prepare( 'UPDATE %i SET form = %s WHERE form = %s', \Dealroot\Installer::forms_table(), $to, $from ) ); // phpcs:enable } /** * Saves which forms count as leads. */ public function handle_save() { check_admin_referer( 'dealroot_save_forms' ); if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to change these settings.', 'dealroot' ), 403 ); } $listed = isset( $_POST['listed'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['listed'] ) ) : array(); $checked = isset( $_POST['capture'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['capture'] ) ) : array(); $rows = isset( $_POST['custom'] ) ? (array) wp_unslash( $_POST['custom'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized below and in Form_Selection::save(). $current = Form_Selection::get(); // Forms that were not on the screen keep their setting. $excluded = array_merge( array_diff( $current['excluded'], $listed ), array_diff( $listed, $checked ) ); $custom = array(); $declined = $current['declined']; foreach ( $rows as $row ) { if ( ! is_array( $row ) ) { continue; } $selector = Form_Selection::sanitize_selector( (string) ( $row['selector'] ?? '' ) ); if ( '' === $selector ) { continue; } if ( empty( $row['on'] ) ) { $declined[] = $selector; continue; } $declined = array_diff( $declined, array( $selector ) ); $custom[] = array( 'selector' => $selector, 'name' => sanitize_text_field( (string) ( $row['name'] ?? '' ) ), ); // A form given another name keeps its leads under the new one. self::rename( sanitize_text_field( (string) ( $row['was'] ?? '' ) ), sanitize_text_field( (string) ( $row['name'] ?? '' ) ) ); } Form_Selection::save( $excluded, $custom, $declined ); wp_safe_redirect( add_query_arg( array( 'page' => self::PAGE, 'saved' => 1, ), admin_url( 'admin.php' ) ) ); exit; } /** * Page output. */ public function render() { if ( ! current_user_can( 'manage_options' ) ) { return; } $scan = Form_Scan::last(); $added = (bool) Form_Selection::get()['custom']; Shell::open( 'forms', __( 'Forms', 'dealroot' ), array( 'description' => __( 'Dealroot looks at the pages of your site like a visitor and lists every form it finds, with the pages it is on. Ticked forms are saved as leads.', 'dealroot' ), 'class' => 'dealroot-forms-page', ) ); ?>

, enter contact-form.', 'dealroot' ); ?>

scan_box( $scan ); if ( $scan || $added || Form_Discovery::all() ) { $this->table( $scan ? $scan : array( 'time' => 0, 'pages' => 0, 'failed' => 0, 'forms' => array(), ) ); } $this->add_box(); ?>

0 ) : ?>

rows( $scan ); $selection = Form_Selection::get(); $counts = Leads::counts_by_form(); $plugins = Form_Scan::plugin_classes(); if ( ! $rows ) { echo '

' . esc_html__( 'No forms were found on the pages that were scanned.', 'dealroot' ) . '

'; return; } ?>
$form ) : ?> row( $index, $form, $selection, $counts, $plugins ); ?>

integration class. */ private function row( $index, array $form, array $selection, array $counts, array $plugins ) { $field = 'dealroot-form-' . $index; $leads = null; switch ( $form['kind'] ) { case 'plugin': $leads = $counts[ $form['key'] ] ?? 0; ?> > > embed_cells( $form, false ); break; } $leads = self::count_prefix( $counts, $form['plugin'] . ':' ); ?> > embed_cells( $form, true ); break; case 'coded': $leads = $counts[ 'html:' . sanitize_title( $form['name'] ) ] ?? 0; ?> data-dealroot in your theme. To stop saving it, remove the attribute there.', 'dealroot' ), array( 'code' => array() ) ); } else { esc_html_e( 'Listed in the settings under Forms → Other forms. To stop saving it, remove it there and scan again.', 'dealroot' ); } ?> pages( $form ); ?> '; } ?> ' . esc_html( sprintf( __( 'Embedded from %s', 'dealroot' ), $form['provider'] ) ) . ''; if ( ! $captured ) { $note = __( 'It runs on the service’s own servers and does not tell your site about submissions, so Dealroot cannot save them.', 'dealroot' ); } elseif ( in_array( $form['plugin'], array( 'typeform', 'calendly' ), true ) ) { /* translators: %s: name of a service such as Calendly */ $note = sprintf( __( '%s tells your site that a visitor sent the form or booked, but not the answers: the lead shows where the visitor came from, without name or email.', 'dealroot' ), $form['provider'] ); } else { $note = __( 'Saved with the visitor’s answers when the form is sent.', 'dealroot' ); } ?> $count ) { if ( 0 === strpos( (string) $key, $prefix ) ) { $total += (int) $count; } } return $total; } /** * Links to the pages a form is on. * * @param array $form Form. */ private function pages( array $form ) { if ( ! empty( $form['found'] ) ) { $sent = $form['found']; $count = max( 1, (int) $sent['count'] ); $label = '' !== (string) $sent['title'] ? (string) $sent['title'] : (string) wp_parse_url( (string) $sent['url'], PHP_URL_PATH ); /* translators: %s: number of times */ echo esc_html( sprintf( _n( 'Sent %s time by a visitor, not found by the scan:', 'Sent %s times by visitors, not found by the scan:', $count, 'dealroot' ), number_format_i18n( $count ) ) ) . ' '; if ( '' !== (string) $sent['url'] ) { echo '' . esc_html( '' !== $label ? $label : (string) $sent['url'] ) . ''; } return; } if ( ! $form['pages'] ) { echo '' . esc_html( 'custom' === $form['kind'] ? __( 'Not found by the scan. Forms in pop-ups often are not; their submissions are saved anyway. To see the page here, scan it above.', 'dealroot' ) : __( 'Not found in the last scan', 'dealroot' ) ) . ''; return; } $links = array(); foreach ( array_slice( $form['pages'], 0, 3 ) as $page ) { $label = '' !== $page['title'] ? $page['title'] : (string) wp_parse_url( $page['url'], PHP_URL_PATH ); $links[] = '' . esc_html( $label ) . ''; } $more = count( $form['pages'] ) - 3; if ( $more > 0 ) { /* translators: %s: number of further pages */ $links[] = esc_html( sprintf( _n( 'and %s more page', 'and %s more pages', $more, 'dealroot' ), number_format_i18n( $more ) ) ); } echo wp_kses( implode( ', ', $links ), array( 'a' => array( 'href' => array(), 'target' => array(), 'rel' => array(), ), ) ); } /** * Forms from the scan, plus picked or switched off forms the scan did not find, so they * can still be changed. * * @param array $scan Last scan. * @return array */ private function rows( array $scan ) { $order = array( 'plugin' => 0, 'custom' => 1, 'coded' => 2, 'auto' => 3, 'embed' => 4, ); $selection = Form_Selection::get(); $manual = self::manual_selectors(); $rows = array(); foreach ( $scan['forms'] as $form ) { $via = (string) ( $form['via'] ?? '' ); // A scan of an earlier version did not record why a form is always saved; the screen // scans again by itself, and the form is listed with its reason then. if ( 'coded' === $form['kind'] && '' === $via ) { continue; } // A form found through a selector that was removed from the settings since the scan // can be switched on or off like any other. if ( 'coded' === $form['kind'] && 'attribute' !== $via && ! in_array( $via, $manual, true ) ) { if ( '' === (string) ( $form['selector'] ?? '' ) ) { continue; } $form['kind'] = 'custom'; $form['key'] = 'custom:' . $form['selector']; } if ( ! isset( $rows[ $form['key'] ] ) ) { $rows[ $form['key'] ] = $form; } } // Forms that visitors sent and the scan did not find. foreach ( Form_Discovery::all() as $selector => $sent ) { $key = 'custom:' . $selector; if ( ! isset( $rows[ $key ] ) && Form_Discovery::is_valid( $selector ) ) { $rows[ $key ] = self::missing_row( 'custom', $key, (string) $sent['name'], array( 'selector' => (string) $selector, 'tag' => ! empty( $sent['newsletter'] ) ? 'newsletter' : 'found', 'found' => $sent, ) ); } } $rows = array_values( $rows ); $found = wp_list_pluck( $rows, 'key' ); foreach ( $selection['custom'] as $form ) { if ( ! in_array( 'custom:' . $form['selector'], $found, true ) ) { $rows[] = self::missing_row( 'custom', 'custom:' . $form['selector'], (string) $form['name'], array( 'selector' => (string) $form['selector'] ) ); } } $plugins = Form_Scan::plugin_classes(); foreach ( $selection['excluded'] as $key ) { if ( ! in_array( $key, $found, true ) ) { list( $plugin, $id ) = explode( ':', $key, 2 ); if ( isset( Registry::EMBEDS[ $plugin ] ) ) { $rows[] = self::missing_row( 'embed', $key, Registry::EMBEDS[ $plugin ], array( 'plugin' => $plugin, 'provider' => Registry::EMBEDS[ $plugin ], ) ); continue; } $rows[] = self::missing_row( 'plugin', $key, isset( $plugins[ $plugin ] ) && ctype_digit( $id ) ? (string) $plugins[ $plugin ]::form_title( (int) $id ) : '', array( 'plugin' => $plugin, 'id' => $id, ) ); } } usort( $rows, static function ( $a, $b ) use ( $order ) { $kind = ( $order[ $a['kind'] ] ?? 9 ) <=> ( $order[ $b['kind'] ] ?? 9 ); return 0 !== $kind ? $kind : strnatcasecmp( (string) $a['name'], (string) $b['name'] ); } ); return $rows; } /** * Row for a form that is set up but was not found. * * @param string $kind plugin or custom. * @param string $key Key. * @param string $name Name. * @param array $extra Further fields. * @return array */ private static function missing_row( $kind, $key, $name, array $extra ) { return array_merge( array( 'kind' => $kind, 'key' => $key, 'plugin' => '', 'id' => '', 'selector' => '', 'provider' => '', 'via' => '', 'tag' => '', 'name' => $name, 'pages' => array(), ), $extra ); } }