prefix . 'email_log'; } /** * Whether the table is there with the columns this class reads. * * @return bool */ public function available() { return array() === array_diff( self::REQUIRED, $this->columns() ); } /** * The table's column names, or none when there is no such table. * * SHOW COLUMNS rather than SHOW TABLES: it also answers for a temporary table, and it tells a * same-named table from some other plugin apart from the real one. * * @return string[] */ private function columns() { global $wpdb; $table = $this->location(); $suppress = $wpdb->suppress_errors( true ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- a table name, built from $wpdb->prefix, cannot be a placeholder. $columns = $wpdb->get_col( "SHOW COLUMNS FROM `{$table}`" ); $wpdb->suppress_errors( $suppress ); return is_array( $columns ) ? $columns : array(); } /** * Rows above an id, oldest first. * * @param int $after_id Last id already read. * @param int $limit Rows to return at most. * @return array[] */ public function rows( $after_id, $limit ) { global $wpdb; $table = $this->location(); $error = in_array( 'error_message', $this->columns(), true ) ? 'error_message' : "'' AS error_message"; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- the table name and the error column are built here, not taken from input. $found = $wpdb->get_results( $wpdb->prepare( "SELECT id, to_email, subject, message, headers, sent_date, result, {$error} FROM `{$table}` WHERE id > %d ORDER BY id ASC LIMIT %d", (int) $after_id, max( 1, (int) $limit ) ), ARRAY_A ); $rows = array(); foreach ( is_array( $found ) ? $found : array() as $row ) { $rows[] = array( 'id' => (int) $row['id'], 'atts' => array( 'to' => array_values( array_filter( array_map( 'trim', explode( ',', (string) $row['to_email'] ) ), 'strlen' ) ), 'subject' => (string) $row['subject'], 'message' => (string) $row['message'], 'headers' => (string) $row['headers'], 'attachments' => array(), ), 'sent_at' => $this->utc( (string) $row['sent_date'] ), 'status' => $this->status( $row['result'] ), 'error' => (string) $row['error_message'], ); } return $rows; } /** * The site-local sent_date as UTC, or '' when it is not a real date. * * @param string $local 'Y-m-d H:i:s' in the site's time zone. * @return string */ private function utc( $local ) { if ( ! preg_match( '/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $local ) || 0 === strpos( $local, '0000' ) ) { return ''; } $date = date_create_immutable_from_format( 'Y-m-d H:i:s', $local, wp_timezone() ); return false === $date ? '' : $date->setTimezone( new \DateTimeZone( 'UTC' ) )->format( 'Y-m-d H:i:s' ); } /** * The result column as a status. NULL is what a row looks like when the plugin wrote it and * never heard back either way. * * @param mixed $result 1, 0 or NULL. * @return string */ private function status( $result ) { if ( null === $result ) { return 'unconfirmed'; } return 0 === (int) $result ? 'failed' : 'sent'; } }