hook = (string) add_management_page( __( 'Email Log', 'adminkeep' ), __( 'Email Log', 'adminkeep' ), self::CAPABILITY, self::PAGE, array( $this, 'render' ) ); // The preview document is this screen's own URL with ?preview=, answered before the admin // header prints. load-{hook} fires only for this page and only once the user may see it. if ( '' !== $this->hook ) { add_action( 'load-' . $this->hook, array( $this, 'maybe_preview' ) ); add_action( 'load-' . $this->hook, array( $this, 'register_columns' ) ); } } /** * Screen Options: a show/hide checkbox for every column but Subject, with From off until * someone turns it on. WordPress builds the panel before the table exists, from these filters; * it stores and applies each user's choice itself. * * @return void */ public function register_columns() { $screen = get_current_screen(); if ( null === $screen ) { return; } add_filter( 'manage_' . $screen->id . '_columns', static function () { // Subject opens the preview and is what stays on a narrow screen, so it is not // offered for hiding at all. return array_diff_key( EmailLogTable::columns(), array( 'subject' => true ) ); } ); add_filter( 'default_hidden_columns', static function ( $hidden, $for ) use ( $screen ) { return $for->id === $screen->id ? array( 'from_email' ) : $hidden; }, 10, 2 ); } /** * The badge and modal styles and the modal script. Scoped to this screen's hook suffix, the * same way SmtpScreen scopes its own. * * @param string $hook_suffix Current admin page. * @return void */ public function enqueue_assets( $hook_suffix ) { if ( '' === $this->hook || $hook_suffix !== $this->hook ) { return; } wp_enqueue_style( 'adminkeep-email-log', ADMINKEEP_URL . 'assets/css/email-log.css', array( 'dashicons' ), \Adminkeep\Assets::version( 'assets/css/email-log.css' ) ); wp_enqueue_script( 'adminkeep-email-log', ADMINKEEP_URL . 'assets/js/email-log.js', array(), \Adminkeep\Assets::version( 'assets/js/email-log.js' ), true ); } /** * House shape: marker field, capability, nonce, work, redirect. * * @return void */ public function handle_action() { if ( ! isset( $_POST[ self::ACTION_FIELD ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Checked below, after the cheap bail. return; } if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to manage the email log.', 'adminkeep' ), '', 403 ); } check_admin_referer( self::NONCE, self::NONCE_FIELD ); $action = sanitize_key( wp_unslash( (string) $_POST[ self::ACTION_FIELD ] ) ); $deleted = 0; if ( 'import' === $action ) { $this->handle_import(); } // The list table posts its dropdown as `action` (top) or `action2` (bottom). "Apply" with // nothing chosen posts -1 in both, and must delete nothing. $bulk = array( isset( $_POST['action'] ) ? sanitize_key( wp_unslash( (string) $_POST['action'] ) ) : '', isset( $_POST['action2'] ) ? sanitize_key( wp_unslash( (string) $_POST['action2'] ) ) : '', ); if ( 'delete_all' === $action ) { $deleted = EmailLogStore::delete_all(); } elseif ( 'delete' === $action && in_array( 'delete', $bulk, true ) && isset( $_POST['email_log'] ) && is_array( $_POST['email_log'] ) ) { $deleted = EmailLogStore::delete( array_map( 'absint', wp_unslash( $_POST['email_log'] ) ) ); } wp_safe_redirect( add_query_arg( self::DONE_ARG, $deleted, self::url() ) ); exit; } /** * Run the import the button asks for, then redirect with its counts. Nothing is imported * unless the button would be showing: a stale form posted after the Email Log plugin was * deactivated just comes back to the log. * * @return void */ private function handle_import() { $source = $this->import_source(); if ( null === $source ) { wp_safe_redirect( self::url() ); exit; } $result = ( new EmailLogImporter() )->run( $source, false ); $counts = array( $result['imported'], $result['skipped_old'], $result['left_full'], $result['failed'] ); wp_safe_redirect( add_query_arg( self::IMPORT_ARG, implode( '-', $counts ), self::url() ) ); exit; } /** * Whether the Email Log plugin (wordpress.org/plugins/email-log) is running on this site. Its * main file defines EMAIL_LOG_FILE, which holds whatever folder the plugin was installed in. * Its own method so a test can say yes: a real constant could never be undefined again. * * @return bool */ protected function email_log_plugin_active() { return defined( 'EMAIL_LOG_FILE' ); } /** * The source the button copies from, or null when there should be no button: the Email Log * plugin is not active, or its table is missing or not the shape the importer reads. * * @return Source|null */ private function import_source() { if ( ! $this->email_log_plugin_active() ) { return null; } $sources = EmailLogImporter::sources(); $source = $sources[ self::IMPORT_SOURCE ]; return $source->available() ? $source : null; } /** * Serve one email's preview document, when asked for one. * * A plain page load with a nonce, not an AJAX action or a REST route: the log keeps its single * guarded way in. Capability first, then the nonce, then the row. * * @return void */ public function maybe_preview() { if ( ! isset( $_GET['preview'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Checked below, after the cheap bail. return; } if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to view the email log.', 'adminkeep' ), '', 403 ); } $nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( (string) $_GET['_wpnonce'] ) ) : ''; if ( ! wp_verify_nonce( $nonce, EmailLogPreview::NONCE ) ) { wp_die( esc_html__( 'This preview link has expired. Reload the email log and try again.', 'adminkeep' ), '', 403 ); } $row = EmailLogStore::get( absint( $_GET['preview'] ) ); if ( null === $row ) { wp_die( esc_html__( 'That email is no longer in the log.', 'adminkeep' ), '', 404 ); } $view = isset( $_GET['view'] ) && in_array( $_GET['view'], array( 'text', 'source' ), true ) ? ( 'text' === $_GET['view'] ? 'text' : 'source' ) : 'preview'; $this->emit( EmailLogPreview::headers(), EmailLogPreview::document( $row, $view ) ); } /** * Send the document and stop. Its own method so tests can capture it instead of exiting. * * @param string[] $headers Header lines. * @param string $body Document. * @return void */ protected function emit( array $headers, $body ) { nocache_headers(); foreach ( $headers as $header ) { header( $header ); } echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- A standalone document under a CSP sandbox; EmailLogPreview::document() escapes everything that is not the logged HTML itself. exit; } /** * Render the screen. * * @return void */ public function render() { if ( ! current_user_can( self::CAPABILITY ) ) { wp_die( esc_html__( 'Sorry, you are not allowed to view the email log.', 'adminkeep' ), '', 403 ); } $table = new EmailLogTable(); $table->prepare_items(); echo '
' . esc_html( sprintf( _n( '%d entry deleted.', '%d entries deleted.', $count, 'adminkeep' ), $count ) ) . '
' . esc_html__( 'The Email Log plugin is active.', 'adminkeep' ) . ' '; echo esc_html( sprintf( /* translators: %d: number of days the email log keeps an entry. */ _n( 'Copy its log into this one. Emails older than %d day are skipped, as this log would already have deleted them, and sign-in links are removed. Running it again copies only new emails. The Email Log plugin\'s own log is not changed.', 'Copy its log into this one. Emails older than %d days are skipped, as this log would already have deleted them, and sign-in links are removed. Running it again copies only new emails. The Email Log plugin\'s own log is not changed.', $retention, 'adminkeep' ), $retention ) ); echo '
'; echo '' . esc_html( implode( ' ', $parts ) ) . '