0 ) { $title .= sprintf( ' %s', esc_html( number_format_i18n( $unread ) ) ); } $hook = add_submenu_page( 'edit.php?post_type=' . Form_Post_Type::POST_TYPE, __( 'Entries', 'flexsolutions-form-builder' ), $title, 'read_tflexforms_entries', self::PAGE, array( self::class, 'render' ) ); if ( false !== $hook ) { add_action( 'load-' . $hook, array( self::class, 'on_load' ) ); } } /** * Screen options and bulk actions (before output, so we can redirect). */ public static function on_load(): void { add_screen_option( 'per_page', array( 'default' => 20, 'option' => 'tflexforms_entries_per_page', ) ); $table = new Entries_List_Table(); $action = $table->current_action(); $statuses = array( 'mark_read' => 'read', 'mark_unread' => 'unread', 'not_spam' => 'unread', 'mark_spam' => Entry_Store::SPAM, ); if ( false === $action || ( 'delete' !== $action && ! isset( $statuses[ $action ] ) ) ) { return; } check_admin_referer( 'bulk-entries' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified just above. $ids = isset( $_REQUEST['entry_ids'] ) && is_array( $_REQUEST['entry_ids'] ) ? array_map( 'absint', wp_unslash( $_REQUEST['entry_ids'] ) ) : array(); if ( 'delete' === $action ) { if ( ! current_user_can( 'delete_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to delete entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $count = Entry_Store::delete( $ids ); wp_safe_redirect( add_query_arg( 'deleted', $count, self::list_url() ) ); exit; } Entry_Store::set_status( $ids, $statuses[ $action ] ); wp_safe_redirect( add_query_arg( 'updated', count( $ids ), self::list_url() ) ); exit; } /** * Saves the per-page screen option. * * @param mixed $status Default. * @param string $option Option name. * @param mixed $value Value. * @return int */ public static function save_per_page( $status, $option, $value ): int { unset( $status, $option ); return max( 1, min( 200, (int) $value ) ); } /** * Page URLs. */ public static function list_url(): string { return admin_url( 'edit.php?post_type=' . Form_Post_Type::POST_TYPE . '&page=' . self::PAGE ); } /** * URL of one entry. * * @param int $id Entry ID. */ public static function entry_url( int $id ): string { return add_query_arg( 'entry', $id, self::list_url() ); } /** * Nonced delete URL. * * @param int $id Entry ID. */ public static function delete_url( int $id ): string { return wp_nonce_url( admin_url( 'admin-post.php?action=tflexforms_delete_entry&entry=' . $id ), 'tflexforms_delete_entry_' . $id ); } /** * Nonced URL that changes one entry's status. * * @param int $id Entry ID. * @param string $status unread|read|spam. */ public static function status_url( int $id, string $status ): string { return wp_nonce_url( admin_url( 'admin-post.php?action=tflexforms_entry_status&status=' . $status . '&entry=' . $id ), 'tflexforms_entry_status_' . $id ); } /** * Nonced URL that resends an entry's notification. * * @param int $id Entry ID. */ public static function resend_url( int $id ): string { return wp_nonce_url( admin_url( 'admin-post.php?action=tflexforms_resend_mail&entry=' . $id ), 'tflexforms_resend_mail_' . $id ); } /** * URL of the form that edits one entry's values. * * @param int $id Entry ID. */ public static function edit_url( int $id ): string { return add_query_arg( 'edit', '1', self::entry_url( $id ) ); } /** * Nonced URL that deletes one note. * * @param int $note_id Note ID. */ public static function delete_note_url( int $note_id ): string { return wp_nonce_url( admin_url( 'admin-post.php?action=tflexforms_delete_note¬e=' . $note_id ), 'tflexforms_delete_note_' . $note_id ); } /** * Nonced file download URL. * * @param int $file_id File row ID. */ public static function download_url( int $file_id ): string { return wp_nonce_url( admin_url( 'admin-post.php?action=tflexforms_download&file=' . $file_id ), 'tflexforms_download_' . $file_id ); } /** * Renders the list or a single entry. */ public static function render(): void { if ( ! current_user_can( 'read_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to view entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $entry_id = absint( Entries_List_Table::filter( 'entry' ) ); if ( $entry_id > 0 ) { if ( '1' === Entries_List_Table::filter( 'edit' ) && current_user_can( 'edit_tflexforms_entries' ) ) { self::render_edit( $entry_id ); } else { self::render_entry( $entry_id ); } return; } $table = new Entries_List_Table(); $table->prepare_items(); ?>


0 ) { printf( '

%s

', esc_html( sprintf( /* translators: %d: number of entries. */ _n( '%d entry deleted.', '%d entries deleted.', $deleted, 'flexsolutions-form-builder' ), $deleted ) ) ); } ?>
search_box( __( 'Search entries', 'flexsolutions-form-builder' ), 'tflexforms-entries' ); $table->display(); ?>
'; echo ''; wp_nonce_field( 'tflexforms_export', '_wpnonce', false ); foreach ( Entries_List_Table::query_args() as $key => $value ) { $name = array( 'status' => 'entry_status', 'search' => 's', )[ $key ] ?? $key; if ( '' !== (string) $value && 0 !== $value ) { printf( '', esc_attr( $name ), esc_attr( 'order' === $key ? strtolower( (string) $value ) : (string) $value ) ); } } echo ''; } /** * Single entry view. Marks the entry as read. * * @param int $id Entry ID. */ private static function render_entry( int $id ): void { $entry = Entry_Store::get( $id ); if ( null === $entry ) { echo '

' . esc_html__( 'Entry not found', 'flexsolutions-form-builder' ) . '

' . esc_html__( 'Back to entries', 'flexsolutions-form-builder' ) . '

'; return; } if ( 'unread' === $entry->status ) { Entry_Store::set_status( array( $id ), 'read' ); } $form_post = get_post( (int) $entry->form_id ); $time = strtotime( $entry->created_at . ' UTC' ); $spam = Entry_Store::SPAM === $entry->status; $mail_failed = ! $spam && ! (int) $entry->mail_sent; $can_resend = $mail_failed && null !== Entry_Store::pending_mail( $entry ) && $form_post instanceof \WP_Post; $retry = $can_resend ? Jobs::find_pending( 'retry_mail', array( 'entry_id' => $id ) ) : null; $mail_log = Entry_Store::mail_log( $entry ); $can_edit = current_user_can( 'edit_tflexforms_entries' ); ?>

' . esc_html__( 'Notification sent.', 'flexsolutions-form-builder' ) . '

'; } elseif ( '0' === $resent ) { echo '

' . esc_html__( 'The notification could not be sent. Check your mail settings and try again.', 'flexsolutions-form-builder' ) . '

'; } if ( $spam ) { echo '

' . esc_html__( 'This entry was flagged as spam, so no notification was sent. If it is genuine, mark it as not spam; you can then send the notification.', 'flexsolutions-form-builder' ) . '

'; } elseif ( $mail_failed ) { if ( null !== $retry ) { $notice = sprintf( /* translators: %s: date and time of the next attempt. */ __( 'The notification email for this entry could not be sent yet. The entry is safe, and the email will be tried again automatically (next attempt: %s). You can also send it now.', 'flexsolutions-form-builder' ), self::format_time( (string) $retry->run_at ) ); } elseif ( $can_resend ) { $notice = __( 'The notification email for this entry could not be sent. The entry is safe; you can try sending it again.', 'flexsolutions-form-builder' ); } else { $notice = __( 'The notification email for this entry could not be sent, and there is no saved message to resend.', 'flexsolutions-form-builder' ); } echo '

' . esc_html( $notice ) . '

'; } ?> values as $name => $value ) : ?> files as $file ) : ?>

page_url ) : ?> user_id > 0 ) : ?> user_id ); ?> ip ) : ?>

0 /* translators: %d: number of values changed. */ ? sprintf( _n( '%d value changed.', '%d values changed.', $count, 'flexsolutions-form-builder' ), $count ) : __( 'Nothing was changed.', 'flexsolutions-form-builder' ); echo '

' . esc_html( $text ) . '

'; } $messages = array( 'tags' => __( 'Tags saved.', 'flexsolutions-form-builder' ), 'note_added' => __( 'Note added.', 'flexsolutions-form-builder' ), 'note_deleted' => __( 'Note deleted.', 'flexsolutions-form-builder' ), ); $done = Entries_List_Table::filter( 'done' ); if ( isset( $messages[ $done ] ) ) { echo '

' . esc_html( $messages[ $done ] ) . '

'; } } /** * The entry's tags, with a form to change them. * * @param int $id Entry ID. * @param bool $can_edit Whether the user may change them. */ private static function render_tags( int $id, bool $can_edit ): void { $tags = Entry_Tags::for_entries( array( $id ) )[ $id ] ?? array(); if ( ! $can_edit && array() === $tags ) { return; } echo '

' . esc_html__( 'Tags', 'flexsolutions-form-builder' ) . '

'; if ( ! $can_edit ) { echo '

' . esc_html( implode( ', ', $tags ) ) . '

'; return; } ?>
'tflexforms-save-tags' ) ); ?>

' . esc_html__( 'Notes', 'flexsolutions-form-builder' ) . ''; if ( array() !== $notes ) { echo ''; } if ( ! $can_edit ) { return; } ?>


'tflexforms-add-note' ) ); ?>

content as $name => $change ) : ?>
created_at ) ); ?> user_id ) ); ?>

$multiline ) : ?> values[ $name ] ?? '' ); $input = 'tflexforms-value-' . md5( $name ); ?>
Field name => whether to edit it as multi-line text. */ private static function editable_fields( object $entry ): array { $types = array(); $form_post = get_post( (int) $entry->form_id ); if ( $form_post instanceof \WP_Post && Form_Post_Type::POST_TYPE === $form_post->post_type ) { foreach ( Form::from_post( $form_post )->input_fields() as $name => $field ) { $types[ (string) $name ] = $field->type; } } $files = array_map( static fn( $f ) => (string) $f->field_name, (array) $entry->files ); $out = array(); foreach ( $entry->values as $name => $value ) { if ( ! in_array( (string) $name, $files, true ) && ! in_array( $types[ $name ] ?? '', array( 'file', 'quiz' ), true ) ) { $out[ (string) $name ] = str_contains( (string) $value, "\n" ) || 'textarea' === ( $types[ $name ] ?? '' ); } } foreach ( $types as $name => $type ) { if ( ! isset( $out[ $name ] ) && ! isset( $entry->values[ $name ] ) && ! in_array( $type, array( 'file', 'quiz' ), true ) ) { $out[ $name ] = 'textarea' === $type; } } return $out; } /** * A user's display name for notes and history. * * @param int $user_id User ID. */ private static function user_name( int $user_id ): string { $user = $user_id > 0 ? get_userdata( $user_id ) : false; if ( $user instanceof \WP_User ) { return $user->display_name; } /* translators: %d: user ID. */ return $user_id > 0 ? sprintf( __( 'Deleted user #%d', 'flexsolutions-form-builder' ), $user_id ) : __( 'Unknown', 'flexsolutions-form-builder' ); } /** * Stops with a 403 unless the current user may change entries. */ private static function require_edit_cap(): void { if ( ! current_user_can( 'edit_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to change entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } } /** * Saves edited values (admin-post). */ public static function save_entry(): void { $id = isset( $_POST['entry'] ) ? absint( wp_unslash( $_POST['entry'] ) ) : 0; check_admin_referer( 'tflexforms_save_entry_' . $id ); self::require_edit_cap(); $entry = Entry_Store::get( $id ); if ( null === $entry ) { wp_die( esc_html__( 'Entry not found.', 'flexsolutions-form-builder' ), '', array( 'response' => 404 ) ); } $posted = isset( $_POST['values'] ) && is_array( $_POST['values'] ) ? wp_unslash( $_POST['values'] ) : array(); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Each value is sanitized below. $allowed = self::editable_fields( $entry ); $values = array(); foreach ( $posted as $name => $value ) { $name = (string) $name; if ( ! isset( $allowed[ $name ] ) || ! is_scalar( $value ) ) { continue; } $value = str_replace( array( "\r\n", "\r" ), "\n", (string) $value ); $values[ $name ] = str_contains( $value, "\n" ) ? sanitize_textarea_field( $value ) : sanitize_text_field( $value ); } $changes = Entry_Store::update_values( $id, $values, get_current_user_id() ); wp_safe_redirect( add_query_arg( 'changed', count( $changes ), self::entry_url( $id ) ) ); exit; } /** * Saves an entry's tags (admin-post). */ public static function save_tags(): void { $id = isset( $_POST['entry'] ) ? absint( wp_unslash( $_POST['entry'] ) ) : 0; check_admin_referer( 'tflexforms_entry_tags_' . $id ); self::require_edit_cap(); if ( null === Entry_Store::get( $id ) ) { wp_die( esc_html__( 'Entry not found.', 'flexsolutions-form-builder' ), '', array( 'response' => 404 ) ); } Entry_Tags::set( $id, isset( $_POST['tags'] ) ? sanitize_text_field( wp_unslash( $_POST['tags'] ) ) : '' ); wp_safe_redirect( add_query_arg( 'done', 'tags', self::entry_url( $id ) ) ); exit; } /** * Adds a note to an entry (admin-post). */ public static function add_note(): void { $id = isset( $_POST['entry'] ) ? absint( wp_unslash( $_POST['entry'] ) ) : 0; check_admin_referer( 'tflexforms_add_note_' . $id ); self::require_edit_cap(); if ( null === Entry_Store::get( $id ) ) { wp_die( esc_html__( 'Entry not found.', 'flexsolutions-form-builder' ), '', array( 'response' => 404 ) ); } $note = isset( $_POST['note'] ) ? sanitize_textarea_field( wp_unslash( $_POST['note'] ) ) : ''; $added = Entry_Notes::add( $id, $note, get_current_user_id() ) > 0; wp_safe_redirect( $added ? add_query_arg( 'done', 'note_added', self::entry_url( $id ) ) : self::entry_url( $id ) ); exit; } /** * Deletes a note (admin-post). */ public static function delete_note(): void { $note_id = isset( $_GET['note'] ) ? absint( wp_unslash( $_GET['note'] ) ) : 0; check_admin_referer( 'tflexforms_delete_note_' . $note_id ); $note = Entry_Notes::get( $note_id ); if ( null === $note || ! Entry_Notes::current_user_can_delete( $note ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to delete this note.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } Entry_Notes::delete( $note_id ); wp_safe_redirect( add_query_arg( 'done', 'note_deleted', self::entry_url( (int) $note->entry_id ) ) ); exit; } /** * What started an email attempt, for the attempts table. * * @param string $source submit, retry or resend. */ private static function attempt_label( string $source ): string { switch ( $source ) { case 'submit': return __( 'On submission', 'flexsolutions-form-builder' ); case 'retry': return __( 'Automatic retry', 'flexsolutions-form-builder' ); default: return __( 'Sent by hand', 'flexsolutions-form-builder' ); } } /** * A UTC database time in the site's date and time format. * * @param string $utc Y-m-d H:i:s in UTC. */ private static function format_time( string $utc ): string { $time = strtotime( $utc . ' UTC' ); return (string) wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), false !== $time ? $time : null ); } /** * Deletes one entry (admin-post). */ public static function delete_single(): void { $id = isset( $_GET['entry'] ) ? absint( wp_unslash( $_GET['entry'] ) ) : 0; check_admin_referer( 'tflexforms_delete_entry_' . $id ); if ( ! current_user_can( 'delete_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to delete entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $count = Entry_Store::delete( array( $id ) ); wp_safe_redirect( add_query_arg( 'deleted', $count, self::list_url() ) ); exit; } /** * Marks one entry read/unread (admin-post). */ public static function change_status(): void { $id = isset( $_GET['entry'] ) ? absint( wp_unslash( $_GET['entry'] ) ) : 0; check_admin_referer( 'tflexforms_entry_status_' . $id ); if ( ! current_user_can( 'read_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to change entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $status = isset( $_GET['status'] ) ? sanitize_key( wp_unslash( $_GET['status'] ) ) : ''; if ( 'not_spam' === $status ) { // Back to the entry, where its held notification can be sent. Entry_Store::set_status( array( $id ), 'unread' ); wp_safe_redirect( self::entry_url( $id ) ); exit; } Entry_Store::set_status( array( $id ), in_array( $status, array( 'read', Entry_Store::SPAM ), true ) ? $status : 'unread' ); wp_safe_redirect( self::list_url() ); exit; } /** * Resends an entry's kept notification (admin-post). */ public static function resend_mail(): void { $id = isset( $_GET['entry'] ) ? absint( wp_unslash( $_GET['entry'] ) ) : 0; check_admin_referer( 'tflexforms_resend_mail_' . $id ); if ( ! current_user_can( 'read_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to change entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $sent = Submission_Handler::resend_notification( $id ); wp_safe_redirect( add_query_arg( 'resent', $sent ? '1' : '0', self::entry_url( $id ) ) ); exit; } /** * Streams an uploaded file. Always sent as a download (never displayed * inline) so an uploaded file can never run as a page on your site. */ public static function download(): void { $file_id = isset( $_GET['file'] ) ? absint( wp_unslash( $_GET['file'] ) ) : 0; check_admin_referer( 'tflexforms_download_' . $file_id ); if ( ! current_user_can( 'read_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to download this file.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Custom table lookup. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', Database::table( 'entry_files' ), $file_id ) ); $path = is_object( $row ) ? Uploads::entry_file_path( (string) $row->stored_name ) : null; if ( null === $path ) { wp_die( esc_html__( 'File not found.', 'flexsolutions-form-builder' ), '', array( 'response' => 404 ) ); } // Stored files are encrypted; decrypt before sending anything. $plain = Uploads::open_plain( $path ); if ( null === $plain ) { wp_die( esc_html__( 'The file could not be decrypted. It may be damaged.', 'flexsolutions-form-builder' ), '', array( 'response' => 500 ) ); } $name = sanitize_file_name( (string) $row->original_name ); nocache_headers(); header( 'Content-Type: application/octet-stream' ); header( 'Content-Disposition: attachment; filename="' . $name . '"; filename*=UTF-8\'\'' . rawurlencode( $name ) ); header( 'Content-Length: ' . (string) fstat( $plain )['size'] ); header( 'X-Content-Type-Options: nosniff' ); header( "Content-Security-Policy: default-src 'none'; sandbox" ); fpassthru( $plain ); fclose( $plain ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- Our own stream. exit; } /** * Exports the entries list's current view as CSV (admin-post): the same * form, status, tag and search filters and order, every page. */ public static function export(): void { check_admin_referer( 'tflexforms_export' ); if ( ! current_user_can( 'read_tflexforms_entries' ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to export entries.', 'flexsolutions-form-builder' ), '', array( 'response' => 403 ) ); } $args = Entries_List_Table::query_args(); $bom = '1' === Entries_List_Table::filter( 'bom' ); $rows = self::csv_rows( $args ); $form_post = $args['form_id'] > 0 ? get_post( $args['form_id'] ) : null; $filename = sanitize_file_name( 'flexsolutions-form-builder-' . ( $form_post instanceof \WP_Post ? $form_post->post_name : 'entries' ) . '-' . gmdate( 'Y-m-d' ) . '.csv' ); nocache_headers(); header( 'Content-Type: text/csv; charset=utf-8' ); header( 'Content-Disposition: attachment; filename="' . $filename . '"' ); header( 'X-Content-Type-Options: nosniff' ); $out = fopen( 'php://output', 'w' ); if ( false === $out ) { exit; } self::write_csv( $out, $rows, $bom ); fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose exit; } /** * The CSV for a list view as rows of cells, header first. Columns: entry * ID, submitted time, status, the form (only when the view spans several * forms), the form's current fields, any other names found in the * entries, then tags. * * @param array $args Entry_Store::query() filters (form_id, status, search, tag, order). * @return array */ public static function csv_rows( array $args ): array { $form_id = (int) ( $args['form_id'] ?? 0 ); $columns = array(); $form_post = $form_id > 0 ? get_post( $form_id ) : null; if ( $form_post instanceof \WP_Post && Form_Post_Type::POST_TYPE === $form_post->post_type ) { foreach ( Form::from_post( $form_post )->input_fields() as $name => $field ) { if ( 'quiz' !== $field->type ) { $columns[ $name ] = true; } } } $items = array(); $page = 1; do { $batch = Entry_Store::query( array_merge( $args, array( 'per_page' => 500, 'page' => $page, ) ) ); $ids = array_map( static fn( $e ) => (int) $e->id, $batch['items'] ); $values = Entry_Store::values_for( $ids ); $tags = Entry_Tags::for_entries( $ids ); foreach ( $batch['items'] as $item ) { $item_values = $values[ (int) $item->id ] ?? array(); foreach ( array_keys( $item_values ) as $name ) { $columns[ $name ] = true; } $items[] = array( $item, $item_values, $tags[ (int) $item->id ] ?? array() ); } ++$page; $batch_size = count( $batch['items'] ); } while ( 500 === $batch_size && $page < 200 ); $with_form = $form_id <= 0; $header = array( 'Entry ID', 'Submitted', 'Status' ); if ( $with_form ) { $header[] = 'Form'; } $rows = array( array_merge( $header, array_map( 'strval', array_keys( $columns ) ), array( 'Tags' ) ) ); $titles = array(); foreach ( $items as [ $item, $item_values, $item_tags ] ) { $time = strtotime( $item->created_at . ' UTC' ); $line = array( (string) $item->id, (string) wp_date( 'Y-m-d H:i:s', false !== $time ? $time : null ), (string) $item->status ); if ( $with_form ) { $fid = (int) $item->form_id; if ( ! isset( $titles[ $fid ] ) ) { $titles[ $fid ] = null !== get_post( $fid ) ? get_the_title( $fid ) : __( '(deleted form)', 'flexsolutions-form-builder' ); } $line[] = $titles[ $fid ]; } foreach ( array_keys( $columns ) as $name ) { $line[] = (string) ( $item_values[ $name ] ?? '' ); } $line[] = implode( ', ', $item_tags ); $rows[] = $line; } return $rows; } /** * Writes CSV rows to a stream, every cell made formula-safe. * * @param resource $out Stream. * @param array $rows Rows from csv_rows(). * @param bool $bom Start with a UTF-8 byte order mark, which * Excel needs to read non-English text. */ public static function write_csv( $out, array $rows, bool $bom ): void { if ( $bom ) { fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite } foreach ( $rows as $row ) { fputcsv( $out, array_map( array( self::class, 'csv_cell' ), $row ), ',', '"', '' ); } } /** * Neutralises spreadsheet formulas (CSV injection): a cell starting with * = + - @ or a control character gets a leading apostrophe. * * @param string $value Cell value. */ public static function csv_cell( string $value ): string { return '' !== $value && str_contains( "=+-@\t\r\n", $value[0] ) ? "'" . $value : $value; } }