prefix . self::TABLE; } /** Create the table. Called on activation and on a version bump. */ public static function install() { global $wpdb; require_once ABSPATH . 'wp-admin/includes/upgrade.php'; $table = self::table(); $collate = $wpdb->get_charset_collate(); dbDelta( "CREATE TABLE {$table} ( id bigint(20) unsigned NOT NULL AUTO_INCREMENT, user_id bigint(20) unsigned NOT NULL DEFAULT 0, created_at datetime NOT NULL, read_at datetime DEFAULT NULL, subject varchar(255) NOT NULL DEFAULT '', body longtext NOT NULL, source varchar(60) NOT NULL DEFAULT '', PRIMARY KEY (id), KEY user_unread (user_id, read_at), KEY created (created_at) ) {$collate};" ); update_option( 'sirius_press_inbox_version', self::VERSION, false ); } public static function hooks() { add_filter( 'pre_wp_mail', array( __CLASS__, 'intercept_mail' ), 10, 2 ); add_action( 'admin_menu', array( __CLASS__, 'menu' ) ); add_action( 'admin_bar_menu', array( __CLASS__, 'admin_bar' ), 80 ); } /** * Deliver a message to a user's inbox. * * @param int $user_id 0 addresses every administrator. * @param string $subject * @param string $body HTML allowed; rendered with wp_kses_post. * @param string $source Free-form tag, e.g. 'wp_mail' or 'export'. * @return int Rows written. */ public static function add( $user_id, $subject, $body, $source = '' ) { global $wpdb; $targets = array(); if ( (int) $user_id > 0 ) { $targets[] = (int) $user_id; } else { foreach ( get_users( array( 'role' => 'administrator', 'fields' => 'ID' ) ) as $id ) { $targets[] = (int) $id; } } $written = 0; foreach ( array_unique( $targets ) as $id ) { $ok = $wpdb->insert( self::table(), array( 'user_id' => $id, 'created_at' => current_time( 'mysql', true ), 'subject' => mb_substr( wp_strip_all_tags( (string) $subject ), 0, 250 ), 'body' => (string) $body, 'source' => mb_substr( (string) $source, 0, 60 ), ), array( '%d', '%s', '%s', '%s', '%s' ) ); $written += $ok ? 1 : 0; } return $written; } /** * Short-circuit wp_mail for placeholder recipients. * * Returning a non-null value from `pre_wp_mail` tells WordPress the send * is handled. Returning `true` specifically means "delivered", which is * the truth here — it was delivered, to the inbox. * * @param null|bool $short Whatever an earlier filter decided. * @param array $atts wp_mail()'s arguments. * @return null|bool */ public static function intercept_mail( $short, $atts ) { if ( null !== $short ) { return $short; // Somebody else already claimed this send. } $to = isset( $atts['to'] ) ? $atts['to'] : array(); if ( ! is_array( $to ) ) { $to = explode( ',', (string) $to ); } $stub_domain = strtolower( SP_Settings::stub_email_domain() ); $captured = array(); $passing = array(); foreach ( $to as $recipient ) { $addr = strtolower( trim( self::bare_address( $recipient ) ) ); if ( '' === $addr ) { continue; } // Any .invalid recipient is ours by definition — RFC 2606 says the // TLD never resolves, so passing it to a mailer only produces a // bounce or a hard error in the log. if ( substr( $addr, -strlen( $stub_domain ) - 1 ) === '@' . $stub_domain || preg_match( '/@[^@]*\.invalid$/', $addr ) ) { $captured[] = $addr; } else { $passing[] = $recipient; } } if ( ! $captured ) { return null; // Nothing of ours — let the normal mailer have it. } $subject = isset( $atts['subject'] ) ? (string) $atts['subject'] : __( '(no subject)', 'sirius-press' ); $body = isset( $atts['message'] ) ? (string) $atts['message'] : ''; foreach ( $captured as $addr ) { $user = get_user_by( 'email', $addr ); self::add( $user ? (int) $user->ID : 0, $subject, self::to_html( $body ), 'wp_mail' ); } if ( $passing ) { // A mixed send: the real recipients still deserve their copy, so // re-enter wp_mail with only those. `pre_wp_mail` is not re-entered // for them because their addresses are not placeholders. $rest = $atts; $rest['to'] = $passing; wp_mail( $rest['to'], $subject, $body, isset( $rest['headers'] ) ? $rest['headers'] : '', isset( $rest['attachments'] ) ? $rest['attachments'] : array() ); } return true; } /** Pull `foo@bar` out of `Name `. */ private static function bare_address( $recipient ) { $recipient = (string) $recipient; if ( preg_match( '/<([^>]+)>/', $recipient, $m ) ) { return $m[1]; } return $recipient; } /** Plain-text mail bodies are the common case; keep their line breaks. */ private static function to_html( $body ) { if ( preg_match( '/<(a|p|br|div|table|html)\b/i', $body ) ) { return $body; } return wpautop( make_clickable( esc_html( $body ) ) ); } // ------------------------------------------------------------- reading it public static function unread_count( $user_id ) { global $wpdb; $table = self::table(); return (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE user_id = %d AND read_at IS NULL", (int) $user_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); } public static function recent( $user_id, $limit = 50, $offset = 0 ) { global $wpdb; $table = self::table(); return (array) $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$table} WHERE user_id = %d ORDER BY created_at DESC, id DESC LIMIT %d OFFSET %d", (int) $user_id, (int) $limit, (int) $offset ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); } public static function mark_all_read( $user_id ) { global $wpdb; $table = self::table(); return (int) $wpdb->query( $wpdb->prepare( "UPDATE {$table} SET read_at = %s WHERE user_id = %d AND read_at IS NULL", current_time( 'mysql', true ), (int) $user_id ) // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared ); } public static function delete_all( $user_id ) { global $wpdb; return (int) $wpdb->delete( self::table(), array( 'user_id' => (int) $user_id ), array( '%d' ) ); } // ------------------------------------------------------------------- UI public static function menu() { $count = self::unread_count( get_current_user_id() ); $label = __( 'Inbox', 'sirius-press' ); if ( $count > 0 ) { $label .= sprintf( ' %d', $count ); } add_menu_page( __( 'Inbox', 'sirius-press' ), $label, 'read', 'sirius-inbox', array( __CLASS__, 'render_page' ), 'dashicons-email-alt', 72 ); } public static function admin_bar( $bar ) { if ( ! is_user_logged_in() ) { return; } $count = self::unread_count( get_current_user_id() ); $bar->add_node( array( 'id' => 'sirius-inbox', 'title' => '' . ( $count > 0 ? '' . (int) $count . '' : '' ), 'href' => admin_url( 'admin.php?page=sirius-inbox' ), 'meta' => array( 'title' => __( 'Sirius Press inbox', 'sirius-press' ) ), ) ); } public static function render_page() { $user_id = get_current_user_id(); if ( isset( $_POST['sirius_inbox_action'] ) && check_admin_referer( 'sirius_inbox' ) ) { $action = sanitize_key( wp_unslash( $_POST['sirius_inbox_action'] ) ); if ( 'read_all' === $action ) { self::mark_all_read( $user_id ); } elseif ( 'delete_all' === $action ) { self::delete_all( $user_id ); } } $items = self::recent( $user_id, 100 ); echo '

' . esc_html__( 'Inbox', 'sirius-press' ) . '

'; echo '

' . esc_html__( 'Messages this site would have emailed to your account. Nothing here was sent anywhere.', 'sirius-press' ) . '

'; echo '
'; wp_nonce_field( 'sirius_inbox' ); echo ' '; echo ''; echo '
'; if ( ! $items ) { echo '

' . esc_html__( 'Nothing here yet.', 'sirius-press' ) . '

'; return; } echo ''; echo ''; echo ''; echo ''; foreach ( $items as $item ) { $unread = empty( $item->read_at ); printf( '', $unread ? ' style="font-weight:600;background:#fff8e5"' : '', esc_html( mysql2date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $item->created_at ) ), esc_html( $item->source ), esc_html( $item->subject ), wp_kses_post( $item->body ) ); } echo '
' . esc_html__( 'When', 'sirius-press' ) . '' . esc_html__( 'Message', 'sirius-press' ) . '
%s
%s
%s
%s
'; self::mark_all_read( $user_id ); } }