'external', 'url' => esc_url( $external ), 'label' => $label, ); } $email = (string) Options::get( 'support_email' ); if ( '' === $email ) { return array( 'type' => 'none', 'url' => '', 'label' => $label, ); } /** * Use the built-in form? * * @since 0.1.0 * * @param bool $use_form True = modal with AJAX form. */ $use_form = (bool) apply_filters( 'quietadmin_support_use_form', true ); if ( $use_form ) { return array( 'type' => 'modal', 'url' => '#qa-modal-support', 'label' => $label, ); } $subject = (string) Options::get( 'support_email_subject' ); return array( 'type' => 'mailto', 'url' => 'mailto:' . $email . '?subject=' . rawurlencode( $subject ), 'label' => $label, ); } /** * Renders the support modal (in the admin footer). * * @since 0.1.0 * * @return void */ public function render_modal() { if ( ! ClientMode::is_active() ) { return; } $target = self::target(); if ( 'modal' !== $target['type'] ) { return; } if ( ! current_user_can( 'qa_request_support' ) ) { return; } Template::render( 'support-modal' ); } /** * AJAX handler for the support form. * * @since 0.1.0 * * @return void */ public function handle_ajax() { check_ajax_referer( self::AJAX_ACTION, 'nonce' ); if ( ! is_user_logged_in() || ! current_user_can( 'qa_request_support' ) ) { wp_send_json_error( array( 'message' => __( 'This action is not allowed.', 'oms-quietadmin' ) ), 403 ); } // Honeypot: bots fill in the hidden field. // phpcs:ignore WordPress.Security.NonceVerification.Matched -- Nonce was verified above via check_ajax_referer(). if ( isset( $_POST['website'] ) && '' !== trim( sanitize_text_field( wp_unslash( $_POST['website'] ) ) ) ) { wp_send_json_error( array( 'message' => __( 'The request was not submitted.', 'oms-quietadmin' ) ), 400 ); } // Rate limit: max. 5 requests per hour and user (fixed window). $user_id = get_current_user_id(); $limit_key = 'quietadmin_support_' . $user_id; $bucket = get_transient( $limit_key ); $bucket = is_array( $bucket ) ? $bucket : array( 'count' => 0, 'until' => 0 ); if ( (int) $bucket['until'] < time() ) { $bucket = array( 'count' => 0, 'until' => 0 ); } $sent = (int) $bucket['count']; /** * Maximum support requests per hour. * * @since 0.1.0 * * @param int $limit Requests. */ $limit = (int) apply_filters( 'quietadmin_support_rate_limit', 5 ); if ( $sent >= $limit ) { wp_send_json_error( array( 'message' => __( 'You have already sent us several messages. We will get back to you as soon as possible – please be patient.', 'oms-quietadmin' ), ), 429 ); } $topic = isset( $_POST['topic'] ) ? sanitize_text_field( wp_unslash( $_POST['topic'] ) ) : ''; $message = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : ''; $phone = isset( $_POST['phone'] ) ? sanitize_text_field( wp_unslash( $_POST['phone'] ) ) : ''; $urgent = ! empty( $_POST['urgent'] ); $length = function_exists( 'mb_strlen' ) ? mb_strlen( $message ) : strlen( $message ); if ( $length < 5 ) { wp_send_json_error( array( 'message' => __( 'Please describe your request in a few sentences.', 'oms-quietadmin' ) ), 400 ); } $user = wp_get_current_user(); $email = (string) Options::get( 'support_email' ); if ( '' === $email || ! is_email( $email ) ) { wp_send_json_error( array( 'message' => __( 'The support channel is not configured. Please contact your agency directly.', 'oms-quietadmin' ) ), 500 ); } $sent_mail = $this->send_mail( $email, $topic, $message, $phone, $urgent, $user ); if ( ! $sent_mail ) { wp_send_json_error( array( 'message' => __( 'Unfortunately the message could not be sent. Please try again or give us a call.', 'oms-quietadmin' ) ), 500 ); } if ( 0 === (int) $bucket['until'] ) { $bucket['until'] = time() + HOUR_IN_SECONDS; } $bucket['count'] = $sent + 1; set_transient( $limit_key, $bucket, max( 60, (int) $bucket['until'] - time() ) ); Tracker::log( Event_Log::TYPE_SUPPORT, 'support_request', $topic ? $topic : __( 'Support request submitted', 'oms-quietadmin' ), array( 'user_id' => $user_id, 'urgent' => $urgent, ) ); wp_send_json_success( array( 'message' => __( 'Thank you! We have received your message. We usually respond within one business day.', 'oms-quietadmin' ), ) ); } /** * Sends the support email to the agency. * * @since 0.1.0 * * @param string $to Recipient. * @param string $topic Topic. * @param string $message Message. * @param string $phone Phone number. * @param bool $urgent Whether the request is flagged as urgent. * @param \WP_User $user Sender. * @return bool */ protected function send_mail( $to, $topic, $message, $phone, $urgent, $user ) { $site_name = wp_specialchars_decode( (string) get_bloginfo( 'name' ), ENT_QUOTES ); $subject = (string) Options::get( 'support_email_subject' ); if ( '' === $subject ) { $subject = sprintf( /* translators: %s: site name. */ __( '[%s] Support request from the dashboard', 'oms-quietadmin' ), $site_name ); } if ( $urgent ) { $subject = __( 'URGENT: ', 'oms-quietadmin' ) . $subject; } if ( '' !== $topic ) { $subject .= ' – ' . $topic; } $template = (string) Options::get( 'support_email_body' ); if ( '' === $template ) { $template = __( "Message:\n\n{message}\n\n---\nCustomer: {name} ({email})\nWebsite: {site}", 'oms-quietadmin' ); } // Older saved templates contain "\n" as two characters. $template = str_replace( '\n', "\n", $template ); $replacements = array( '{message}' => $message, '{topic}' => $topic, '{name}' => trim( $user->display_name ), '{email}' => $user->user_email, '{phone}' => $phone, '{site}' => home_url( '/' ), '{user}' => $user->user_login, '{url}' => self::admin_url_of( $user ), ); // Single pass with strtr(): placeholders in client texts are not replaced again. $body = strtr( $template, $replacements ); // sanitize_text_field() strips line breaks (protection against header injection). $reply_name = trim( str_replace( array( '"', '<', '>', ',', ';' ), '', sanitize_text_field( $user->display_name ) ) ); $reply_mail = sanitize_email( $user->user_email ); $headers = array( 'Content-Type: text/plain; charset=UTF-8' ); if ( is_email( $reply_mail ) ) { $headers[] = '' !== $reply_name ? sprintf( 'Reply-To: %s <%s>', $reply_name, $reply_mail ) : 'Reply-To: ' . $reply_mail; } /** * Filters the support email before it is sent. * * @since 0.1.0 * * @param array $mail Mail data (to, subject, body, headers). */ $mail = apply_filters( 'quietadmin_support_mail', array( 'to' => $to, 'subject' => $subject, 'body' => $body, 'headers' => $headers, ) ); return (bool) wp_mail( $mail['to'], $mail['subject'], $mail['body'], $mail['headers'] ); } /** * Builds the context URL for the agency. * * @since 0.1.0 * * @param \WP_User $user Client. * @return string */ protected static function admin_url_of( $user ) { return admin_url( 'user-edit.php?user_id=' . (int) $user->ID ); } }