0 ) { $spoken[] = sprintf( /* translators: %s: number of unread submissions. */ _n( '%s unread', '%s unread', $counts['unread'], 'accessible-forms-by-accessible-org' ), number_format_i18n( $counts['unread'] ) ); } if ( $counts['review'] > 0 ) { $spoken[] = sprintf( /* translators: %s: number of submissions held for review. */ _n( '%s needs review', '%s need review', $counts['review'], 'accessible-forms-by-accessible-org' ), number_format_i18n( $counts['review'] ) ); } $badge = sprintf( ' %3$s', (int) $total, esc_html( number_format_i18n( $total ) ), /* translators: list separator between badge counts, e.g. "3 unread, 2 need review". */ esc_html( implode( _x( ', ', 'badge count separator', 'accessible-forms-by-accessible-org' ), $spoken ) ) ); return $badge; } /** * Show the same badge on the plugin's top-level menu item, so waiting * messages are visible while the menu is collapsed. */ public static function badge_top_level_menu() { global $menu; $badge = self::menu_badge(); if ( '' === $badge || ! is_array( $menu ) ) { return; } foreach ( $menu as $position => $item ) { if ( isset( $item[0], $item[2] ) && 'edit.php?post_type=accessible_form' === $item[2] && false === strpos( $item[0], 'awaiting-mod' ) ) { $menu[ $position ][0] = $item[0] . $badge; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- Appends a count badge to this plugin's own menu item. } } } /** * Markers for the newest failure from the last week: the newest failed * submission and the newest failed notification attempt. Either growing * means a new failure, which brings a dismissed notice back. * * @return array{entry:int, job:int} */ private static function latest_failure() { global $wpdb; // The window of Entries::recent_failure_clause(), written out in full: a recent submission, or a // failed job last tried in the last week (jobs are timed in UTC). // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Plugin-owned tables; must reflect the latest delivery state. $row = $wpdb->get_row( $wpdb->prepare( "SELECT MAX(e.id) AS entry, MAX(n.id) AS job FROM %i e LEFT JOIN %i n ON n.entry_id = e.id AND n.status = 'failed' WHERE e.mail_status = 'failed' AND (e.created_at >= %s OR (e.time_version & 1) = 0 OR e.id IN (SELECT entry_id FROM %i WHERE status = 'failed' AND next_at >= %s))", Entries::table_name(), Notifications::table(), Entries::recent_failure_cutoff(), Notifications::table(), gmdate( 'Y-m-d H:i:s', time() - WEEK_IN_SECONDS ) ), ARRAY_A ); return array( 'entry' => (int) ( $row['entry'] ?? 0 ), 'job' => (int) ( $row['job'] ?? 0 ), ); } /** * Notice about notifications that failed in the last 7 days. Each person * can dismiss it; it returns when another notification fails. */ public static function failed_mail_notice() { if ( ! current_user_can( 'read_accessible_form_entries' ) ) { return; } $failures = Entries::recent_mail_failures(); if ( $failures < 1 ) { return; } $latest = self::latest_failure(); $dismissed = get_user_meta( get_current_user_id(), self::DISMISSED_META . get_current_blog_id(), true ); if ( is_array( $dismissed ) && $latest['entry'] <= (int) ( $dismissed['entry'] ?? 0 ) && $latest['job'] <= (int) ( $dismissed['job'] ?? 0 ) ) { return; } // Not limited by arrival date: a recent failure can belong to an older submission. $review = EntriesTable::url( array( 'mail_status' => 'failed' ) ); $dismiss = wp_nonce_url( add_query_arg( array( 'action' => 'accessible_forms_dismiss_mail_notice', 'entry' => $latest['entry'], 'job' => $latest['job'], ), admin_url( 'admin-post.php' ) ), 'accessible_forms_dismiss_mail_notice' ); echo '

' . esc_html( sprintf( /* translators: %d: number of submissions whose notification failed in the last 7 days. */ _n( 'Across your forms, %d notification email failed to send in the last 7 days.', 'Across your forms, %d notification emails failed to send in the last 7 days.', $failures, 'accessible-forms-by-accessible-org' ), $failures ) ) . ' ' . esc_html__( 'The submissions are stored safely. Open an affected entry and check Notification history for the cause.', 'accessible-forms-by-accessible-org' ) . ' ' . esc_html( current_user_can( 'send_accessible_form_notifications' ) ? __( 'For an entry in Received, fix the cause, then choose “Queue notification again”.', 'accessible-forms-by-accessible-org' ) : __( 'Ask someone with notification access to fix the cause and queue the email again.', 'accessible-forms-by-accessible-org' ) ) . '

' . esc_html__( 'Review affected entries', 'accessible-forms-by-accessible-org' ) . ' '; if ( current_user_can( 'manage_accessible_forms_settings' ) ) { echo '' . esc_html__( 'Email settings', 'accessible-forms-by-accessible-org' ) . ' '; } echo '' . esc_html__( 'Dismiss until another notification fails', 'accessible-forms-by-accessible-org' ) . '

'; } /** * Hide the failed-notification notice for the current user until a newer * failure is recorded. */ public static function dismiss_mail_notice() { check_admin_referer( 'accessible_forms_dismiss_mail_notice' ); if ( ! current_user_can( 'read_accessible_form_entries' ) ) { wp_die( esc_html__( 'You cannot access submissions.', 'accessible-forms-by-accessible-org' ), '', array( 'response' => 403 ) ); } // The notice passes the newest failure it showed, so a failure that // happens in the meantime still brings the notice back. $latest = self::latest_failure(); update_user_meta( get_current_user_id(), self::DISMISSED_META . get_current_blog_id(), array( 'entry' => min( $latest['entry'], isset( $_GET['entry'] ) ? absint( $_GET['entry'] ) : 0 ), 'job' => min( $latest['job'], isset( $_GET['job'] ) ? absint( $_GET['job'] ) : 0 ), ) ); wp_safe_redirect( wp_get_referer() ? wp_get_referer() : EntriesTable::url() ); exit(); } /** * Current list filters, read individually from the allowlisted query args. * * @return array */ public static function current() { $input = array(); // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only, allowlisted list filters; queries enforce permissions. foreach ( array( 'form_id', 'status', 's', 'search', 'date_from', 'date_to', 'workflow', 'mail_status', 'assignee', 'paged', 'orderby', 'order' ) as $key ) { if ( isset( $_GET[ $key ] ) && is_scalar( $_GET[ $key ] ) ) { $input[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $key ] ) ); } } // phpcs:enable WordPress.Security.NonceVerification.Recommended return self::filters( $input ); } /** * Allowlisted, bounded list filters. * * @param array $input Raw filters. * @return array */ public static function filters( array $input ) { $input = array_filter( $input, 'is_scalar' ); $out = array( 'form_id' => absint( $input['form_id'] ?? 0 ) ); if ( isset( $input['paged'] ) ) { $out['paged'] = max( 1, min( 100000, absint( $input['paged'] ) ) ); } if ( isset( $input['orderby'] ) ) { $out['orderby'] = in_array( $input['orderby'], array( 'created_at', 'id' ), true ) ? $input['orderby'] : 'created_at'; } if ( isset( $input['order'] ) ) { $out['order'] = 'asc' === strtolower( (string) $input['order'] ) ? 'asc' : 'desc'; } foreach ( array( 'status', 'workflow', 'mail_status', 'assignee', 'date_from', 'date_to' ) as $key ) { if ( isset( $input[ $key ] ) && is_scalar( $input[ $key ] ) ) { $out[ $key ] = substr( sanitize_text_field( (string) $input[ $key ] ), 0, 30 ); } } $out['search'] = FormOptions::text( $input['search'] ?? ( $input['s'] ?? '' ), 200 ); return $out; } /** Add-on tools shown above the entries list, such as saved views and exports. */ public static function render() { Extensions::notify( 'accessible_forms_entries_tools', self::current() ); } public static function url_filters( array $filters ) { $filters['s'] = $filters['search'] ?? ''; unset( $filters['search'] ); return $filters; } }