PluginProbe
Email Log / trunk
Email Log vtrunk
2.63 2.4.8 2.4.9 2.6 2.61 2.62 trunk 0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.8.1 0.9 0.9.1 0.9.2 1.1 1.5 1.5.1 1.5.2 1.5.3 1.5.4 All 61 releases
email-log / include / Core / DB / TableManager.php

TableManager.php in Email Log trunk, at include/Core/DB/TableManager.php

582 lines 16.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php namespace EmailLog\Core\DB;
2
3 /**
4 * Handle installation and db table creation.
5 */
6
7 use EmailLog\Core\Loadie;
8 use EmailLog\Util;
9
10 defined( 'ABSPATH' ) || exit; // Exit if accessed directly.
11
12 /**
13 * Helper class to create table.
14 *
15 * @since 2.0.0
16 */
17 class TableManager implements Loadie {
18
19 /* Database table name */
20 const LOG_TABLE_NAME = 'email_log';
21
22 /* Database option name */
23 const DB_OPTION_NAME = 'email-log-db';
24
25 /* Database version */
26 const DB_VERSION = '0.3';
27
28 /**
29 * Setup hooks.
30 */
31 public function load() {
32 add_action( 'wpmu_new_blog', array( $this, 'create_table_for_new_blog' ) );
33
34 add_filter( 'wpmu_drop_tables', array( $this, 'delete_table_from_deleted_blog' ) );
35
36 // Do any DB upgrades.
37 $this->update_table_if_needed();
38 }
39
40 /**
41 * On plugin activation, create table if needed.
42 *
43 * @param bool $network_wide True if the plugin was network activated.
44 */
45 public function on_activate( $network_wide ) {
46 if ( is_multisite() && $network_wide ) {
47 // Note: if there are more than 10,000 blogs or
48 // if `wp_is_large_network` filter is set, then this may fail.
49 $sites = get_sites();
50
51 foreach ( $sites as $site ) {
52 switch_to_blog( $site->blog_id );
53 $this->create_table_if_needed();
54 restore_current_blog();
55 }
56 } else {
57 $this->create_table_if_needed();
58 }
59 }
60
61 /**
62 * Create email log table when a new blog is created.
63 *
64 * @param int $blog_id Blog Id.
65 */
66 public function create_table_for_new_blog( $blog_id ) {
67 if ( is_plugin_active_for_network( 'email-log/email-log.php' ) ) {
68 switch_to_blog( $blog_id );
69 $this->create_table_if_needed();
70 restore_current_blog();
71 }
72 }
73
74 /**
75 * Add email log table to the list of tables deleted when a blog is deleted.
76 *
77 * @param array $tables List of tables to be deleted.
78 *
79 * @return string[] $tables Modified list of tables to be deleted.
80 */
81 public function delete_table_from_deleted_blog( $tables ) {
82 $tables[] = $this->get_log_table_name();
83
84 return $tables;
85 }
86
87 /**
88 * Get email log table name.
89 *
90 * @return string Email Log Table name.
91 */
92 public function get_log_table_name() {
93 global $wpdb;
94
95 return $wpdb->prefix . self::LOG_TABLE_NAME;
96 }
97
98 /**
99 * Insert log data into DB.
100 *
101 * @param array $data Data to be inserted.
102 */
103 public function insert_log( $data ) {
104 global $wpdb;
105
106 $table_name = $this->get_log_table_name();
107 $wpdb->insert( $table_name, $data ); //phpcs:ignore
108 }
109
110 /**
111 * Delete log entries by ids.
112 *
113 * @param string $ids Comma separated list of log ids.
114 *
115 * @return false|int Number of log entries that got deleted. False on failure.
116 */
117 public function delete_logs( $ids ) {
118 global $wpdb;
119
120 $table_name = $this->get_log_table_name();
121
122 // Can't use wpdb->prepare for the below query. If used it results in this bug // https://github.com/sudar/email-log/issues/13.
123 $ids = esc_sql( $ids );
124
125 return $wpdb->query( "DELETE FROM {$table_name} where id IN ( {$ids} )" ); //phpcs:ignore
126 }
127
128 /**
129 * Delete all log entries.
130 *
131 * @return false|int Number of log entries that got deleted. False on failure.
132 */
133 public function delete_all_logs() {
134 global $wpdb;
135
136 $table_name = $this->get_log_table_name();
137
138 return $wpdb->query( "DELETE FROM {$table_name}" ); //@codingStandardsIgnoreLine
139 }
140
141 /**
142 * Deletes Email Logs older than the specified interval.
143 *
144 * @param int $interval_in_days No. of days beyond which logs are to be deleted.
145 *
146 * @return int $deleted_rows_count Count of rows deleted.
147 */
148 public function delete_logs_older_than( $interval_in_days ) {
149 global $wpdb;
150 $table_name = $this->get_log_table_name();
151
152 $deleted_rows_count = $wpdb->query( $wpdb->prepare( "DELETE FROM {$table_name} WHERE sent_date < DATE_SUB( CURDATE(), INTERVAL %d DAY )", $interval_in_days ) ); //phpcs:ignore
153
154 return $deleted_rows_count;
155 }
156
157 /**
158 * Fetch log item by ID.
159 *
160 * @param array $ids Optional. Array of IDs of the log items to be retrieved.
161 * @param array $additional_args {
162 * Optional. Array of additional args.
163 *
164 * @type string $date_column_format MySQL date column format. Refer
165 *
166 * @link https://dev.mysql.com/doc/refman/5.5/en/date-and-time-functions.html#function_date-format
167 * }
168 *
169 * @return array Log item(s).
170 */
171 public function fetch_log_items_by_id( $ids = array(), $additional_args = array() ) {
172 global $wpdb;
173 $table_name = $this->get_log_table_name();
174
175 $query = "SELECT * FROM {$table_name}";
176
177 // When `date_column_format` exists, should replace the `$query` var.
178 $date_column_format_key = 'date_column_format';
179 if ( array_key_exists( $date_column_format_key, $additional_args ) && ! empty( $additional_args[ $date_column_format_key ] ) ) {
180 $query = "SELECT DATE_FORMAT(sent_date, \"{$additional_args[ $date_column_format_key ]}\") as sent_date_custom, el.* FROM {$table_name} as el";
181 }
182
183 if ( ! empty( $ids ) ) {
184 $ids = array_map( 'absint', $ids );
185
186 // Can't use wpdb->prepare for the below query. If used it results in this bug https://github.com/sudar/email-log/issues/13.
187 $ids_list = esc_sql( implode( ',', $ids ) );
188
189 $query .= " where id IN ( {$ids_list} )";
190 }
191
192 return $wpdb->get_results( $query, 'ARRAY_A' ); //@codingStandardsIgnoreLine
193 }
194
195 /**
196 * Fetch log items.
197 *
198 * @since 2.3.0 Implemented Advanced Search. Search queries could look like the following.
199 * Example:
200 * id: 2
201 * to: test@test.com
202 *
203 * @param array $request Request object.
204 * @param int $per_page Entries per page.
205 * @param int $current_page_no Current page no.
206 *
207 * @return array Log entries and total items count.
208 */
209 public function fetch_log_items( $request, $per_page, $current_page_no ) {
210 global $wpdb;
211 $table_name = $this->get_log_table_name();
212
213 $query = 'SELECT * FROM ' . $table_name;
214 $count_query = 'SELECT count(*) FROM ' . $table_name;
215 $query_cond = '';
216
217 if ( isset( $request['s'] ) && is_string( $request['s'] ) && $request['s'] !== '' ) {
218 $search_term = trim( esc_sql( $request['s'] ) );
219
220 if ( Util\is_advanced_search_term( $search_term ) ) {
221 $predicates = Util\get_advanced_search_term_predicates( $search_term );
222
223 foreach ( $predicates as $column => $email ) {
224 switch ( $column ) {
225 case 'id':
226 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
227 $query_cond .= "id = '$email'";
228 break;
229 case 'to':
230 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
231 $query_cond .= "to_email LIKE '%$email%'";
232 break;
233 case 'email':
234 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
235 $query_cond .= ' ( '; /* Begin 1st */
236 $query_cond .= " ( to_email LIKE '%$email%' OR subject LIKE '%$email%' ) "; /* Begin 2nd & End 2nd */
237 $query_cond .= ' OR ';
238 $query_cond .= ' ( '; /* Begin 3rd */
239 $query_cond .= "headers <> ''";
240 $query_cond .= ' AND ';
241 $query_cond .= ' ( '; /* Begin 4th */
242 $query_cond .= "headers REGEXP '[F|f]rom:.*$email' OR ";
243 $query_cond .= "headers REGEXP '[CC|Cc|cc]:.*$email' OR ";
244 $query_cond .= "headers REGEXP '[BCC|Bcc|bcc]:.*$email' OR ";
245 $query_cond .= "headers REGEXP '[R|r]eply-[T|t]o:.*$email'";
246 $query_cond .= ' ) '; /* End 4th */
247 $query_cond .= ' ) '; /* End 3rd */
248 $query_cond .= ' ) '; /* End 1st */
249 break;
250 case 'cc':
251 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
252 $query_cond .= ' ( '; /* Begin 1st */
253 $query_cond .= "headers <> ''";
254 $query_cond .= ' AND ';
255 $query_cond .= ' ( '; /* Begin 2nd */
256 $query_cond .= "headers REGEXP '[CC|Cc|cc]:.*$email' ";
257 $query_cond .= ' ) '; /* End 2nd */
258 $query_cond .= ' ) '; /* End 1st */
259 break;
260 case 'bcc':
261 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
262 $query_cond .= ' ( '; /* Begin 1st */
263 $query_cond .= "headers <> ''";
264 $query_cond .= ' AND ';
265 $query_cond .= ' ( '; /* Begin 2nd */
266 $query_cond .= "headers REGEXP '[BCC|Bcc|bcc]:.*$email' ";
267 $query_cond .= ' ) '; /* End 2nd */
268 $query_cond .= ' ) '; /* End 1st */
269 break;
270 case 'reply-to':
271 $query_cond .= empty( $query_cond ) ? ' WHERE ' : ' AND ';
272 $query_cond .= ' ( '; /* Begin 1st */
273 $query_cond .= "headers <> ''";
274 $query_cond .= ' AND ';
275 $query_cond .= ' ( '; /* Begin 2nd */
276 $query_cond .= "headers REGEXP '[R|r]eply-to:.*$email' ";
277 $query_cond .= ' ) '; /* End 2nd */
278 $query_cond .= ' ) '; /* End 1st */
279 break;
280 }
281 }
282 } else {
283 $query_cond .= " WHERE ( to_email LIKE '%$search_term%' OR subject LIKE '%$search_term%' ) ";
284 }
285 }
286
287 if ( isset( $request['d'] ) && $request['d'] !== '' ) {
288 $search_date = trim( esc_sql( $request['d'] ) );
289 if ( '' === $query_cond ) {
290 $query_cond .= " WHERE sent_date BETWEEN '$search_date 00:00:00' AND '$search_date 23:59:59' ";
291 } else {
292 $query_cond .= " AND sent_date BETWEEN '$search_date 00:00:00' AND '$search_date 23:59:59' ";
293 }
294 }
295
296 // Ordering parameters.
297 $order_by = 'sent_date';
298 $order = 'DESC';
299
300 $allowed_order_by = [
301 'sent_date',
302 'to_email',
303 'subject',
304 ];
305
306 $sanitized_order_by = ( ! empty( $request['orderby'] ) ) ? sanitize_text_field( $request['orderby'] ) : '';
307 if ( ! empty( $sanitized_order_by ) && in_array( $sanitized_order_by, $allowed_order_by, true ) ) {
308 $order_by = $sanitized_order_by;
309 }
310
311 if ( ! empty( $request['order'] ) && 'asc' === strtolower( sanitize_text_field( $request['order'] ) ) ) {
312 $order = 'ASC';
313 }
314
315 if ( ! empty( $order_by ) & ! empty( $order ) ) {
316 $query_cond .= ' ORDER BY ' . $order_by . ' ' . $order;
317 }
318
319 // Find total number of items.
320 $count_query = $count_query . $query_cond;
321 $total_items = $wpdb->get_var( $count_query ); //phpcs:ignore
322
323 // Adjust the query to take pagination into account.
324 if ( ! empty( $current_page_no ) && ! empty( $per_page ) ) {
325 $offset = ( $current_page_no - 1 ) * $per_page;
326 $query_cond .= ' LIMIT ' . (int) $offset . ',' . (int) $per_page;
327 }
328
329 // Fetch the items.
330 $query = $query . $query_cond;
331 $items = $wpdb->get_results( $query ); //phpcs:ignore
332
333 return array( $items, $total_items );
334 }
335
336 /**
337 * Create email log table.
338 *
339 * @global object $wpdb
340 */
341 public function create_table_if_needed() {
342 global $wpdb;
343
344 $table_name = $this->get_log_table_name();
345
346 if ( $wpdb->get_var( "show tables like '{$table_name}'" ) != $table_name ) { //phpcs:ignore
347
348 $sql = $this->get_create_table_query();
349
350 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
351 dbDelta( $sql );
352
353 add_option( self::DB_OPTION_NAME, self::DB_VERSION );
354 }
355 }
356
357 /**
358 * Get the total number of email logs.
359 *
360 * @return int Total email log count
361 */
362 public function get_logs_count() {
363 global $wpdb;
364
365 return $wpdb->get_var( 'SELECT count(*) FROM ' . $this->get_log_table_name() ); //phpcs:ignore
366 }
367
368 /**
369 * Fetches the log id by item data.
370 *
371 * Use this method to get the log item id when the error instance only returns the log item data.
372 *
373 * @param array $data Array of Email information {
374 * @type array|string to
375 * @type string subject
376 * @type string message
377 * @type array|string headers
378 * @type array|string attachments
379 * }
380 *
381 * @return int Log item id.
382 */
383 public function fetch_log_id_by_data( $data ) {
384 if ( empty( $data ) || ! is_array( $data ) ) {
385 return 0;
386 }
387
388 global $wpdb;
389 $table_name = $this->get_log_table_name();
390
391 $query = "SELECT ID FROM {$table_name}";
392 $query_cond = '';
393 $where = array();
394
395 // Execute the following `if` conditions only when $data is array.
396 if ( array_key_exists( 'to', $data ) ) {
397 // Since the value is stored as CSV in DB, convert the values from error data to CSV to compare.
398 $to_email = Util\stringify( $data['to'] );
399
400 $to_email = trim( esc_sql( $to_email ) );
401 $where[] = "to_email = '$to_email'";
402 }
403
404 if ( array_key_exists( 'subject', $data ) ) {
405 $subject = trim( esc_sql( $data['subject'] ) );
406 $where[] = "subject = '$subject'";
407 }
408
409 if ( array_key_exists( 'attachments', $data ) ) {
410 if ( is_array( $data['attachments'] ) ) {
411 $attachments = count( $data['attachments'] ) > 0 ? 'true' : 'false';
412 } else {
413 $attachments = empty( $data['attachments'] ) ? 'false' : 'true';
414 }
415 $attachments = trim( esc_sql( $attachments ) );
416 $where[] = "attachments = '$attachments'";
417 }
418
419 foreach ( $where as $index => $value ) {
420 $query_cond .= 0 === $index ? ' WHERE ' : ' AND ';
421 $query_cond .= $value;
422 }
423
424 // Get only the latest logged item when multiple rows match.
425 $query_cond .= ' ORDER BY id DESC LIMIT 1';
426
427 $query = $query . $query_cond;
428
429 return absint( $wpdb->get_var( $query ) ); //phpcs:ignore
430 }
431
432 /**
433 * Sets email sent status and error message for the given log item when email fails.
434 *
435 * @param int $log_item_id ID of the log item whose email sent status should be set to failed.
436 * @param string $message Error message.
437 *
438 * @since 2.4.0 Include error message during update.
439 * @since 2.3.0
440 *
441 * @global \wpdb $wpdb
442 *
443 * @see TableManager::get_log_table_name()
444 */
445 public function mark_log_as_failed( $log_item_id, $message ) {
446 global $wpdb;
447 $table_name = $this->get_log_table_name();
448
449 $wpdb->update( //phpcs:ignore
450 $table_name,
451 array(
452 'result' => '0',
453 'error_message' => $message,
454 ),
455 array( 'ID' => $log_item_id ),
456 array(
457 '%d', // `result` format.
458 '%s', // `error_message` format.
459 ),
460 array(
461 '%d', // `ID` format.
462 )
463 );
464 }
465
466 /**
467 * Updates the DB schema.
468 *
469 * Adds new columns to the Database as of v0.2.
470 *
471 * @since 2.3.0
472 */
473 private function update_table_if_needed() {
474 if ( get_option( self::DB_OPTION_NAME, false ) === self::DB_VERSION ) {
475 return;
476 }
477
478 $sql = $this->get_create_table_query();
479
480 require_once ABSPATH . 'wp-admin/includes/upgrade.php';
481 dbDelta( $sql );
482
483 update_option( self::DB_OPTION_NAME, self::DB_VERSION );
484 }
485
486 /**
487 * Gets the Create Table query.
488 *
489 * @since 2.4.0 Added error_message column.
490 * @since 2.3.0
491 *
492 * @return string
493 */
494 private function get_create_table_query() {
495 global $wpdb;
496 $table_name = $this->get_log_table_name();
497 $charset_collate = $wpdb->get_charset_collate();
498
499 $sql = 'CREATE TABLE ' . $table_name . ' (
500 id mediumint(9) NOT NULL AUTO_INCREMENT,
501 to_email VARCHAR(500) NOT NULL,
502 subject VARCHAR(500) NOT NULL,
503 message TEXT NOT NULL,
504 headers TEXT NOT NULL,
505 attachments TEXT NOT NULL,
506 sent_date timestamp NOT NULL,
507 attachment_name VARCHAR(1000),
508 ip_address VARCHAR(15),
509 result TINYINT(1),
510 error_message VARCHAR(1000),
511 PRIMARY KEY (id)
512 ) ' . $charset_collate . ';';
513
514 return $sql;
515 }
516
517 /**
518 * Callback for the Array filter.
519 *
520 * @since 2.3.0
521 *
522 * @param string $column A column from the array Columns.
523 *
524 * @return bool
525 */
526 private function validate_columns( $column ) {
527 return in_array( $column, array( 'to' ), true );
528 }
529
530 /**
531 * Query log items by column.
532 *
533 * @since 2.3.0
534 *
535 * @param array $columns Key value pair based on which items should be retrieved.
536 *
537 * @uses \EmailLog\Core\DB\TableManager::validate_columns()
538 *
539 * @return array|object|null
540 */
541 public function query_log_items_by_column( $columns ) {
542 if ( ! is_array( $columns ) ) {
543 return;
544 }
545
546 // Since we support PHP v5.2.4, we cannot use ARRAY_FILTER_USE_KEY
547 // TODO: PHP v5.5: Once WordPress updates minimum PHP version to v5.5, start using ARRAY_FILTER_USE_KEY.
548 $columns_keys = array_keys( $columns );
549 if ( ! array_filter( $columns_keys, array( $this, 'validate_columns' ) ) ) {
550 return;
551 }
552
553 global $wpdb;
554
555 $table_name = $this->get_log_table_name();
556 $query = "SELECT id, sent_date, to_email, subject FROM {$table_name}";
557 $query_cond = '';
558 $where = array();
559
560 // Execute the following `if` conditions only when $data is array.
561 if ( array_key_exists( 'to', $columns ) ) {
562 // Since the value is stored as CSV in DB, convert the values from error data to CSV to compare.
563 $to_email = Util\stringify( $columns['to'] );
564
565 $to_email = trim( esc_sql( $to_email ) );
566 $where[] = "to_email = '$to_email'";
567
568 foreach ( $where as $index => $value ) {
569 $query_cond .= 0 === $index ? ' WHERE ' : ' AND ';
570 $query_cond .= $value;
571 }
572
573 // Get only the latest logged item when multiple rows match.
574 $query_cond .= ' ORDER BY id DESC';
575
576 $query = $query . $query_cond;
577
578 return $wpdb->get_results( $query ); //phpcs:ignore
579 }
580 }
581 }
582