is_ready() ) { return $this->note(); } ++$this->count; return $this->form( self::ID_PREFIX . $this->count, $config ) . $this->assets(); } /** * Where the form would be, for someone who can set it up. Page caches skip logged-in users, * so this is never served to a visitor from a cache. * * @return string */ private function note() { if ( ! current_user_can( ContactFormScreen::CAPABILITY ) ) { return ''; } return sprintf( '

%2$s

', esc_url( ContactFormScreen::url() ), esc_html__( 'Contact form: set up spam protection to show this form', 'adminkeep' ) ); } /** * The form. Send is disabled until the front-end script has set up the spam check, so * without that script nothing can be sent; the noscript line says why. * * @param string $id The form's id. * @param ContactFormConfig $config Configuration in force. * @return string */ private function form( $id, ContactFormConfig $config ) { $html = '
data_attributes( $config ) . '>'; $html .= $this->field( $id, 'name', __( 'Name', 'adminkeep' ), '' ); $html .= $this->field( $id, 'email', __( 'Email', 'adminkeep' ), '' ); $html .= $this->field( $id, 'message', __( 'Message', 'adminkeep' ), '' ); $html .= '
'; $html .= '
'; $html .= ''; $html .= '
'; $html .= '
'; return $html; } /** * One labelled field with the place its own error message goes. * * @param string $id The form's id. * @param string $name Field name. * @param string $label Label text. * @param string $control The control, with %1$s for its id and %2$s for its message's id. * @return string */ private function field( $id, $name, $label, $control ) { $field_id = $id . '-' . $name; $error_id = $field_id . '-error'; return '
' . '' . sprintf( $control, esc_attr( $field_id ), esc_attr( $error_id ) ) . '' . '
'; } /** * What the front-end script needs, as data attributes of the form. Nothing per user or per * request: see the class comment. The nonce is added only when it is the chosen check. Read back with dataset, so text is escaped for the * attribute only and an owner's "Use <info@…>" reaches the page as typed. * * @param ContactFormConfig $config Configuration in force. * @return string Attributes, with a leading space. */ private function data_attributes( ContactFormConfig $config ) { $data = array( 'route' => esc_url_raw( rest_url( RestController::NAMESPACE_V1 . '/contact-form' ) ), 'provider' => (string) $config->get( 'provider' ), 'site-key' => $config->site_key(), 'action' => CaptchaVerifier::ACTION, 'i18n-sending' => __( 'Sending…', 'adminkeep' ), 'i18n-error' => $config->error_text(), 'i18n-check' => __( 'Please complete the check above.', 'adminkeep' ), 'i18n-blocked' => __( 'The spam check could not load. Please allow it, or contact us another way.', 'adminkeep' ), ); if ( 'nonce' === $data['provider'] ) { $data['nonce'] = ContactFormNonce::create(); } $html = ''; foreach ( $data as $name => $value ) { // Not esc_attr(): it leaves an existing entity alone, so an owner's typed "<" would // read back as "<". Encoding everything makes dataset return exactly what was typed. $html .= ' data-' . $name . '="' . htmlspecialchars( (string) $value, ENT_QUOTES, 'UTF-8' ) . '"'; } return $html; } /** * The form's style and script, for the end of every render. * * @return string */ private function assets() { if ( null === self::$assets ) { self::$assets = array( 'css' => $this->read( 'assets/css/contact-form.css' ), 'js' => $this->read( 'assets/js/contact-form.js' ), ); } // A stylesheet of ours cannot hold a closing tag; stripped in case a file ever does. $css = str_ireplace( '' . $css . '' . wp_get_inline_script_tag( self::$assets['js'] ); } /** * A file of the plugin's own, or '' if it is missing. * * @param string $path Path inside the plugin. * @return string */ private function read( $path ) { if ( ! is_readable( ADMINKEEP_DIR . $path ) ) { return ''; } // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- A local file shipped with the plugin. $source = (string) file_get_contents( ADMINKEEP_DIR . $path ); // No empty line may survive: wpautop() (a legacy Text widget, older themes and builders run // the shortcode's output through it) turns a blank line inside a script or style into // a closing and an opening paragraph tag, which breaks both. return trim( (string) preg_replace( "/\n[ \t\r]*(?:\n[ \t\r]*)+/", "\n", str_replace( "\r\n", "\n", $source ) ) ); } }