'
. esc_html__( 'HolmesPortfolio Forms is ready.', 'holmesportfolio-forms' ) . ' '
. esc_html__( 'The example forms below are yours to open, copy, or drop on a page with their shortcode.', 'holmesportfolio-forms' )
. '
';
}
public static function enqueue_screen_scripts() {
if ( wp_script_is( 'holmesforms-admin-screens', 'enqueued' ) ) {
return;
}
wp_enqueue_script( 'holmesforms-confirm', HOLMESFORMS_URL . 'assets/js/holmesforms-confirm.js', array(), holmesforms_asset_version( 'assets/js/holmesforms-confirm.js' ), true );
wp_localize_script(
'holmesforms-confirm',
'holmesformsConfirmText',
array(
'areYouSure' => __( 'Are you sure?', 'holmesportfolio-forms' ),
'cancel' => __( 'Cancel', 'holmesportfolio-forms' ),
'yes' => __( 'Yes', 'holmesportfolio-forms' ),
)
);
wp_enqueue_script( 'holmesforms-admin-screens', HOLMESFORMS_URL . 'assets/js/holmesforms-admin-screens.js', array( 'holmesforms-confirm' ), holmesforms_asset_version( 'assets/js/holmesforms-admin-screens.js' ), true );
}
public function encryption_notice() {
if ( ! current_user_can( self::CAP ) ) {
return;
}
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-crypt.php';
global $wpdb;
$has_sealed = $this->has_sealed_rows();
$count = 0;
$problem = '';
if ( $has_sealed ) {
if ( ! HP_Forms_Crypt::is_active() ) {
$problem = __( 'The encryption key is no longer available (wp-config.php, an environment variable, or the holmesforms_encryption_key filter).', 'holmesportfolio-forms' );
} elseif ( ! HP_Forms_Crypt::is_key_correct() ) {
$problem = __( 'The current encryption key does not match the key that encrypted your entries.', 'holmesportfolio-forms' );
}
}
if ( '' !== $problem ) {
$sealed_check = get_transient( 'holmesforms_enc_check' );
if ( ! is_array( $sealed_check ) ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL -- the plugin's own table, a fixed search for sealed rows
$sealed_check = array( 'count' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i WHERE data LIKE %s', HP_Forms_Storage::table(), $wpdb->esc_like( 'hpfenc1:' ) . '%' ) ) );
set_transient( 'holmesforms_enc_check', $sealed_check, self::ENCRYPTION_CHECK_CACHE_SECONDS );
}
$count = (int) $sealed_check['count'];
if ( $count < 1 ) {
delete_transient( 'holmesforms_enc_has_sealed' );
$problem = '';
}
}
if ( $has_sealed ) {
if ( '' !== $problem ) {
echo '
'
. esc_html__( 'HolmesPortfolio Forms: stored entries cannot be read.', 'holmesportfolio-forms' )
. ' ' . esc_html( $problem ) . ' '
. esc_html(
sprintf(
/* translators: %s: number of entries */
_n(
'%s stored entry is encrypted and will stay unreadable until the correct key is restored.',
'%s stored entries are encrypted and will stay unreadable until the correct key is restored.',
$count,
'holmesportfolio-forms'
),
number_format_i18n( $count )
)
) . ' '
. esc_html__( 'Until then, don\'t export entries or use the button that encrypts older entries.', 'holmesportfolio-forms' ) . ' '
. esc_html__( 'While the key is wrong, new entries aren\'t saved. Form emails still send.', 'holmesportfolio-forms' )
. '
' . esc_html(
sprintf(
/* translators: %s: number of form submissions */
_n( 'While the encryption key was wrong, %s form submission wasn\'t saved as an entry. Its emails were sent as usual.', 'While the encryption key was wrong, %s form submissions weren\'t saved as entries. Their emails were sent as usual.', $unsaved_count, 'holmesportfolio-forms' ),
number_format_i18n( $unsaved_count )
)
) . ' ' . esc_html__( 'Dismiss', 'holmesportfolio-forms' ) . '
' . esc_html__( 'HolmesPortfolio Forms: encryption is on. Do you have a spare copy of the key?', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'The key is the HOLMESFORMS_ENCRYPTION_KEY line in wp-config.php, unless your site sets it somewhere else. This plugin never shows it.', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'Keep a spare copy somewhere other than this website, such as your password manager. If the key is lost, entries saved with it can\'t be read by anyone.', 'holmesportfolio-forms' ) . '
', HP_Forms_Kses::admin() );
}
public function second_step_notice() {
if ( ! current_user_can( self::CAP_ENTRIES ) || ! self::is_reminder_screen() || self::is_snoozed( 'twofa' ) ) {
return;
}
$user = wp_get_current_user();
$has_second_factor = apply_filters( 'holmesforms_has_second_factor', null, $user );
$has_two_factor_plugin = class_exists( 'Two_Factor_Core' ) && method_exists( 'Two_Factor_Core', 'is_user_using_two_factor' );
if ( null === $has_second_factor && $has_two_factor_plugin ) {
$has_second_factor = (bool) Two_Factor_Core::is_user_using_two_factor( $user->ID );
}
if ( true === $has_second_factor ) {
return;
}
if ( $has_two_factor_plugin ) {
$text = __( 'You haven\'t set up a second sign-in step yet. Anyone who can see entries should use one as well as their password. It can be a code from an app on your phone.', 'holmesportfolio-forms' );
$link = '' . esc_html__( 'Set it up on your profile', 'holmesportfolio-forms' ) . '';
} elseif ( current_user_can( 'install_plugins' ) ) {
$text = __( 'Staff who can see entries should use a second sign-in step as well as their password. It can be a code from an app on their phone. The official Two-Factor plugin adds it for free.', 'holmesportfolio-forms' );
$link = '' . esc_html__( 'Find the Two-Factor plugin', 'holmesportfolio-forms' ) . '';
} else {
return;
}
echo wp_kses( '
' . esc_html__( 'Copy or download this line. A new key is made each time this page opens, so use the one you saved.', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'Paste it into wp-config.php (not .htaccess), above the line that says "That\'s all, stop editing!".', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'Keep a spare copy somewhere other than this website, such as your password manager. If the key is lost, entries saved with it can\'t be read by anyone.', 'holmesportfolio-forms' ) . '
'
. ''
. '
' . esc_html( $key_line ) . '
'
. '
'
. ''
. '
';
}
private static function enqueue_key_script() {
if ( wp_script_is( 'holmesforms-key', 'enqueued' ) ) {
return;
}
wp_enqueue_style( 'holmesforms-admin', HOLMESFORMS_URL . 'assets/css/admin.css', array(), holmesforms_asset_version( 'assets/css/admin.css' ) );
wp_enqueue_script( 'holmesforms-key', HOLMESFORMS_URL . 'assets/js/holmesforms-key.js', array(), holmesforms_asset_version( 'assets/js/holmesforms-key.js' ), true );
wp_localize_script(
'holmesforms-key',
'holmesformsKey',
array(
'copied' => __( 'Copied', 'holmesportfolio-forms' ),
'failed' => __( 'Press Ctrl+C to copy', 'holmesportfolio-forms' ),
'downloaded' => __( 'Downloaded', 'holmesportfolio-forms' ),
'fileName' => 'holmesforms-encryption-key-' . sanitize_file_name( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ) . '.txt',
/* translators: 1: site address, 2: date */
'fileHead' => sprintf( __( 'HolmesPortfolio Forms encryption key for %1$s, made %2$s.', 'holmesportfolio-forms' ), home_url(), wp_date( 'j F Y' ) ),
'fileFoot' => array(
__( 'Paste the line above into wp-config.php, above the line that says "That\'s all, stop editing!".', 'holmesportfolio-forms' ),
__( 'Keep this file somewhere other than your website, such as your password manager, then delete it from your Downloads folder.', 'holmesportfolio-forms' ),
__( 'If the key is lost, entries saved with it can\'t be read by anyone.', 'holmesportfolio-forms' ),
),
)
);
}
public function register_menu() {
$parent_cap = current_user_can( self::CAP ) ? self::CAP
: ( current_user_can( self::CAP_ENTRIES ) ? self::CAP_ENTRIES : self::CAP );
$parent_cap = (string) apply_filters( 'holmesforms_menu_parent_cap', $parent_cap );
$menu_title = (string) apply_filters( 'holmesforms_menu_title', __( 'Forms', 'holmesportfolio-forms' ) );
add_menu_page(
__( 'HolmesPortfolio Forms', 'holmesportfolio-forms' ),
wp_kses_post( $menu_title ),
$parent_cap,
self::MENU_SLUG,
array( $this, 'screen_list' ),
'dashicons-feedback',
self::MENU_POSITION
);
add_submenu_page( self::MENU_SLUG, __( 'All Forms', 'holmesportfolio-forms' ), __( 'All Forms', 'holmesportfolio-forms' ), self::CAP, self::MENU_SLUG, array( $this, 'screen_list' ) );
add_submenu_page( self::MENU_SLUG, __( 'Create New form', 'holmesportfolio-forms' ), __( 'Add New', 'holmesportfolio-forms' ), self::CAP, self::BUILD_SLUG, array( $this, 'screen_builder' ) );
add_submenu_page( self::MENU_SLUG, __( 'Entries', 'holmesportfolio-forms' ), __( 'Entries', 'holmesportfolio-forms' ), self::CAP_ENTRIES, self::ENTRIES_SLUG, array( $this, 'screen_entries' ) );
$settings_cap = apply_filters( 'holmesforms_settings_menu_cap', self::CAP );
add_submenu_page( self::MENU_SLUG, __( 'Settings', 'holmesportfolio-forms' ), __( 'Settings', 'holmesportfolio-forms' ), $settings_cap, self::SETTINGS_SLUG, array( $this, 'screen_settings' ) );
add_submenu_page( self::MENU_SLUG, __( 'Pro and Casework', 'holmesportfolio-forms' ), __( 'Pro and Casework', 'holmesportfolio-forms' ), self::CAP, self::ADDONS_SLUG, array( $this, 'screen_add_ons' ) );
if ( ! current_user_can( self::CAP ) ) {
remove_submenu_page( self::MENU_SLUG, self::MENU_SLUG );
}
}
/** The one page about the paid add-on: what Pro and Casework add, and where to read more. */
public function screen_add_ons() {
if ( ! current_user_can( self::CAP ) ) {
return;
}
$pro_items = array(
array( __( 'Payments', 'holmesportfolio-forms' ), __( 'Payments on a form, through a Stripe or PayPal payment link or by bank transfer. With Stripe, the form and any uploaded files can wait until Stripe confirms the payment.', 'holmesportfolio-forms' ) ),
array( __( 'Booking calendars', 'holmesportfolio-forms' ), __( 'Booking calendars for appointments, rooms, tables and holiday stays, with a link customers can use to cancel.', 'holmesportfolio-forms' ) ),
array( __( 'Signatures and PDFs', 'holmesportfolio-forms' ), __( 'Signatures, drawn or typed, and a PDF (Portable Document Format) file of each entry\'s answers to download or attach to an email.', 'holmesportfolio-forms' ) ),
array( __( 'Save and resume', 'holmesportfolio-forms' ), __( 'Save and resume, so people can finish a long form later, and repeatable groups, so they can fill in the same set of questions more than once.', 'holmesportfolio-forms' ) ),
array( __( 'Statuses and notes', 'holmesportfolio-forms' ), __( 'A status and internal notes on each entry, messages to the person who sent it, and a staff-only extra form attached to an entry.', 'holmesportfolio-forms' ) ),
array( __( 'Staff roles', 'holmesportfolio-forms' ), __( 'Four ready-made staff roles, so people can build forms or work entries without being site administrators, and a tick list of who sees which forms.', 'holmesportfolio-forms' ) ),
array( __( 'Polls and maps', 'holmesportfolio-forms' ), __( 'Polls that count votes across every submission, and map questions where people mark where something is.', 'holmesportfolio-forms' ) ),
array( __( 'Webhooks and mailing lists', 'holmesportfolio-forms' ), __( 'Webhooks that send each entry to another system, and Mailchimp and Brevo sign-ups for people who tick your consent box.', 'holmesportfolio-forms' ) ),
array( __( 'Analytics', 'holmesportfolio-forms' ), __( 'Analytics as daily totals: where people stop, how long a form takes and which optional questions get skipped.', 'holmesportfolio-forms' ) ),
array( __( 'Activity log', 'holmesportfolio-forms' ), __( 'A fuller activity log for admins: who opened an entry, who downloaded a PDF, and changes to settings and roles.', 'holmesportfolio-forms' ) ),
);
$casework_items = array(
array( __( 'Pick up and hand over', 'holmesportfolio-forms' ), __( 'Staff pick up a case, work it and close it, release it back to the pool, or hand it to a named colleague. Every hand-over is logged on the case.', 'holmesportfolio-forms' ) ),
array( __( 'Teams', 'holmesportfolio-forms' ), __( 'Teams see only their own forms, and can pass a case to another team when you allow it.', 'holmesportfolio-forms' ) ),
array( __( 'Response times', 'holmesportfolio-forms' ), __( 'A response-time target for each form, in days or hours, counting working days or every day. Cases past their target show as overdue, and a reminder email goes out.', 'holmesportfolio-forms' ) ),
array( __( 'Complaints and subject access', 'holmesportfolio-forms' ), __( 'Complaints in two stages, each with its own target, and a clock for subject access requests, where someone asks for a copy of their personal data. The dates follow the UK General Data Protection Regulation (UK GDPR) as we understand it. Check them against the Information Commissioner\'s Office (ICO) guidance.', 'holmesportfolio-forms' ) ),
array( __( 'Case reports', 'holmesportfolio-forms' ), __( 'Case reports by month, quarter, financial year or calendar year: cases received, closed, closed on time and still open, with the median time to close. Download the figures as a spreadsheet (CSV) file.', 'holmesportfolio-forms' ) ),
array( __( 'Reopening', 'holmesportfolio-forms' ), __( 'You decide who can reopen a closed case.', 'holmesportfolio-forms' ) ),
array( __( 'Replies and sign-in', 'holmesportfolio-forms' ), __( 'The person who sent the form can reply through a one-time link, or sign in to see the status of their own cases.', 'holmesportfolio-forms' ) ),
array( __( 'Maps of reports', 'holmesportfolio-forms' ), __( 'A map of reported locations for staff, and a public map that shows only the reports you choose.', 'holmesportfolio-forms' ) ),
);
$cards = static function ( array $items ) {
$html = '
' . esc_html__( 'Pro and Casework', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'You already have the whole form builder. If your team also takes payments, books appointments or works each entry as a case, we\'re building a paid add-on plugin for that. It comes in two plans, Pro and Casework. Here\'s what each one adds.', 'holmesportfolio-forms' ) . '
'
. '
' . esc_html__( 'What Pro adds', 'holmesportfolio-forms' ) . '
' . esc_html__( 'Pro and Casework are the two plans of a separate add-on plugin from us. It isn\'t on sale yet, and what it includes may change before launch. Nothing on this site needs it. The free plugin is complete on its own, and every form you build with it keeps working whether or not you ever install the add-on.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'If a test email goes to spam or never arrives, set up a mail plugin that sends through a real mailbox on your own domain, using the Simple Mail Transfer Protocol (SMTP).', 'holmesportfolio-forms' ) . '
';
}
private static function die_unless_editable_form( $form_id ) {
if ( $form_id && HOLMESFORMS_POST_TYPE !== get_post_type( $form_id ) ) {
wp_die( esc_html__( 'That is not a form.', 'holmesportfolio-forms' ), '', array( 'response' => 403 ) );
}
if ( $form_id && ! self::is_editable_form( $form_id ) ) {
wp_die( esc_html__( 'That form isn\'t shared with you.', 'holmesportfolio-forms' ), '', array( 'response' => 403 ) );
}
}
/** @return bool A form this user may change, within any add-on scope. */
public static function is_editable_form( $form_id ) {
$form_id = (int) $form_id;
if ( ! $form_id || HOLMESFORMS_POST_TYPE !== get_post_type( $form_id ) ) {
return false;
}
$scope = self::editable_form_ids();
return null === $scope || in_array( $form_id, $scope, true );
}
/** @return int The other user who has the form open in the builder, or 0. */
private static function other_editor_id( $form_id ) {
$lock_holder = (int) wp_check_post_lock( $form_id );
return get_current_user_id() !== $lock_holder ? $lock_holder : 0;
}
private static function user_display_name( $user_id, $fallback ) {
$user = get_userdata( $user_id );
return $user ? $user->display_name : $fallback;
}
private static function render_lock_banner( $form_id, $lock_holder ) {
$lock_value = get_post_meta( $form_id, '_edit_lock', true );
$locked_since = '';
if ( $lock_value ) {
$colon_position = strpos( (string) $lock_value, ':' );
$locked_at = (int) substr( (string) $lock_value, 0, $colon_position ? $colon_position : strlen( (string) $lock_value ) );
$locked_since = wp_date( 'H:i', $locked_at );
}
echo '
'
. esc_html(
sprintf(
/* translators: 1: display name, 2: local time */
__( 'This form is being edited by %1$s (locked at %2$s).', 'holmesportfolio-forms' ),
self::user_display_name( $lock_holder, __( 'another user', 'holmesportfolio-forms' ) ),
$locked_since
)
) . ' '
. esc_html__( 'Saving is disabled until you take over. Taking over removes them from the builder and any unsaved changes they have are lost.', 'holmesportfolio-forms' )
. '
' . esc_html__( 'This form is not saved yet. Press Save to keep it.', 'holmesportfolio-forms' ) . '
';
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, a notice flag from the redirect
$notice = isset( $_GET['holmesforms_notice'] ) ? sanitize_key( wp_unslash( $_GET['holmesforms_notice'] ) ) : '';
if ( 'saved' === $notice || 'restored' === $notice ) {
$message = 'restored' === $notice
? __( 'Version restored. Your previous form was saved to the history list first.', 'holmesportfolio-forms' )
: __( 'Form saved.', 'holmesportfolio-forms' );
echo '
' . esc_html( $message ) . '
';
$renamed = $form_id ? get_transient( 'holmesforms_renamed_' . get_current_user_id() . '_' . $form_id ) : false;
if ( is_array( $renamed ) && $renamed ) {
delete_transient( 'holmesforms_renamed_' . get_current_user_id() . '_' . $form_id );
$lines = array();
foreach ( $renamed as $from => $to ) {
$lines[] = in_array( $from, HP_Forms_Form::RESERVED_KEYS, true )
/* translators: 1: the old Data Name, 2: the new Data Name */
? sprintf( __( 'The Data Name "%1$s" belongs to the built-in [%1$s] token, so it is now "%2$s".', 'holmesportfolio-forms' ), $from, $to )
/* translators: 1: the old Data Name, 2: the new Data Name */
: sprintf( __( 'Another question already uses the Data Name "%1$s", so this one is now "%2$s".', 'holmesportfolio-forms' ), $from, $to );
}
echo '
' . esc_html( implode( ' ', $lines ) ) . ' ' . esc_html__( 'Check any rule, email or message that used the old name, and update it to the new one.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'Your copy is ready. Give it a new title below. It starts with no entries.', 'holmesportfolio-forms' ) . ' '
. esc_html__( 'It has the same reference prefix as the original. To tell the two forms\' entries apart, give this one its own prefix on the Setup tab.', 'holmesportfolio-forms' ) . '
';
echo '';
echo '';
echo '';
echo '' . esc_html__( 'Optional. Put related forms in a group, for example Contact forms, Highways or Licensing. Then you can filter the Forms list by group.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'The search box only matches an exact reference or email, not part of one. That keeps it fast, however many entries you have.', 'holmesportfolio-forms' ) . '
';
}
private static function render_export_notice() {
$notice = isset( $_GET['holmesforms_notice'] ) ? sanitize_key( wp_unslash( $_GET['holmesforms_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, a notice flag from the redirect
if ( 'export_too_big' === $notice ) {
$count = isset( $_GET['n'] ) ? absint( $_GET['n'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
echo '
' . esc_html(
sprintf(
/* translators: 1: selected row count, 2: the export limit */
__( 'That selection is %1$d entries, more than the %2$d this can export at once. Narrow the filter, or pick fewer forms, and export in batches.', 'holmesportfolio-forms' ),
$count,
HP_Forms_Entries::MAX_EXPORT_ROWS
)
) . '
' . esc_html__( 'The starter blocks and forms are built into the plugin, so you can restore them at any time. A reset skips anything you have locked.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'A short guided walk through the main choices: look, privacy, email, spam and starter forms. Skipped it, or want to check your choices again? Run it here. Everything in it can also be changed on the Settings tabs.', 'holmesportfolio-forms' ) . '
';
}
private function render_lead_capture_card() {
if ( ! apply_filters( 'holmesforms_lead_capture_enabled', false ) ) {
return;
}
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-leads.php';
$result = isset( $_GET['holmesforms_lead'] ) ? sanitize_key( wp_unslash( $_GET['holmesforms_lead'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, a result flag from the redirect
$notices = array(
'sent' => array( 'success', __( 'Thanks. Check your inbox and click the link to confirm.', 'holmesportfolio-forms' ) ),
'queued' => array( 'warning', __( 'HolmesPortfolio’s server was busy. Your sign-up is saved here. We’ll keep trying to send it for about a day. The confirmation email arrives once it goes through.', 'holmesportfolio-forms' ) ),
'updated' => array( 'success', __( 'Your sign-up is updated.', 'holmesportfolio-forms' ) ),
'join_sent' => array( 'success', __( 'Check the inbox you signed up with. Open the link in our email and confirm to join the mailing list.', 'holmesportfolio-forms' ) ),
'join_wait' => array( 'warning', __( 'We’ve already emailed a link to that address today. Check your inbox, or try again tomorrow.', 'holmesportfolio-forms' ) ),
'removed' => array( 'success', __( 'Your sign-up is removed. HolmesPortfolio has deleted your details from its sign-up list.', 'holmesportfolio-forms' ) ),
'failed' => array( 'error', __( 'That didn’t work. Try again in a few minutes.', 'holmesportfolio-forms' ) ),
);
if ( isset( $notices[ $result ] ) ) {
echo '
' . esc_html( $notices[ $result ][1] ) . '
';
}
echo '
' . esc_html__( 'Sign up with HolmesPortfolio', 'holmesportfolio-forms' ) . '
';
echo '';
if ( 'signed_up' === $state ) {
$lead_state = HP_Forms_Leads::sign_up_state();
$print_manage_button = static function ( $action, $label, $button_class, $confirm_text = '' ) {
echo '';
};
if ( $lead_state['mailing'] ) {
$print_manage_button( 'leave', __( 'Leave the mailing list', 'holmesportfolio-forms' ), 'button-secondary' );
} else {
$print_manage_button( 'join', __( 'Join the mailing list', 'holmesportfolio-forms' ), 'button-secondary' );
}
$print_manage_button( 'delete', __( 'Remove my sign-up', 'holmesportfolio-forms' ), 'button-secondary', __( 'Remove your sign-up? HolmesPortfolio deletes your sign-up details and you leave the mailing list.', 'holmesportfolio-forms' ) );
}
echo '
' . esc_html__( 'Tick the forms you want to add. Ticking one that is already here resets it to the built-in version.', 'holmesportfolio-forms' ) . ' ' . esc_html__( 'Each form is an example, written in good faith. It is not legal advice. Check it and set your own before you publish.', 'holmesportfolio-forms' ) . '
';
}
echo '';
}
/* Import / Export */
public function render_tools_body() {
$notice = isset( $_GET['holmesforms_notice'] ) ? sanitize_key( wp_unslash( $_GET['holmesforms_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, a notice flag from the redirect
$count = isset( $_GET['n'] ) ? absint( $_GET['n'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
if ( 'imported' === $notice ) {
$replaced = isset( $_GET['r'] ) ? absint( $_GET['r'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
echo '
' . sprintf(
/* translators: %d: number of forms imported */
esc_html( _n( 'Imported %d form.', 'Imported %d forms.', $count, 'holmesportfolio-forms' ) ),
(int) $count
);
if ( $replaced ) {
echo ' ' . esc_html(
sprintf(
/* translators: %d: number of forms replaced */
_n( '%d matching form was replaced.', '%d matching forms were replaced.', $replaced, 'holmesportfolio-forms' ),
(int) $replaced
)
);
}
$skipped = isset( $_GET['s'] ) ? absint( $_GET['s'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
if ( $skipped ) {
echo ' ' . esc_html(
sprintf(
/* translators: %d: number of forms skipped because they are already here */
_n( '%d form was skipped because it is already here. Tick "Replace forms" to overwrite it.', '%d forms were skipped because they are already here. Tick "Replace forms" to overwrite them.', $skipped, 'holmesportfolio-forms' ),
(int) $skipped
)
);
}
if ( isset( $_GET['capped'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
echo ' ' . esc_html(
sprintf(
/* translators: %d: maximum forms per import */
__( 'The file held more than %d forms. The rest were skipped.', 'holmesportfolio-forms' ),
self::MAX_IMPORT_FORMS
)
);
}
echo '
';
$settings_status = isset( $_GET['st'] ) ? absint( $_GET['st'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read only, counts from the redirect
if ( $settings_status ) {
echo '
';
if ( 2 === $settings_status ) {
echo '
' . esc_html__( 'Some secret keys for the spam check couldn\'t be opened with this site\'s encryption key, so they\'re now blank.', 'holmesportfolio-forms' ) . ' ' . esc_html__( 'Enter them again on the Spam tab.', 'holmesportfolio-forms' ) . '
';
}
echo '
' . esc_html__( 'Encrypted entries only open with the key they were saved with. If your entries came from another site, wp-config.php here needs the same HOLMESFORMS_ENCRYPTION_KEY line as that site.', 'holmesportfolio-forms' ) . '
' . sprintf(
/* translators: %s: server upload limit, e.g. "8 MB" */
esc_html__( 'That file is larger than this server accepts for uploads (%s). Export fewer forms, or raise the upload limit.', 'holmesportfolio-forms' ),
esc_html( size_format( wp_max_upload_size() ) )
) . '
' . esc_html__( 'Back up your forms and settings from here, or bring a backup or a single form back in. To export one form, use Export on the Forms list. Files hold forms and settings only, with no submissions, entries, uploaded files or drafts. They hold email addresses you entered, so keep them private. Secret keys are locked or left out, so after a restore you may need to enter some of them again.', 'holmesportfolio-forms' ) . '
' . esc_html(
sprintf(
/* translators: 1: max forms per file, 2: server upload limit */
__( 'Upload a backup file or a form you exported, from this or another site. By default forms are added as new and existing ones are left alone. Up to %1$d forms and %2$s per file.', 'holmesportfolio-forms' ),
self::MAX_IMPORT_FORMS,
size_format( wp_max_upload_size() )
)
) . '
' . esc_html__( 'A form with this name is already here.', 'holmesportfolio-forms' ) . ' '
. esc_html(
sprintf(
/* translators: %s: form title */
__( 'Importing will overwrite “%s”. Its current version is kept in that form’s history. To keep both, rename the form in the file and import again.', 'holmesportfolio-forms' ),
$form_title
)
)
. '
' . esc_html__( 'The GOV.UK Service Manual advises against CAPTCHA tests. The honeypot, timing check and rate limit are always on and need no setup. Turnstile and reCAPTCHA add a challenge that visitors see, so choose None unless you have a specific reason.', 'holmesportfolio-forms' )
. self::guide_link_html( 'spam' ) . '
', HP_Forms_Kses::admin() );
}
public function words_intro() {
echo wp_kses( '
' . esc_html__( 'Checks what visitors type in text inputs and text areas for swear words and slurs. It matches whole words only, so place names like Scunthorpe aren’t caught. Capitals and simple disguises like sh1t or f.u.c.k are still caught. Email and web addresses are not checked. Nothing leaves your site.', 'holmesportfolio-forms' )
. self::guide_link_html( 'blocked-words' ) . '
', HP_Forms_Kses::admin() );
}
public function field_words_on() {
$words = HP_Forms_Words::settings();
$option_name = esc_attr( self::SETTINGS );
echo '';
echo '' . esc_html__( 'See the built-in list', 'holmesportfolio-forms' ) . '
' . esc_html__( 'Words that also have an everyday meaning, like cock, bitch or faggots (the dish), are left out. That way an answer using them in their everyday sense isn’t refused. Add them below if your forms need them.', 'holmesportfolio-forms' ) . '
';
}
public function field_words_own() {
printf(
'
%s
',
esc_attr( self::SETTINGS ),
esc_textarea( HP_Forms_Words::settings()['own'] ),
esc_html__( 'One word or phrase per line. Each is matched as a whole word, whatever the capitals.', 'holmesportfolio-forms' )
);
}
public function field_words_action() {
$words = HP_Forms_Words::settings();
$options = array(
'refuse' => __( 'Ask them to change it before the form can be sent (recommended)', 'holmesportfolio-forms' ),
'flag' => __( 'Accept it and highlight the words for staff', 'holmesportfolio-forms' ),
'spam' => __( 'Accept it but save it as spam, with no emails sent', 'holmesportfolio-forms' ),
);
echo '
' . esc_html__( 'Whichever you choose, staff see the words highlighted when they open the entry.', 'holmesportfolio-forms' ) . '
';
}
public function field_words_message() {
printf(
'
%s %s
',
esc_attr( self::SETTINGS ),
esc_attr( HP_Forms_Words::settings()['message'] ),
/* translators: %s is typed as it is; it is not replaced here. */
esc_html__( 'Shown at the question when an answer is refused. Put %s where you want the words found to appear. Leave the box blank to use this wording:', 'holmesportfolio-forms' ),
esc_html( HP_Forms_Words::default_message() )
);
}
public function field_words_forms() {
$words = HP_Forms_Words::settings();
$forms = get_posts(
array(
'post_type' => HOLMESFORMS_POST_TYPE,
'post_status' => array( 'publish', 'draft' ),
'numberposts' => -1,
'orderby' => 'title',
'order' => 'ASC',
)
);
$options = array(
'' => __( 'Same as above', 'holmesportfolio-forms' ),
'refuse' => __( 'Ask them to change it', 'holmesportfolio-forms' ),
'flag' => __( 'Accept and highlight for staff', 'holmesportfolio-forms' ),
'spam' => __( 'Save as spam', 'holmesportfolio-forms' ),
'off' => __( 'Do not check', 'holmesportfolio-forms' ),
);
if ( ! $forms ) {
echo '
' . esc_html__( 'No forms yet.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'For example, a complaints form could accept and highlight, so nobody quoting what was said to them is turned away.', 'holmesportfolio-forms' ) . '
';
}
public function signin_intro() {
echo wp_kses( '
' . esc_html__( 'The strongest protection for the admin side is set up outside this plugin. Lock wp-admin and the login page to your own network in the .htaccess file, and ask staff for a second step when they sign in, for example with the free Two-Factor plugin.', 'holmesportfolio-forms' )
. self::guide_link_html( 'protect' ) . '
', HP_Forms_Kses::admin() );
}
public function guide_intro() {
$url = self::guide_url();
printf(
'
',
esc_html__( 'The full user guide covers how to build a form, every field and setting, show and hide rules, and the starter forms, including the request forms. Only signed-in staff can open it. Find it at', 'holmesportfolio-forms' ),
esc_url( $url ),
esc_html( preg_replace( '#^https?://#', '', $url ) ) . ' ' . esc_html__( '(opens in new tab)', 'holmesportfolio-forms' )
);
}
public function support_intro() {
printf(
'
' . esc_html__( 'Deadlines counted in working days, such as 20 working days, skip these bank holidays. So do response times, where they’re used. Pick one part of the UK, all of them, or none. A form can use a different choice on its builder Setup tab. Put [working-days-note] in an email or message to show how a deadline was counted.', 'holmesportfolio-forms' ) . '
';
}
public function field_bank_holiday_updates() {
$last = HP_Forms_Holidays::last_date();
printf(
'',
esc_attr( self::SETTINGS ),
checked( HP_Forms_Holidays::is_updating(), true, false ),
esc_html__( 'Fetch the latest bank holidays from GOV.UK once a week', 'holmesportfolio-forms' )
);
/* translators: %s: date of the last bank holiday on the list */
echo '
' . esc_html( sprintf( __( 'The plugin comes with GOV.UK\'s list, which runs to %s. Each plugin update brings a newer copy. Turn this on to fetch updates from gov.uk every week as well. Only the public list is fetched, and no personal data is sent.', 'holmesportfolio-forms' ), $last ? wp_date( get_option( 'date_format' ), strtotime( $last . ' 12:00:00' ) ) : __( 'no date', 'holmesportfolio-forms' ) ) ) . '
' . esc_html__( 'The bank holiday list runs out within six months. Turn this on, or update the plugin, so working-day dates stay right.', 'holmesportfolio-forms' ) . '
';
}
}
public function field_working_days() {
$week = HP_Forms_Holidays::working_days();
echo '
' . esc_html__( 'Monday to Friday by default. Tick Saturday and Sunday for a seven-day operation. At least one day is always kept.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'The look every form on the site uses. The GOV.UK Design System is made by the Government Digital Service, and the UK government uses it for its own online services. It is the recommended choice. GOV.UK says: "Using the GOV.UK Design System in a service does not immediately make that service accessible." Test each published form. Making your forms accessible is your responsibility. GOV.UK and Bootstrap load that framework\'s own stylesheet. Plain and None use the plugin\'s own class names.', 'holmesportfolio-forms' )
. self::guide_link_html( 'builder' ) . '
' . esc_html__( 'The colour of buttons, links and highlights on forms that use the Plain style. Leave it empty for the default blue. It needs a contrast of at least 4.5 to 1 against white. A colour that is too light is not saved. Forms in the GOV.UK or Bootstrap style keep their own colours.', 'holmesportfolio-forms' )
. self::guide_link_html( 'settings-accent' ) . '
', HP_Forms_Kses::admin() );
}
public function field_nojs() {
$value = 'allow' === $this->get_setting( 'nojs', 'block' ) ? 'allow' : 'block';
$options = array(
'block' => __( 'Hide the form and ask them to turn JavaScript on (default)', 'holmesportfolio-forms' ),
'allow' => __( 'Let them fill it in without JavaScript', 'holmesportfolio-forms' ),
);
echo '';
echo wp_kses( '
' . esc_html__( 'GOV.UK\'s guidance for government services recommends that forms work without JavaScript. The plugin hides forms by default on purpose, because letting people in can cause more problems than it solves. Without JavaScript, rules that show or hide questions stop working, and so do several other features. If you let them in, every page shows at once and the form is sent in one go. A list that changes with an earlier answer shows them every choice. If someone picks a choice that doesn\'t match their earlier answer, they see an error asking them to pick another.', 'holmesportfolio-forms' )
. self::guide_link_html( 'no-js' ) . '
' . esc_html__( 'None of your forms use anything that stops working without JavaScript.', 'holmesportfolio-forms' ) . '
';
return;
}
echo wp_kses( '
' . esc_html__( 'What stops working for visitors without JavaScript', 'holmesportfolio-forms' ) . '
' . $items_html . '
', HP_Forms_Kses::admin() );
echo '
' . esc_html__( 'To stop these problems, choose "Hide the form and ask them to turn JavaScript on" above. Visitors without JavaScript then see a message asking them to turn it on.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'This warning shows once for each kind of problem on this form.', 'holmesportfolio-forms' ) . '
';
}
public function field_style_classes() {
echo '
' . esc_html__( 'Want the forms to look different from what the style mode and accent colour give you? Add CSS under Appearance, Customize, Additional CSS. Start each rule with .hpf-form so it only touches the forms. On a block theme, Additional CSS is in the Site Editor under Styles. The class names below show you what to target.', 'holmesportfolio-forms' ) . '
';
$mode_classes = array(
array( __( 'Question text', 'holmesportfolio-forms' ), '.govuk-label', '.form-label', '.hpf-label' ),
array( __( 'Help line under it', 'holmesportfolio-forms' ), '.govuk-hint', '.form-text', '.hpf-hint' ),
array( __( 'Red error message', 'holmesportfolio-forms' ), '.govuk-error-message', '.invalid-feedback', '.hpf-error' ),
array( __( 'A whole question', 'holmesportfolio-forms' ), '.govuk-form-group', '.hpf-group', '.hpf-group' ),
array( __( 'A question with an error', 'holmesportfolio-forms' ), '.govuk-form-group--error', '.hpf-group--error', '.hpf-group--error' ),
array( __( 'Text box', 'holmesportfolio-forms' ), '.govuk-input', '.form-control', '.hpf-input' ),
array( __( 'Text area', 'holmesportfolio-forms' ), '.govuk-textarea', 'textarea.form-control', '.hpf-textarea' ),
array( __( 'Dropdown', 'holmesportfolio-forms' ), '.govuk-select', '.form-select', '.hpf-select' ),
array( __( 'Radio button', 'holmesportfolio-forms' ), '.govuk-radios__input', '.form-check-input', '.hpf-choice__input' ),
array( __( 'Tick box', 'holmesportfolio-forms' ), '.govuk-checkboxes__input', '.form-check-input', '.hpf-choice__input' ),
array( __( 'Words next to a radio button or tick box', 'holmesportfolio-forms' ), '.govuk-radios__label, .govuk-checkboxes__label', '.form-check-label', '.hpf-choice__label' ),
array( __( 'Date: the row of boxes', 'holmesportfolio-forms' ), '.govuk-date-input', '.hpf-date', '.hpf-date' ),
array( __( 'Date: the day, month and year boxes', 'holmesportfolio-forms' ), '.govuk-date-input__input', '.hpf-date .form-control', '.hpf-date__input' ),
array( __( 'Buttons', 'holmesportfolio-forms' ), '.govuk-button', '.btn', '.hpf-button' ),
array( __( 'Secondary buttons, such as Back', 'holmesportfolio-forms' ), '.govuk-button--secondary', '.btn-secondary', '.hpf-button--secondary' ),
array( __( 'Error summary (the list of errors at the top)', 'holmesportfolio-forms' ), '.govuk-error-summary', '.alert-danger', '.hpf-error-summary' ),
array( __( 'Alert: Notification banner', 'holmesportfolio-forms' ), '.govuk-notification-banner', '.alert-primary', '.hpf-alert--notification' ),
array( __( 'Alert: Inset text', 'holmesportfolio-forms' ), '.govuk-inset-text', '.alert-secondary', '.hpf-alert--inset' ),
array( __( 'Alert: Warning text', 'holmesportfolio-forms' ), '.govuk-warning-text', '.alert-warning', '.hpf-alert--warning' ),
array( __( 'Alert: Error message', 'holmesportfolio-forms' ), '.govuk-error-message', '.alert-danger', '.hpf-alert--error' ),
array( __( 'Thank-you message', 'holmesportfolio-forms' ), '.govuk-panel--confirmation', '.alert-success', '.hpf-panel--confirmation' ),
);
$shared_classes = array(
array( __( 'The whole form', 'holmesportfolio-forms' ), '.hpf-form' ),
array( __( 'One page', 'holmesportfolio-forms' ), '.hpf-page' ),
array( __( 'Page title', 'holmesportfolio-forms' ), '.hpf-page__title' ),
array( __( 'Group', 'holmesportfolio-forms' ), '.hpf-container' ),
array( __( 'Group with a heading (the box)', 'holmesportfolio-forms' ), '.hpf-container--titled' ),
array( __( 'Group heading', 'holmesportfolio-forms' ), '.hpf-container__heading' ),
array( __( 'Group people can fill in more than once', 'holmesportfolio-forms' ), '.hpf-container--repeat' ),
array( __( 'Continue and Back', 'holmesportfolio-forms' ), '.hpf-next, .hpf-back' ),
array( __( 'Heading and list inside the error summary', 'holmesportfolio-forms' ), '.hpf-error-summary__title, .hpf-error-summary__list' ),
array( __( 'Text inside an alert', 'holmesportfolio-forms' ), '.hpf-alert__body' ),
array( __( 'Word or character count', 'holmesportfolio-forms' ), '.hpf-wordcount' ),
array( __( 'Count once it goes over the limit', 'holmesportfolio-forms' ), '.hpf-wordcount.is-over' ),
array( __( 'File attachments', 'holmesportfolio-forms' ), '.hpf-attach' ),
array( __( 'Answers summary', 'holmesportfolio-forms' ), '.hpf-cya' ),
array( __( 'Answers summary: page heading', 'holmesportfolio-forms' ), '.hpf-cya__page' ),
array( __( 'Answers summary: group heading', 'holmesportfolio-forms' ), '.hpf-cya__section' ),
array( __( 'Answers summary: line after a group', 'holmesportfolio-forms' ), '.hpf-cya__group-end' ),
array( __( 'Answers summary: the list of answers', 'holmesportfolio-forms' ), '.hpf-cya__list' ),
array( __( 'Answers summary: one answer', 'holmesportfolio-forms' ), '.hpf-cya__row' ),
array( __( 'Answers summary: Go to buttons', 'holmesportfolio-forms' ), '.hpf-cya__goto' ),
array( __( 'Text inside the thank-you message', 'holmesportfolio-forms' ), '.hpf-panel__body' ),
);
echo '' . esc_html__( 'Class names you can style', 'holmesportfolio-forms' ) . '';
echo '
' . esc_html__( 'These names change with the style mode above:', 'holmesportfolio-forms' ) . '
' . esc_html__( 'In GOV.UK mode, the white background on radio buttons and tick boxes comes from that same pair of class names. Target them if your theme needs a different colour.', 'holmesportfolio-forms' ) . '
';
echo '
' . esc_html__( 'The thank-you message has a different class name in each style mode. To restyle it everywhere, list all three, as this example does. It gives the message a light grey background, a dark border, dark text and blue links:', 'holmesportfolio-forms' ) . '
' . esc_html__( 'In GOV.UK, Plain and None modes, the message sets its white text with both color and -webkit-text-fill-color. Set both in your own CSS too, or the text can stay white.', 'holmesportfolio-forms' ) . '
';
echo '
' . esc_html__( 'Additional CSS belongs to your theme. If you switch theme, add your rules again under the new one.', 'holmesportfolio-forms' ) . '
';
echo '
' . esc_html__( 'You rarely need !important. A rule that starts with .hpf-form is more specific than most of the plugin\'s own styles, so it usually wins. Add !important only when the rule you want to beat uses it too. Many themes set text and link colours with !important, so the thank-you example uses it as well.', 'holmesportfolio-forms' ) . '
';
echo '
' . esc_html__( 'Some rules already use !important, so a normal rule can\'t change them. In GOV.UK mode, that includes the helper classes whose names start govuk-!-, such as spacing, text size and width. In every mode, it includes the text colour of links styled as buttons. To change one of these, add !important to your rule too.', 'holmesportfolio-forms' ) . '
';
echo '
' . esc_html__( 'Don\'t override anything that\'s hidden on purpose. Screen-reader text (.govuk-visually-hidden, .visually-hidden and .hpf-visually-hidden) must stay hidden on screen and readable by screen readers. The spam trap (.hpf-hp) must stay hidden too.', 'holmesportfolio-forms' ) . '
';
echo wp_kses( '
' . esc_html__( 'Some field types have extra class names of their own.', 'holmesportfolio-forms' ) . self::guide_link_html( 'css-hooks' ) . '
', HP_Forms_Kses::admin() );
echo '';
}
/** @param string $anchor Guide anchor id, without the '#'. */
/** @param string $anchor A section id in the guide, or a full address when an add-on's own guide holds the section. */
public static function guide_link_html( $anchor ) {
$url = 0 === strpos( (string) $anchor, 'http' ) ? (string) $anchor : self::guide_url() . '#' . $anchor;
return sprintf(
' %s%s',
esc_url( $url ),
esc_html__( 'Read more in the guide', 'holmesportfolio-forms' ),
self::new_tab_hint()
);
}
public static function guide_url() {
return class_exists( 'HP_Forms_Guide' ) ? HP_Forms_Guide::guide_file_url() : home_url( '/holmesforms-guide/' );
}
/** @return array */
public static function help_strings() {
return array(
'more' => __( 'Read more in the guide', 'holmesportfolio-forms' ),
/* translators: %s: the setting or field name */
'named' => __( '%s help', 'holmesportfolio-forms' ),
'plain' => __( 'What is this?', 'holmesportfolio-forms' ),
);
}
/** @return array */
public static function settings_help_map() {
return (array) apply_filters(
'holmesforms_settings_help_map',
array(
'Default style mode' => array( 'settings-style', __( "The look every form on your site uses. Pick one of:\n- GOV.UK Design System, which the UK government uses for its own online services. It's the default and the recommended choice.\n- Bootstrap\n- Plain\n- None (your theme's own styles)\nThere's no per-form override.", 'holmesportfolio-forms' ) ),
'Accent colour (Plain style)' => array( 'settings-accent', __( "The colour of buttons, links and highlights on Plain-style forms.\n- Leave it empty for the default blue.\n- It needs a contrast of 4.5 to 1 against white. A colour that is too light is not saved.", 'holmesportfolio-forms' ) ),
'Styling with your own CSS' => array( 'settings-css', __( 'There\'s no CSS box in the plugin. Add CSS under Appearance, Customize, Additional CSS, and start each rule with .hpf-form so it only touches the forms. The class names are listed below.', 'holmesportfolio-forms' ) ),
'Store submissions' => array( 'settings-store', __( "Choose which forms keep their entries:\n- every form\n- only the forms you pick\n- none\nNotification emails still send whichever you choose. This only decides whether a copy stays in the database.\nEntries caught by the hidden spam trap go to Spam, so staff can rescue a real person.", 'holmesportfolio-forms' ) ),
'Which forms to store' => array( 'settings-store', __( 'Shows when storage is set to "Choose per form". Tick the forms whose entries you want to keep.', 'holmesportfolio-forms' ) ),
'Storage used' => array( 'settings-storage-usage', __( 'A read-only breakdown of what the plugin holds in the database. It shows a total and what the next clean-up will remove.', 'holmesportfolio-forms' ) ),
'Delete entries after (days)' => array( 'settings-retention', __( "How long entries are kept.\n- Older entries are deleted once a day.\n- The default is 365 days. You can pick anything from 30 to 730.\n- There's no \"keep forever\" option.", 'holmesportfolio-forms' ) ),
'Per-form retention' => array( 'settings-retention', __( 'Sets a different retention period for one form. It uses the same 30 to 730 day range.', 'holmesportfolio-forms' ) ),
'Record the submitter IP' => array( 'settings-ip', __( 'Off by default. An IP address is personal data, so only turn this on if your privacy notice covers it.', 'holmesportfolio-forms' ) ),
'Encryption at rest' => array( 'encryption', __( "Read-only status. Stored answers are encrypted once you set a key in one of these places:\n- wp-config.php\n- a filter\n- an environment variable", 'holmesportfolio-forms' ) ),
'Reference prefix' => array( 'settings-ref', __( "The letters before the number in a submission reference. The default is ENQ.\n- It applies to new submissions only.\n- A form can set its own prefix on its Setup tab.", 'holmesportfolio-forms' ) ),
'When emails are sent' => array( 'settings-send', __( "Emails go out straight after the visitor sees the thank-you message, so they don't wait.
- Your server sends them, even if the visitor closes the page.
- A failed email is tried 3 times, then admins see a notice.
- If your host blocks this, the plugin sends before the thank-you message instead.", 'holmesportfolio-forms' ) ),
'Sending limit' => array( 'settings-mail-cap', __( "The most form emails this site sends in an hour and in a day.
- Over the limit, form emails wait in the send queue and go out once there's room. Waiting doesn't use up a try.
- An email still waiting near the end of the keep time is sent anyway, so none are lost.
- Blank means no limit.
- Ask your host what its limit is.", 'holmesportfolio-forms' ) ),
'Default from address' => array( 'settings-email', __( "The \"from\" address on every email the plugin sends, so a private inbox isn't shown as the sender.\n- Use a no-reply address on your own domain.\n- An email action can use a different one.\n- Replies go to the form's Group inbox.", 'holmesportfolio-forms' ) ),
'Email look' => array( 'email-look', __( "Colours, font, text size and width for every email the plugin sends.\n- Blank means the default.\n- Text, headings and links need a contrast of at least 4.5 to 1 against the colour behind them. A colour that fails isn't saved, and the old one stays.\n- Pictures, wording and other HTML go in the header, footer and each form's email actions.\n- A developer can add more CSS with the holmesforms_email_css filter. It's added after these settings, so it can override them.", 'holmesportfolio-forms' ) ),
'Header on every email' => array( 'settings-email', __( 'HTML at the top of every notification email, such as a logo or a standard greeting.', 'holmesportfolio-forms' ) ),
'Footer on every email' => array( 'settings-email', __( 'HTML at the bottom of every notification email, such as contact details or a legal line.', 'holmesportfolio-forms' ) ),
'Additional challenge' => array( 'spam', __( "Choose None, Cloudflare Turnstile or Google reCAPTCHA v2. The GOV.UK Service Manual advises against a CAPTCHA test.\n- Both read information from your visitors' browsers and can set cookies. reCAPTCHA sets one called _GRECAPTCHA. The plugin sets no cookies of its own.", 'holmesportfolio-forms' ) ),
'Challenge keys' => array( 'spam', __( "The site key and secret key for the challenge you picked.\n- Secret keys are masked.\n- Each provider keeps its own keys when you switch.\n- To remove a provider's keys, pick it, empty its site key box and save.",'holmesportfolio-forms' ) ),
'Visitor address' => array( 'settings-visitor-ip', __( 'How the site finds a visitor\'s address for the spam limits. Only choose Cloudflare or a proxy if one really sits in front of your site.', 'holmesportfolio-forms' ) ),
'Unusual activity alert' => array( 'settings-flood', __( "How many entries one form can get in an hour before the site admin is warned.\n- The warning is one email a day for that form.\n- Entries are still accepted.\n- 0 turns the alert off.", 'holmesportfolio-forms' ) ),
'Styling' => array( 'settings-style', __( 'How every form looks: the GOV.UK Design System (recommended), Bootstrap, plain or your theme’s own styles, plus extra CSS.', 'holmesportfolio-forms' ) ),
'Email appearance' => array( 'settings-email', __( 'The sender address, the look, and the header and footer wrapped around every email the plugin sends.', 'holmesportfolio-forms' ) ),
'Data & privacy' => array( 'settings-store', __( 'Which entries are kept, for how long, and what else is recorded about each one.', 'holmesportfolio-forms' ) ),
'Spam protection' => array( 'spam', __( 'The checks that keep out spam: the hidden trap, the time check, rate limits and an optional challenge.', 'holmesportfolio-forms' ) ),
'Blocked words' => array( 'blocked-words', __( "Checks text input and text area answers for swear words and slurs.\n- Whole words only, so place names like Scunthorpe are safe.\n- Simple disguises like sh1t are still caught.\n- Email and web addresses are skipped.", 'holmesportfolio-forms' ) ),
'Check answers' => array( 'blocked-words', __( "Turn the check on or off, and choose which lists to use.\n- The built-in list covers strong swear words and slurs.\n- WordPress's Disallowed Comment Keys list is under Settings, Discussion.", 'holmesportfolio-forms' ) ),
'Your own blocked words' => array( 'blocked-words', __( 'Extra words or phrases to block, one per line. Each is matched as a whole word, so "ass" does not catch "class".', 'holmesportfolio-forms' ) ),
'When an answer has a blocked word' => array( 'blocked-words', __( "What happens when an answer has a blocked word.\n- Ask them to change it (recommended): the form shows an error at that question and can't be sent until it's reworded.\n- Accept and highlight: the entry arrives as normal with the words marked for staff.\n- Save as spam: it goes to the Spam list and no emails are sent.", 'holmesportfolio-forms' ) ),
'Message to the visitor' => array( 'blocked-words', /* translators: %s is typed as it is; it is not replaced here. */ __( 'The error shown at the question when an answer is refused. %s is replaced with the words found.', 'holmesportfolio-forms' ) ),
'Different for a form' => array( 'blocked-words', __( 'Give one form its own rule, or turn the check off for it. Useful on a complaints form, where people may need to quote what was said to them.', 'holmesportfolio-forms' ) ),
'Visitors without JavaScript' => array( 'no-js', __( 'What people see when their browser has JavaScript turned off. By default the form is hidden and they\'re asked to turn JavaScript on. GOV.UK\'s guidance for government services recommends letting them fill it in. If you do, several features stop working.', 'holmesportfolio-forms' ) ),
'Working days' => array( 'settings-bank-holidays', __( 'The days that count when a deadline is set in working days, like 20 working days. Monday to Friday by default.', 'holmesportfolio-forms' ) ),
'Bank holidays to leave out' => array( 'settings-bank-holidays', __( 'Bank holidays skipped when counting working days. Pick one part of the UK, all of them, or none. The list comes from gov.uk. A form can use a different choice on its builder Setup tab.', 'holmesportfolio-forms' ) ),
'Keep the list up to date' => array( 'settings-bank-holidays', __( "The plugin ships with GOV.UK's bank holiday list, and each update brings a newer copy.\n- Turn this on to also fetch the list from gov.uk once a week.\n- Only the public list is fetched. No personal data is sent.", 'holmesportfolio-forms' ) ),
'Restore starter content' => array( 'settings-starter', __( 'Adds back any missing starter block, and the starter forms you tick. Tick a form that is already here to reset it to the built-in version. Anything you have locked is skipped.', 'holmesportfolio-forms' ) ),
'A basic example' => array( 'settings-email', __( 'Fills the header and footer boxes above with your site name at the top and a short line at the bottom. Nothing is saved until you press Save changes.', 'holmesportfolio-forms' ) ),
'Test your emails' => array( 'emails', __( 'Sends a test email so you can check it arrives and doesn\'t land in spam. If it never arrives, set up a mail plugin that sends through a mailbox on your own domain, using the Simple Mail Transfer Protocol (SMTP).', 'holmesportfolio-forms' ) ),
'Address lookup' => array( 'set-address-lookup', __( 'Adds a Find address box to address fields. The visitor types a UK postcode and the town and county are filled in. The postcode goes to api.postcodes.io only when they press the button.', 'holmesportfolio-forms' ) ),
'Show on address fields' => array( 'set-address-lookup', __( 'Adds the Find address box to every address field on your forms.', 'holmesportfolio-forms' ) ),
'Daily limit' => array( 'set-address-lookup', __( 'The most lookups a day. 0 means no limit. Over the limit, visitors type the address themselves.', 'holmesportfolio-forms' ) ),
'Protect the sign-in' => array( 'protect', __( 'How to protect the admin side outside this plugin: lock wp-admin to your own network in .htaccess, and ask staff for a second step when they sign in.', 'holmesportfolio-forms' ) ),
'Automatic backups' => array( 'auto-backup', __( 'Saves your forms, settings and saved blocks once a week and keeps the last 8. A backup never holds submissions or uploads, but it can hold email addresses and keys, so keep downloads private.', 'holmesportfolio-forms' ) ),
'Import' => array( 'import-export', __( 'Adds forms, and if you tick them, settings and saved blocks, from a backup or an exported form. Forms already here are left alone unless you tick Replace.', 'holmesportfolio-forms' ) ),
)
);
}
public function email_intro() {
echo wp_kses( '
' . esc_html__( 'These settings wrap every HTML email this plugin sends: the look, then a header and footer. Email apps vary, so for anything in the header and footer that must hold everywhere, put the style on the HTML itself. Plain-text emails get the header and footer as plain text.', 'holmesportfolio-forms' )
. self::guide_link_html( 'emails' ) . '
' . esc_html__( 'Emails, webhooks and mailing list updates are sent by your server, so closing the page does not stop them. If your host blocks this, the plugin notices and sends before the thank-you message instead. Choose the second option only if your email plugin needs it, or while you look into an email problem.', 'holmesportfolio-forms' ) . '
';
}
public function field_mail_cap() {
$boxes = array(
'mail_cap_hour' => __( 'Most emails an hour', 'holmesportfolio-forms' ),
'mail_cap_day' => __( 'Most emails a day', 'holmesportfolio-forms' ),
);
echo '
' . esc_html__( 'Many hosts limit how many emails a site can send, for example 50 an hour or 100 a day. Ask your host for yours and enter it here. Leave a box blank for no limit. When a limit is reached, form emails wait in the send queue without using up any of their 3 tries. They go out once the hour or day has room again. Only this plugin\'s form emails are counted here. Other plugins\' emails still count towards your host\'s limit, so leave some room. For reliable sending, use an SMTP plugin that sends through a real email service.', 'holmesportfolio-forms' ) . '
',
esc_attr( self::SETTINGS ),
esc_attr( $from_email ),
/* translators: %s: an example email address */
esc_html( sprintf( __( 'For example, %s', 'holmesportfolio-forms' ), 'noreply@' . wp_parse_url( home_url(), PHP_URL_HOST ) ) ),
esc_html(
sprintf(
/* translators: %s: the WordPress admin email address */
__( 'The "from" address on every email this plugin sends, so a private inbox isn\'t shown as the sender. Use an address on your own domain that your site is allowed to send as. Your domain\'s Sender Policy Framework (SPF) and DomainKeys Identified Mail (DKIM) records decide this. Leave it blank to use the WordPress admin address (%s). An email action can set its own. If this is a no-reply address, set a Group inbox on each form so replies still reach you.', 'holmesportfolio-forms' ),
$admin_email
)
)
);
}
public function field_email_look() {
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-mailer.php';
$defaults = HP_Forms_Mailer::LOOK_DEFAULTS;
$labels = self::email_look_labels();
$fonts = array(
'arial' => 'Arial',
'verdana' => 'Verdana',
'trebuchet' => 'Trebuchet MS',
'georgia' => 'Georgia',
'times' => 'Times New Roman',
'system' => __( 'The reader\'s own system font', 'holmesportfolio-forms' ),
);
$hints = array(
'email_page_colour' => __( 'Behind the panel.', 'holmesportfolio-forms' ),
'email_panel_colour' => __( 'Behind the message.', 'holmesportfolio-forms' ),
'email_footer_colour' => __( 'Blank uses the panel colour with a thin line above.', 'holmesportfolio-forms' ),
'email_footer_text' => __( 'Blank uses the text colour.', 'holmesportfolio-forms' ),
);
$preview = array(
'page' => $defaults['email_page_colour'],
'panel' => $defaults['email_panel_colour'],
'text' => $defaults['email_text_colour'],
'heading' => $defaults['email_heading_colour'],
'link' => $defaults['email_link_colour'],
'font' => HP_Forms_Mailer::LOOK_FONTS[ $defaults['email_font'] ],
);
echo wp_kses( '
' . esc_html__( 'Colours, font and size for every email this plugin sends. Blank means the default. Text, headings and links need a contrast of at least 4.5 to 1 against the colour behind them. A colour that fails isn\'t saved, and the old one stays. Pictures, wording and other HTML go in the header, footer and each form\'s email actions.', 'holmesportfolio-forms' )
. self::guide_link_html( 'email-look' ) . '
' . esc_html__( 'Name', 'holmesportfolio-forms' ) . ' Sam Example
'
. '
' . esc_html__( 'View on the website', 'holmesportfolio-forms' ) . '
'
. '
'
. ''
. '
';
echo '
' . esc_html__( 'The preview is a rough guide. Use Send test email at the bottom of this tab to see the real thing in your inbox.', 'holmesportfolio-forms' ) . '
';
echo '
';
}
public function field_email_header() {
printf(
'
%s
',
esc_attr( self::SETTINGS ),
esc_textarea( $this->get_setting( 'email_header' ) ),
esc_html__( 'HTML added above the message body, such as a logo. Use a full https:// image URL.', 'holmesportfolio-forms' )
);
}
public function field_email_example() {
$site = trim( wp_specialchars_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
$header = '
' . esc_html__( 'Sent from the forms on our website.', 'holmesportfolio-forms' ) . '
';
printf(
'
%s
',
esc_attr( $header ),
esc_attr( $footer ),
esc_attr__( 'Replace what is there?', 'holmesportfolio-forms' ),
esc_attr__( 'This replaces what is in the header and footer boxes. Nothing is saved until you press Save changes.', 'holmesportfolio-forms' ),
esc_attr__( 'Replace', 'holmesportfolio-forms' ),
esc_html__( 'Fill in the basic example', 'holmesportfolio-forms' ),
esc_html__( 'Fills the header and footer boxes above: your site name at the top and a short line at the bottom. Change the words to yours, then press Save changes. Nothing is saved until you do.', 'holmesportfolio-forms' )
);
}
public function field_email_footer() {
printf(
'
%s
',
esc_attr( self::SETTINGS ),
esc_textarea( $this->get_setting( 'email_footer' ) ),
esc_html__( 'HTML added below the message body, such as an address or an unsubscribe note.', 'holmesportfolio-forms' )
);
}
public function field_store_mode() {
$value = $this->get_setting( 'store_mode', 'all' );
$options = array(
'all' => __( 'Save every form’s submissions (default)', 'holmesportfolio-forms' ),
'per_form' => __( 'Choose per form: see the list below', 'holmesportfolio-forms' ),
'never' => __( 'Never save any submissions', 'holmesportfolio-forms' ),
);
self::render_setting_select( 'hpf-store-mode', 'store_mode', $options, $value );
echo '
' . esc_html__( 'Emails still send in every mode. This only controls whether a copy of the answers is kept in the database. With “Never save” the retention setting below does nothing.', 'holmesportfolio-forms' ) . '
';
}
public function field_storage_usage() {
global $wpdb;
$rows = get_transient( 'holmesforms_storage_usage' );
if ( ! is_array( $rows ) ) {
$rows = array();
$submissions_table = HP_Forms_Storage::table();
$entry_count = (int) HP_Forms_Storage::count();
$table_bytes = (int) $wpdb->get_var( $wpdb->prepare( 'SELECT data_length + index_length FROM information_schema.TABLES WHERE table_schema = %s AND table_name = %s', DB_NAME, $submissions_table ) ); // phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables
$rows[] = array(
/* translators: %s: number of entries */
'label' => sprintf( _n( 'Stored entries (%s row)', 'Stored entries (%s rows)', $entry_count, 'holmesportfolio-forms' ), number_format_i18n( $entry_count ) ),
'bytes' => $table_bytes,
);
// phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables, cached below
$meta_bytes = (int) $wpdb->get_var(
"SELECT SUM(LENGTH(pm.meta_value)) FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_type = '" . esc_sql( HOLMESFORMS_POST_TYPE ) . "' AND pm.meta_key LIKE '\_holmesforms\_%'"
); // phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables
// phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables, cached below
$revision_bytes = (int) $wpdb->get_var(
"SELECT SUM(LENGTH(pm.meta_value)) FROM {$wpdb->postmeta} pm
INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
WHERE p.post_type = '" . esc_sql( HOLMESFORMS_POST_TYPE ) . "' AND pm.meta_key = '" . esc_sql( self::REVISION_META ) . "'"
); // phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables
$rows[] = array(
'label' => __( 'Form definitions', 'holmesportfolio-forms' ),
'bytes' => max( 0, $meta_bytes - $revision_bytes ),
);
$rows[] = array(
'label' => __( 'Version history (last 30 saves per form)', 'holmesportfolio-forms' ),
'bytes' => $revision_bytes,
);
/** @param array $add_on_tables An add-on's tables: suffix => { label, count (show the row count), drafts (rows are saved drafts) }. */
$add_on_tables = (array) apply_filters( 'holmesforms_storage_size_tables', array() );
foreach ( $add_on_tables as $suffix => $table_info ) {
$suffix = sanitize_key( $suffix );
$table_info = wp_parse_args(
(array) $table_info,
array(
'label' => $suffix,
'count' => false,
'drafts' => false,
)
);
$table = $wpdb->prefix . $suffix;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- checks the table exists; the figures are cached below
if ( $wpdb->get_var( $wpdb->prepare( 'SHOW TABLES LIKE %s', $table ) ) !== $table ) {
continue;
}
$row_count = $table_info['count'] ? (int) $wpdb->get_var( $wpdb->prepare( 'SELECT COUNT(*) FROM %i', $table ) ) : 0; // phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables
$label = $table_info['count']
/* translators: 1: table description, 2: row count */
? sprintf( _n( '%1$s (%2$s row)', '%1$s (%2$s rows)', $row_count, 'holmesportfolio-forms' ), $table_info['label'], number_format_i18n( $row_count ) )
: $table_info['label'];
$rows[] = array(
'label' => $label,
'bytes' => (int) $wpdb->get_var( $wpdb->prepare( 'SELECT data_length + index_length FROM information_schema.TABLES WHERE table_schema = %s AND table_name = %s', DB_NAME, $table ) ), // phpcs:ignore WordPress.DB -- storage figures from the plugin's own tables
'drafts' => ! empty( $table_info['drafts'] ) ? $row_count : null,
);
}
set_transient( 'holmesforms_storage_usage', $rows, self::STORAGE_USAGE_CACHE_SECONDS );
}
$total = 0;
$drafts = 0;
foreach ( $rows as $row ) {
$total += (int) $row['bytes'];
if ( isset( $row['drafts'] ) ) {
$drafts = (int) $row['drafts'];
}
}
if ( $drafts > 0 ) {
echo '
';
printf(
esc_html(
/* translators: %s: number of saved drafts */
_n(
'Heads up: %s visitor has used "Save and come back later". Their saved draft is held here, with the answers so far and the email the resume link went to. It\'s a part-finished form, not spam or a queued email. Saved drafts clear themselves when the resume link expires (set on Settings → Workflow).',
'Heads up: %s visitors have used "Save and come back later". Their saved drafts are held here, with the answers so far and the email each resume link went to. They\'re part-finished forms, not spam or queued emails. Saved drafts clear themselves when the resume link expires (set on Settings → Workflow).',
$drafts,
'holmesportfolio-forms'
)
),
esc_html( number_format_i18n( $drafts ) )
);
echo '
';
$due_count = (int) HP_Forms_Storage::count_expired();
$message = $due_count > 0
/* translators: %s: number of entries */
? sprintf( _n( 'The daily clean-up will remove %s entry that is now past its retention period.', 'The daily clean-up will remove %s entries that are now past their retention period.', $due_count, 'holmesportfolio-forms' ), number_format_i18n( $due_count ) )
: __( 'No stored entries are currently past their retention period.', 'holmesportfolio-forms' );
echo '
' . esc_html( $message ) . ' ';
esc_html_e( 'Figures refresh a few minutes after a change. Export what you need and delete the rest to keep this down.', 'holmesportfolio-forms' );
echo '
';
}
public function field_storage_diagnostics() {
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-storage.php';
$stats = get_option( HP_Forms_Storage::STORAGE_DIAGNOSTICS_OPTION );
if ( ! is_array( $stats ) || empty( $stats['computed_at'] ) ) {
echo '
' . esc_html__( 'Not worked out yet. This is done once a day by the same clean-up that removes entries past their retention period, so it will appear the day after this plugin was activated.', 'holmesportfolio-forms' ) . '
';
return;
}
printf(
'
%s
',
esc_html(
sprintf(
/* translators: %s: how long ago this was worked out, e.g. "3 hours" */
__( 'Worked out %s ago. Reading through every entry to find the largest ones takes too long to do while you wait, so this is only ever done once a day.', 'holmesportfolio-forms' ),
human_time_diff( (int) $stats['computed_at'] )
)
)
);
if ( empty( $stats['by_form'] ) && empty( $stats['largest'] ) ) {
echo '
' . esc_html__( 'Uploaded files and photos aren\'t kept in entries, so they don\'t count towards these sizes. Open a large entry to see what\'s taking the space, such as long answers, a group filled in many times or a drawn signature.', 'holmesportfolio-forms' ) . '
';
}
public function field_retention() {
if ( 'never' === $this->get_setting( 'store_mode', 'all' ) ) {
echo '
' . esc_html__( 'Not used. No submissions are stored. Change “Store submissions” above to set a retention period.', 'holmesportfolio-forms' ) . '
';
$this->hide_row_when_store_mode_never();
return;
}
$bounds = HP_Forms_Storage::retention_bounds();
$days = HP_Forms_Storage::retention_days_for( 0 );
printf(
' %5$s',
(int) $bounds['min'],
(int) $bounds['max'],
esc_attr( self::SETTINGS ),
(int) $days,
esc_html(
sprintf(
/* translators: 1: minimum days, 2: maximum days */
__( 'A daily job removes stored entries older than this. Allowed range %1$d–%2$d days. Retention always applies. There is no "keep forever".', 'holmesportfolio-forms' ),
(int) $bounds['min'],
(int) $bounds['max']
)
)
);
echo '
' . wp_kses_post(
sprintf(
/* translators: %s: link to the ICO storage limitation guidance */
__( 'UK data protection law says personal data must not be kept longer than you need it for the purpose you collected it. There is no fixed number in law. %s explains how to decide. A developer can change the allowed range with a filter if you have a documented reason. This is guidance, not legal advice.', 'holmesportfolio-forms' ),
'' . esc_html__( 'The Information Commission’s Office (ICO) guidance on storage limitation', 'holmesportfolio-forms' ) . self::new_tab_hint() . ''
)
) . '
';
$this->hide_row_when_store_mode_never();
}
private function hide_row_when_store_mode_never() {
echo '';
}
/** Blank follows the global value. */
public function field_retention_overrides() {
$mode = $this->get_setting( 'store_mode', 'all' );
if ( 'never' === $mode ) {
echo '
' . esc_html__( 'Not used. No submissions are stored.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'No forms are set to store submissions, so there is nothing to age out.', 'holmesportfolio-forms' ) . '
';
return;
}
}
echo '
' . esc_html(
sprintf(
/* translators: 1: minimum days, 2: maximum days, 3: global value */
__( 'Leave a box blank to use the value above (%3$d days). An override outside %1$d–%2$d days is brought back into that range.', 'holmesportfolio-forms' ),
(int) $bounds['min'],
(int) $bounds['max'],
(int) HP_Forms_Storage::retention_days_for( 0 )
)
) . '
',
esc_attr( self::SETTINGS ),
checked( ! empty( $this->get_setting( 'store_ip' ) ), true, false ),
esc_html__( 'Save the visitor IP address with each stored entry', 'holmesportfolio-forms' ),
esc_html__( 'Off by default, and only kept when submissions are stored at all. An IP address is personal data under UK data protection law. Turn this on only if you have a specific reason, for example fraud investigation, and your privacy notice covers it. This is guidance, not legal advice.', 'holmesportfolio-forms' )
);
$this->hide_row_when_store_mode_never();
}
public function field_encryption() {
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-crypt.php';
if ( ! HP_Forms_Crypt::is_active() ) {
/** @param string $plain_note What is held as plain text while encryption is off; an add-on names its own data. */
$plain_note = (string) apply_filters( 'holmesforms_encryption_plain_note', __( 'Stored answers are held as plain text in the database.', 'holmesportfolio-forms' ) );
echo '
' . esc_html__( 'An encryption key is set but it is shorter than 16 characters, so it is ignored and nothing is encrypted. Use a longer key.', 'holmesportfolio-forms' ) . '
';
}
echo '
' . esc_html__( 'To encrypt them at rest:', 'holmesportfolio-forms' ) . '
' . esc_html__( 'Entries saved before you add the key stay unencrypted for now. Once the key is in place, a button appears here to encrypt them.', 'holmesportfolio-forms' ) . '
';
echo wp_kses( '
' . esc_html__( 'To keep the key off the web server entirely (for NHS DSP Toolkit or a strict security review), set an HOLMESFORMS_ENCRYPTION_KEY environment variable instead, or return it from the holmesforms_encryption_key filter in a must-use plugin (fetching it from a secrets manager). The order tried is: filter, then the constant, then the environment variable.', 'holmesportfolio-forms' )
. self::guide_link_html( 'encryption-key-source' ) . '
', HP_Forms_Kses::admin() );
return;
}
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL -- the plugin's own table, counts sealed and plain rows
$stored_counts = (array) $wpdb->get_row( $wpdb->prepare( "SELECT SUM( data LIKE %s ) sealed, SUM( data NOT LIKE %s AND data <> '' ) plain FROM %i", $wpdb->esc_like( 'hpfenc1:' ) . '%', $wpdb->esc_like( 'hpfenc1:' ) . '%', HP_Forms_Storage::table() ), ARRAY_A );
$plain_count = (int) ( $stored_counts['plain'] ?? 0 );
$sealed_count = (int) ( $stored_counts['sealed'] ?? 0 );
$source = HP_Forms_Crypt::source_name();
$source_label = array(
'filter' => __( 'the holmesforms_encryption_key filter (a secrets manager or similar)', 'holmesportfolio-forms' ),
'env' => __( 'an HOLMESFORMS_ENCRYPTION_KEY environment variable', 'holmesportfolio-forms' ),
'constant' => __( 'the HOLMESFORMS_ENCRYPTION_KEY constant in wp-config.php', 'holmesportfolio-forms' ),
);
echo '
' . esc_html__( 'On.', 'holmesportfolio-forms' ) . ' '
. esc_html__( 'New entries are sealed with libsodium before they are written, and opened again on read.', 'holmesportfolio-forms' ) . '
';
if ( ! extension_loaded( 'sodium' ) ) {
echo '
' . esc_html__( 'Encryption still works, but this server\'s PHP is missing the sodium extension, so opening, listing, searching and exporting entries can be much slower. Ask your host to turn on the PHP sodium extension, which comes as standard with PHP 7.2 and later.', 'holmesportfolio-forms' ) . '
' . esc_html(
sprintf(
/* translators: %s: where the key currently comes from */
__( 'Key currently supplied by: %s.', 'holmesportfolio-forms' ),
$source_label[ $source ]
)
);
if ( 'constant' === $source ) {
echo ' ' . esc_html__( 'That sits on the same server as the database it protects. To keep it off the web root instead (for NHS DSP Toolkit or a strict security review), move it to an environment variable, or return it from the holmesforms_encryption_key filter in a must-use plugin (fetching it from a secrets manager). Whichever you set is picked up automatically. Nothing else changes.', 'holmesportfolio-forms' );
}
echo wp_kses( self::guide_link_html( 'encryption' ) . '
' . esc_html__( 'While encryption is on, the Entries search box scans the most recent few thousand entries rather than all of them.', 'holmesportfolio-forms' ) . '
';
}
public function handle_adopt_key() {
if ( ! current_user_can( 'holmesforms_admin' ) || ! check_admin_referer( 'holmesforms_adopt_key' ) ) {
wp_die( esc_html__( 'You are not allowed to do that.', 'holmesportfolio-forms' ), '', array( 'response' => 403 ) );
}
if ( HP_Forms_Crypt::is_active() ) {
HP_Forms_Crypt::adopt_key();
}
delete_transient( 'holmesforms_enc_check' );
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=' . self::MENU_SLUG ) );
exit;
}
public function handle_unsaved_seen() {
if ( ! current_user_can( self::CAP ) || ! check_admin_referer( 'holmesforms_unsaved_seen' ) ) {
wp_die( esc_html__( 'You are not allowed to do that.', 'holmesportfolio-forms' ), '', array( 'response' => 403 ) );
}
delete_option( HP_Forms_Storage::OPTION_UNSAVED );
wp_safe_redirect( wp_get_referer() ? wp_get_referer() : admin_url( 'admin.php?page=' . self::MENU_SLUG ) );
exit;
}
/** Re-writes plaintext rows through the encryptor in batches. */
public function handle_encrypt_backfill() {
if ( ! current_user_can( 'holmesforms_admin' ) || ! check_admin_referer( 'holmesforms_encrypt_backfill' ) ) {
wp_die( esc_html__( 'You are not allowed to do that.', 'holmesportfolio-forms' ), '', array( 'response' => 403 ) );
}
require_once HOLMESFORMS_DIR . 'includes/class-hp-forms-crypt.php';
delete_transient( 'holmesforms_enc_check' );
if ( ! HP_Forms_Crypt::can_write() ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => self::SETTINGS_SLUG,
'tab' => 'data',
),
admin_url( 'admin.php' )
)
);
exit;
}
global $wpdb;
$table = HP_Forms_Storage::table();
$encrypted = 0;
for ( $batch = 0; $batch < self::ENCRYPT_BATCHES; $batch++ ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery -- the plugin's own table, rows still to seal
$rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, data, context FROM %i WHERE data NOT LIKE %s AND data <> '' LIMIT %d", $table, $wpdb->esc_like( 'hpfenc1:' ) . '%', self::ENCRYPT_BATCH_SIZE ), ARRAY_A );
if ( ! $rows ) {
break;
}
foreach ( $rows as $row ) {
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- the plugin's own table, writes the sealed row
$wpdb->update(
$table,
array(
'data' => HP_Forms_Crypt::encrypt( (string) $row['data'] ),
'context' => HP_Forms_Crypt::is_sealed( $row['context'] ) ? $row['context'] : HP_Forms_Crypt::encrypt( (string) $row['context'] ),
),
array( 'id' => (int) $row['id'] ),
array( '%s', '%s' ),
array( '%d' )
);
++$encrypted;
}
}
delete_transient( 'holmesforms_enc_has_sealed' );
wp_safe_redirect(
add_query_arg(
array(
'page' => self::SETTINGS_SLUG,
'tab' => 'data',
'holmesforms_enc' => $encrypted,
),
admin_url( 'admin.php' )
)
);
exit;
}
public function field_ref_prefix() {
printf(
' %s',
(int) self::MAX_REF_PREFIX_LENGTH,
esc_attr( self::SETTINGS ),
esc_attr( $this->get_setting( 'ref_prefix', 'ENQ' ) ),
esc_html__( 'Every submission gets a reference like ENQ-000042, shown in the entry, the spreadsheet export and any email or submit message that uses the [reference] token. The number only ever counts up, so a reference is never reused even after an entry is deleted. Letters and digits only.', 'holmesportfolio-forms' )
);
}
public function field_spam_provider() {
$value = $this->get_setting( 'spam_provider', 'none' );
$options = array(
'none' => __( 'None', 'holmesportfolio-forms' ),
'turnstile' => __( 'Cloudflare Turnstile (GOV.UK does not recommend it)', 'holmesportfolio-forms' ),
'recaptcha' => __( 'Google reCAPTCHA v2, the “I’m not a robot” box (GOV.UK does not recommend it)', 'holmesportfolio-forms' ),
);
self::render_setting_select( 'hpf-spam-provider', 'spam_provider', $options, $value );
}
public function field_visitor_ip() {
$value = $this->get_setting( 'visitor_ip', 'direct' );
$options = array(
'direct' => __( 'Straight from the visitor (most sites)', 'holmesportfolio-forms' ),
'cloudflare' => __( 'Through Cloudflare', 'holmesportfolio-forms' ),
'proxy' => __( 'Through another proxy or load balancer my host runs', 'holmesportfolio-forms' ),
);
self::render_setting_select( 'hpf-visitor-ip', 'visitor_ip', $options, $value );
echo '
' . esc_html__( 'How the site finds a visitor\'s address for the spam limits. Behind Cloudflare or a proxy, "Straight from the visitor" makes every visitor look like one address. Busy forms can then turn real people away with "Too many attempts". Only pick a proxy if one really is in front of your site. Otherwise a visitor could fake their address. The limit is 30 sends in 10 minutes from one address. People sharing one office or school connection count as one address. Signed-in staff are not counted. On the proxy choice, the forwarded address is only believed from a private address, or one listed with the holmesforms_trusted_proxies filter.', 'holmesportfolio-forms' ) . '
' . esc_html__( 'Recent visits arrived with proxy or Cloudflare headers. Your site probably sits behind one, so check this setting.', 'holmesportfolio-forms' ) . '
';
}
}
public function field_flood_limit() {
printf(
' %s
%s
',
esc_attr( self::SETTINGS ),
(int) $this->get_setting( 'flood_limit', HOLMESFORMS_DEFAULT_FLOOD_LIMIT ),
esc_html__( 'entries to one form in an hour (0 = off)', 'holmesportfolio-forms' ),
esc_html__( 'Above this, the site emails the site admin address once a day for that form and shows a notice here, because it may be spam. Entries are still accepted.', 'holmesportfolio-forms' )
);
}
public function field_captcha_keys() {
$provider = $this->get_setting( 'spam_provider', 'none' );
$keys_by_provider = array(
'turnstile' => array(
'turnstile_site' => __( 'Turnstile site key', 'holmesportfolio-forms' ),
'turnstile_secret' => __( 'Turnstile secret key', 'holmesportfolio-forms' ),
),
'recaptcha' => array(
'recaptcha_site' => __( 'reCAPTCHA site key', 'holmesportfolio-forms' ),
'recaptcha_secret' => __( 'reCAPTCHA secret key', 'holmesportfolio-forms' ),
),
);
$cookie_warnings = array(
'turnstile' => __( 'With Turnstile on, Cloudflare\'s check loads with your forms and reads information from your visitors\' browsers and devices to tell people from bots. Cloudflare can also set its own cookies while it runs. HolmesPortfolio Forms sets no cookies of its own. Add Cloudflare to your cookie notice and privacy policy. The UK\'s cookie rules (the Privacy and Electronic Communications Regulations) cover reading information from a device, not only cookies. Check whether you need visitors\' consent first.', 'holmesportfolio-forms' ),
'recaptcha' => __( 'With reCAPTCHA on, Google\'s check loads with your forms, sets a cookie called _GRECAPTCHA and reads information from your visitors\' browsers and devices. Google may also receive other Google cookies a visitor already has. HolmesPortfolio Forms sets no cookies of its own. Add Google to your cookie notice and privacy policy. The UK\'s cookie rules (the Privacy and Electronic Communications Regulations) cover reading information from a device, not only cookies. Check whether you need visitors\' consent first.', 'holmesportfolio-forms' ),
);
foreach ( $keys_by_provider as $group_provider => $fields ) {
$hidden_attribute = $provider === $group_provider ? '' : ' hidden';
echo wp_kses( '
', HP_Forms_Kses::admin() );
echo '
' . esc_html__( 'This uses cookies or similar technology.', 'holmesportfolio-forms' ) . ' ' . esc_html( $cookie_warnings[ $group_provider ] ) . '
' . esc_html__( 'Already saved. Leave the box blank to keep it. To remove both keys, empty the site key box above and save.', 'holmesportfolio-forms' ) . '
';
}
}
echo '
';
}
echo '
' . esc_html__( 'Keys for the provider you are not using are kept, so you can switch back. To remove a provider\'s keys, clear its site key and save. That removes its secret key too.', 'holmesportfolio-forms' ) . '
';
if ( 'recaptcha' === $provider ) {
$this->render_captcha_key_note(
__( 'Which reCAPTCHA keys work', 'holmesportfolio-forms' ),
array( __( 'reCAPTCHA v2, "I\'m not a robot" checkbox', 'holmesportfolio-forms' ) ),
array( __( 'reCAPTCHA v3', 'holmesportfolio-forms' ), __( 'reCAPTCHA v2 Invisible', 'holmesportfolio-forms' ), __( 'reCAPTCHA v2 Android', 'holmesportfolio-forms' ) ),
__( 'Pick Turnstile if you want a check that shows nothing to most visitors.', 'holmesportfolio-forms' )
);
} elseif ( 'turnstile' === $provider ) {
$this->render_captcha_key_note(
__( 'Which Turnstile keys work', 'holmesportfolio-forms' ),
array( __( 'Managed', 'holmesportfolio-forms' ), __( 'Non-Interactive', 'holmesportfolio-forms' ), __( 'Invisible', 'holmesportfolio-forms' ) ),
array(),
__( 'Non-Interactive and Invisible show no challenge to most visitors.', 'holmesportfolio-forms' )
);
}
if ( 'none' !== $provider ) {
echo '
';
echo '
' . esc_html__( 'Checks the saved secret key with the provider. The site key is checked when the check first loads on a form. Save your changes first.', 'holmesportfolio-forms' ) . '