Headline
CVE-2023-3092: data-list-table.php in smtp-mail/trunk/includes – WordPress Plugin Repository
The SMTP Mail plugin for WordPress is vulnerable to Stored Cross-Site Scripting via an email subject in versions up to, and including, 1.2.16 due to insufficient input sanitization and output escaping when the ‘Save Data SendMail’ feature is enabled. This makes it possible for unauthenticated attackers to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
1<?php2defined(‘ABSPATH’) or die;34/*************************** LOAD THE BASE CLASS *******************************5 *******************************************************************************6 * The WP_List_Table class isn’t automatically available to plugins, so we need7 * to check if it’s available and load it if necessary. In this tutorial, we are8 * going to use the WP_List_Table class directly from WordPress core.9 *10 * IMPORTANT:11 * Please note that the WP_List_Table class technically isn’t an official API,12 * and it could change at some point in the distant future. Should that happen,13 * I will update this plugin with the most current techniques for your reference14 * immediately.15 *16 * If you are really worried about future compatibility, you can make a copy of17 * the WP_List_Table class (file path is shown just below) to use and distribute18 * with your plugins. If you do that, just remember to change the name of the19 * class to avoid conflicts with core.20 *21 * Since I will be keeping this tutorial up-to-date for the foreseeable future,22 * I am going to work with the copy of the class provided in WordPress core.23 */24if(!class_exists(‘WP_List_Table’)){25 require_once( ABSPATH . ‘wp-admin/includes/class-wp-list-table.php’ );26}272829/************************** CREATE A PACKAGE CLASS *****************************30 *******************************************************************************31 * Create a new list table package that extends the core WP_Users_List_Table class.32 * WP_Users_List_Table contains most of the framework for generating the table, but we33 * need to define and override some methods so that our data can be displayed34 * exactly the way we need it to be.35 * 36 * To display this example on a page, you will first need to instantiate the class,37 * then call $yourInstance->prepare_items() to handle any data manipulation, then38 * finally call $yourInstance->display() to render the table to the page.39 * 40 * Our theme for this list table is going to be movies.41 */42class SMTPMail_Data_List_Table extends WP_List_Table {4344 /** ************************************************************************45 * REQUIRED. Set up a constructor that references the parent constructor. We 46 * use the parent reference to set some default configs.47 ***************************************************************************/48 function __construct(){49 global $status, $page;50 51 //Set parent defaults52 parent::__construct( array(53 ‘singular’ => 'customer’, //singular name of the listed records54 ‘plural’ => 'customers’, //plural name of the listed records55 ‘ajax’ => false //does this table support ajax?56 ) );57 }58 59 /** ************************************************************************60 * Recommended. This method is called when the parent class can’t find a method61 * specifically build for a given column. Generally, it’s recommended to include62 * one method for each column you want to render, keeping your package class63 * neat and organized. For example, if the class needs to process a column64 * named 'title’, it would first see if a method named $this->column_title() 65 * exists - if it does, that method will be used. If it doesn’t, this one will66 * be used. Generally, you should try to use custom column methods as much as 67 * possible. 68 * 69 * Since we have defined a column_title() method later on, this method doesn’t70 * need to concern itself with any column with a name of 'title’. Instead, it71 * needs to handle everything else.72 * 73 * For more detailed insight into how columns are handled, take a look at 74 * WP_List_Table::single_row_columns()75 * 76 * @param array $item A singular item (one full row’s worth of data)77 * @param array $column_name The name/slug of the column to be processed78 * @return string Text or HTML to be placed inside the column <td>79 **************************************************************************/80 function column_default($item, $column_name){81 switch($column_name){82 case 'id’:83 return (int) $item[$column_name];84 case 'status’:85 return ( intval($item[$column_name]) == 1 ? ‘Success’ : ‘Fail’ );86 case 'message’:87 return wp_trim_words( $item[$column_name], 30 ); 88 case 'location’:89 // $params = json_decode($item[‘params’]);90 // if( $params && is_object($params) && isset($params->ip) ) {91 // return '<a href="’. esc_url( 'http://whois.photoboxone.com/location/’. $params->ip ) .’" target="_blank" rel="help">’. __('View Map’, ‘smtp-mail’) .’</a>’;92 // }93 return '’;94 case 'subject’:95 return $this->column_title($item);96 default:97 return $item[$column_name]; //Show the whole array for troubleshooting purposes98 }99 }100101102 /** ************************************************************************103 * Recommended. This is a custom column method and is responsible for what104 * is rendered in any column with a name/slug of ‘title’. Every time the class105 * needs to render a column, it first looks for a method named 106 * column_{$column_title} - if it exists, that method is run. If it doesn’t107 * exist, column_default() is called instead.108 * 109 * This example also illustrates how to implement rollover actions. Actions110 * should be an associative array formatted as ‘slug’=>’link html’ - and you111 * will need to generate the URLs yourself. You could even ensure the links112 * 113 * 114 * @see WP_List_Table::::single_row_columns()115 * @param array $item A singular item (one full row’s worth of data)116 * @return string Text to be placed inside the column <td> (movie title only)117 **************************************************************************/118 function column_title($item){119 $page = sanitize_text_field( isset($_REQUEST[‘page’])?$_REQUEST[‘page’]:’’ );120 121 //Build row actions122 $actions = array(123 ‘detail’ => '<a href="’. esc_url( admin_url('options-general.php?page=’.$page.’&tab=list&action=detail&code=’.intval($item[‘id’]) ) ).’">Detail</a>’,124 ‘delete’ => '<a href="’. esc_url( admin_url('options-general.php?page=’.$page.’&tab=list&action=delete&code=’.intval($item[‘id’]) ) ).’">Delete</a>’,125 );126 127 //Return the title contents128 return sprintf('%1$s %2$s’,129 /*$1%s*/ $item[‘subject’],130 /*$2%s*/ $this->row_actions($actions)131 );132 }133134135 /** ************************************************************************136 * REQUIRED if displaying checkboxes or using bulk actions! The ‘cb’ column137 * is given special treatment when columns are processed. It ALWAYS needs to138 * have it’s own method.139 * 140 * @see WP_List_Table::::single_row_columns()141 * @param array $item A singular item (one full row’s worth of data)142 * @return string Text to be placed inside the column <td> (movie title only)143 **************************************************************************/144 function column_cb($item){145 return sprintf(146 '<input type="checkbox" name="%1$s[]" value="%2$s" />’,147 /*$1%s*/ $this->_args[‘singular’], //Let’s simply repurpose the table’s singular label (“movie”)148 /*$2%s*/ $item[‘id’] //The value of the checkbox should be the record’s id149 );150 }151152153 /** ************************************************************************154 * REQUIRED! This method dictates the table’s columns and titles. This should155 * return an array where the key is the column slug (and class) and the value 156 * is the column’s title text. If you need a checkbox for bulk actions, refer157 * to the $columns array below.158 * 159 * The ‘cb’ column is treated differently than the rest. If including a checkbox160 * column in your table you must create a column_cb() method. If you don’t need161 * bulk actions or checkboxes, simply leave the ‘cb’ entry out of your array.162 * 163 * @see WP_List_Table::::single_row_columns()164 * @return array An associative array containing column information: 'slugs’=>’Visible Titles’165 **************************************************************************/166 function get_columns(){167 $columns = array(168 ‘cb’ => '<input type="checkbox" />’, //Render a checkbox instead of text169 ‘id’ => 'ID’,170 ‘subject’ => 'Subject’,171 ‘from_email’ => 'From’,172 ‘to_email’ => ‘To’,173 //’to_name’ => 'Name’,174 ‘status’ => 'Status’, 175 ‘message’ => 'Message’,176 ‘created’ => 'Created’,177 ‘location’ => 'Location’,178 );179 return $columns;180 }181182183 /** ************************************************************************184 * Optional. If you want one or more columns to be sortable (ASC/DESC toggle), 185 * you will need to register it here. This should return an array where the 186 * key is the column that needs to be sortable, and the value is db column to 187 * sort by. Often, the key and value will be the same, but this is not always188 * the case (as the value is a column name from the database, not the list table).189 * 190 * This method merely defines which columns should be sortable and makes them191 * clickable - it does not handle the actual sorting. You still need to detect192 * the ORDERBY and ORDER querystring variables within prepare_items() and sort193 * your data accordingly (usually by modifying your query).194 * 195 * @return array An associative array containing all the columns that should be sortable: 'slugs’=>array('data_values’,bool)196 **************************************************************************/197 function get_sortable_columns() {198 //true means it’s already sorted199 $sortable_columns = array(200 ‘id’ => array('id’,false),201 ‘subject’ => array('subject’,false),202 ‘created’ => array('created’,false)203 );204205 return $sortable_columns;206 }207208209 /** ************************************************************************210 * Optional. If you need to include bulk actions in your list table, this is211 * the place to define them. Bulk actions are an associative array in the format212 * 'slug’=>’Visible Title’213 * 214 * If this method returns an empty value, no bulk action will be rendered. If215 * you specify any bulk actions, the bulk actions box will be rendered with216 * the table automatically on display().217 * 218 * Also note that list tables are not automatically wrapped in <form> elements,219 * so you will need to create those manually in order for bulk actions to function.220 * 221 * @return array An associative array containing all the bulk actions: 'slugs’=>’Visible Titles’222 **************************************************************************/223 function get_bulk_actions() {224 $actions = array(225 ‘delete’ => 'Delete’,226 );227 return $actions;228 }229230231 /** ************************************************************************232 * Optional. You can handle your bulk actions anywhere or anyhow you prefer.233 * For this example package, we will handle it in the class to keep things234 * clean and organized.235 * 236 * @see $this->prepare_items()237 **************************************************************************/238 function process_bulk_action() {239 $class = 'notice notice-success’;240 $message = '’;241 $action = strtolower(str_replace(' ',’’, $this->current_action()) );242 243 //Detect when a bulk action is being triggered…244 if( 'delete’==$action ) {245 $class = 'notice notice-success’;246 $message = 'Mail data deleted.’;247 if( $this->delete_item() == false ){248 $class = 'error’;249 $message = ‘Data post NULL’;250 }251 }252253 if( $message!=’’ ){254 echo '<div id="message" class="updated ‘.esc_attr($class).’ is-dismissible">255 '.esc_attr($message).’<button type="button" class="notice-dismiss"><span class="screen-reader-text">’.__( ‘Dismiss this notice.’ ).’</span></button>256 </div>’;257 }258 }259260261 /** ************************************************************************262 * REQUIRED! This is where you prepare your data for display. This method will263 * usually be used to query the database, sort and filter the data, and generally264 * get it ready to be displayed. At a minimum, we should set $this->items and265 * $this->set_pagination_args(), although the following properties and methods266 * are frequently interacted with here…267 * 268 * @global WPDB $wpdb269 * @uses $this->_column_headers270 * @uses $this->items271 * @uses $this->get_columns()272 * @uses $this->get_sortable_columns()273 * @uses $this->get_pagenum()274 * @uses $this->set_pagination_args()275 **************************************************************************/276 function prepare_items() {277 global $wpdb; //This is used only if making any database queries278279280 /**281 * First, lets decide how many records per page to show282 */283 $per_page = $this->get_current_user_screen_meta(‘per_page’, 20);284 if( isset($_POST[‘per_page’]) && intval($_POST[‘per_page’]) > 0 ){285 $per_page = (int) $_POST[‘per_page’]; 286 $this->update_current_user_screen_meta(‘per_page’, $per_page);287 }288289 /**290 * REQUIRED. Now we need to define our column headers. This includes a complete291 * array of columns to be displayed (slugs & titles), a list of columns292 * to keep hidden, and a list of columns that are sortable. Each of these293 * can be defined in another method (as we’ve done here) before being294 * used to build the value for our _column_headers property.295 */296 $columns = $this->get_columns();297 $where = ‘’;298 $hidden = array();299 $sortable = $this->get_sortable_columns();300 301 302 /**303 * REQUIRED. Finally, we build an array to be used by the class for column 304 * headers. The $this->_column_headers property takes an array which contains305 * 3 other arrays. One for all columns, one for hidden columns, and one306 * for sortable columns.307 */308 $this->_column_headers = array($columns, $hidden, $sortable);309 310 311 /**312 * Optional. You can handle your bulk actions however you see fit. In this313 * case, we’ll handle them within our package just to keep things clean.314 */315 $this->process_bulk_action();316 317 318 /**319 * Instead of querying a database, we’re going to fetch the example data320 * property we created for use in this plugin. This makes this example 321 * package slightly different than one you might build on your own. In 322 * this example, we’ll be using array manipulation to sort and paginate 323 * our data. In a real-world implementation, you will probably want to 324 * use sort and pagination data to build a custom query instead, as you’ll325 * be able to use your precisely-queried data immediately.326 */327 //$data = $this->example_data;328 329 /**330 * This checks for sorting input and sorts the data in our array accordingly.331 * 332 * In a real-world situation involving a database, you would probably want 333 * to handle sorting by passing the ‘orderby’ and ‘order’ values directly 334 * to a custom query. The returned data will be pre-sorted, and this array335 * sorting technique would be unnecessary.336 */337 function usort_reorder($a,$b){338 $orderby = sanitize_text_field( (!empty($_REQUEST[‘orderby’])) ? $_REQUEST[‘orderby’] : ‘id’ ); //If no sort, default to title339 $order = sanitize_text_field( (!empty($_REQUEST[‘order’])) ? $_REQUEST[‘order’] : ‘asc’ ); //If no order, default to asc340 $result = strcmp($a[$orderby], $b[$orderby]); //Determine sort order341 return ($order===’asc’) ? $result : -$result; //Send final sort direction to usort342 }343 //usort($data, ‘usort_reorder’);344 345 346 /***********************************************************************347 * ---------------------------------------------------------------------348 * vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv349 * 350 * In a real-world situation, this is where you would place your query.351 *352 * For information on making queries in WordPress, see this Codex entry:353 * http://codex.wordpress.org/Class_Reference/wpdb354 * 355 * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^356 * ---------------------------------------------------------------------357 **********************************************************************/358 global $table_prefix;359360 $table = $table_prefix.’smtpmail_data’;361 362 $search = sanitize_text_field( (!empty($_REQUEST[‘s’])) ? $_REQUEST[‘s’] : ‘’ ); //If no sort, default to title363 364 if( $search!=’’ ){365 $where = " WHERE `subject` LIKE ‘%$search%’ OR `message` LIKE ‘%$search%’ “;366 }367 368 /**369 * REQUIRED for pagination. Let’s figure out what page the user is currently 370 * looking at. We’ll need this later, so you should always include it in 371 * your own package classes.372 */373 $current_page = (int) $this->get_pagenum();374 375 /**376 * REQUIRED for pagination. Let’s check how many items are in our data array. 377 * In real-world use, this would be the total number of items in your database, 378 * without filtering. We’ll need this later, so you should always include it 379 * in your own package classes.380 */381 // $total_items = count($data);382 $total_items = (int) $wpdb->get_var( 'SELECT count(*) FROM '.$table. $where );383 384 /**385 * The WP_List_Table class does not handle pagination for us, so we need386 * to ensure that the data is trimmed to only the current page. We can use387 * array_slice() to 388 */389 //$data = array_slice($data,(($current_page-1)*$per_page),$per_page);390 391 392 393 /**394 * REQUIRED. Now we can add our *sorted* data to the items property, where 395 * it can be used by the rest of the class.396 */397 //$this->items = $data;398 399 $orderby = sanitize_text_field( (!empty($_REQUEST[‘orderby’])) ? $_REQUEST[‘orderby’] : ‘created’ ); //If no sort, default to created400 $order = strtoupper( sanitize_text_field( (!empty($_REQUEST[‘order’])) ? $_REQUEST[‘order’] : ‘DESC’ ) ); //If no order, default to desc401 $sort = '’;402403 $field_sort = array_keys( $this->get_sortable_columns() );404405 if( in_array($orderby,$field_sort) ) {406 $sort = " ORDER BY `$orderby` “;407408 if( in_array($order,array(‘DESC’,’ASC’)) ) {409 $sort .= $order;410 }411 }412413 $query = ' SELECT * FROM '. $table . $where . $sort . ' LIMIT %d, %d’;414 415 $query = $wpdb->prepare($query, ($current_page-1)*$per_page, $per_page);416 417 $this->items = $wpdb->get_results( $query , ARRAY_A );418 419 if( count($this->items) == 0 && intval( smtpmail_options(‘save_data’) ) == 0 ) {420 $this->items[] = array(421 ‘id’ => 1,422 ‘subject’ => 'Example Data’,423 'from_email’=> '[email protected]’,424 ‘to_email’ => '[email protected]’,425 ‘status’ => 1,426 ‘message’ => 'Message’,427 ‘created’ => '2019-07-15’,428 ‘params’ => '{"ip":"103.234.36.66"}’,429 );430 }431 432 /*433 var_export($this->example_data);434 echo '<hr/>’;435 foreach($this->items as $key => $item){436 $this->items[$key][‘id’] = (int)$item[‘id’];437 }438 var_export($this->items);439 */440 441 /**442 * REQUIRED. We also have to register our pagination options & calculations.443 */444 $this->set_pagination_args( array(445 ‘total_items’ => $total_items, //WE have to calculate the total number of items446 ‘per_page’ => $per_page, //WE have to determine how many items to show on a page447 ‘total_pages’ => ceil($total_items/$per_page) //WE have to calculate the total number of pages448 ) );449 }450 451 452 function display_screen_options()453 {454 $columns = $this->get_columns();455 unset($columns[‘cb’]);456 unset($columns[‘id’]);457 458 $has_post = count($_POST)>0;459 460 $labels = '’;461 462 $hiddencolumns = $this->get_current_user_screen_meta(‘hiddencolumns’, array() );463 //var_dump($hiddencolumns);464 $updatehiddencolumns = array();465 466 foreach( $columns as $name => $title )467 {468 $checked = 1; 469 470 $key = $name.’-hide’;471 if( $has_post && empty($_POST[$key]) ){472 $updatehiddencolumns[$name] = 1;473 $checked = 0;474 } else if( isset($hiddencolumns[$name]) && intval($hiddencolumns[$name]) == 1 ){475 $checked = 0;476 }477 478 $labels .= ‘<label><input ‘.( $checked ?’checked="checked"’:’’ ).’ class="hide-column-tog” name="’.479 esc_attr($key).’” type="checkbox" id="’.esc_attr($name).’-hide" value="’.esc_attr($name).’" >’.esc_attr($title).’</label>’;480 }481 482 if( count($updatehiddencolumns) ){483 $this->update_current_user_screen_meta('hiddencolumns’, $updatehiddencolumns );484 }485 486 $per_page = $this->get_current_user_screen_meta('per_page’, 20);487 488 489 echo '<div id="screen-meta" class="metabox-prefs smtpmail_screen_meta_options">490 <div id="screen-options-wrap" class="hidden" tabindex="-1" aria-label="Screen Options Tab">491 <form id="adv-settings" method="post">492 <fieldset class="metabox-prefs">493 <legend>Columns</legend>’.$labels.’494 </fieldset>495 <fieldset class="screen-options">496 <legend>Pagination</legend>497 <label for="subscribers_per_page">Number of items per page:</label>498 <input type="number" step="1" min="1" max="999" class="screen-per-page" name="per_page" id="subscribers_per_page" maxlength="3" value="’.$per_page.’">499 </fieldset>500 <p class="submit"><input type="submit" name="screen-options-apply" id="screen-options-apply" class="button button-primary" value="Apply"></p>501 <input type="hidden" id="screenoptionnonce" name="screenoptionnonce" value="414643bdd4">502 </form>503 </div> 504 </div>505 <div id="screen-meta-links">506 <div id="contextual-help-link-wrap" class="hide-if-no-js screen-meta-toggle" style="visibility: hidden;">507 <button type="button" id="contextual-help-link" class="button show-settings" aria-controls="contextual-help-wrap" aria-expanded="false">Help</button>508 </div>509 <div id="screen-options-link-wrap" class="hide-if-no-js screen-meta-toggle">510 <button type="button" id="show-settings-link" class="button show-settings screen-meta-active" aria-controls="screen-options-wrap" aria-expanded="true">Screen Options</button>511 </div>512 </div>’;513 514 }515 516 function delete_item()517 {518 global $wpdb, $table_prefix;519 520 $table_name = $table_prefix.’smtpmail_data’;521522 if( isset($_GET[‘customer’]) ) {523 $ids = array_map( 'intval’, $_GET[‘customer’] );524 if( is_array($ids) && count($ids)>0 ) {525 $ids = implode(',’,$ids);526 return $wpdb->query( “DELETE FROM `$table_name` WHERE id IN ( $ids );” );527 }528 }529530 $id = absint( isset($_GET[‘code’])?$_GET[‘code’]: 0 );531 if( $id == 0 ) return false;532 533 return $wpdb->delete( 534 $table_name,535 array( ‘id’ => $id ), 536 array( ‘%d’ )537 );538 }539 540 function get_item( $id = 0 )541 {542 global $wpdb, $table_prefix;543 544 return $wpdb->get_row( ‘SELECT * FROM `’.$table_prefix.’smtpmail_data` WHERE `id`=’ .$id );545 }546 547 function get_current_user_screen_meta( $key, $default )548 {549 $current_user = wp_get_current_user();550 $v = (string) get_user_meta($current_user->ID, 'screen_meta_smtpmail_customer_’.$key, $single = true);551 if( $v && $v != ‘’ ){552 if( is_array($default) ){553 $v = json_decode($v);554 if( is_object($v) ){555 $v = get_object_vars($v);556 }557 } else if( is_numeric($default) ) {558 $v = (int) $v;559 }560 return $v;561 }562 return $default;563 }564 565 function update_current_user_screen_meta( $key, $value )566 {567 $current_user = wp_get_current_user();568 return update_user_meta($current_user->ID, 'screen_meta_smtpmail_customer_’.$key, json_encode($value) );569 }570 571}572573/**574 * List page575 *576 * @since 1.1.1577 *578 */579function smtpmail_render_customer_list_page(){580 581$action = sanitize_text_field( isset($_GET[‘action’])?$_GET[‘action’]: ‘’ );582$id = absint( isset($_GET[‘code’])?$_GET[‘code’]: 0 );583$page = sanitize_text_field( isset($_REQUEST[‘page’])?$_REQUEST[‘page’]: ‘’ );584585$save_data = (int) smtpmail_options(‘save_data’);586587//Create an instance of our package class…588$SMTPMail_Data_Table = new SMTPMail_Data_List_Table();589590if( $id > 0 && $action == ‘detail’ ):591592 $item = $SMTPMail_Data_Table->get_item( $id );593 594 smtpmail_render_customer_detail_form( $item );595596else:597 //Fetch, prepare, sort, and filter our data…598 $SMTPMail_Data_Table->prepare_items();599 ?>600 601 <?php add_thickbox(); ?>602 <div class="wrap-list-table">603 604 <!-- Forms are NOT created automatically, so you need to wrap the table in one to use features like bulk actions -->605 <form id="customers-filter" method="get">606 <!-- For plugins, we also need to ensure that the form posts back to our current page -->607 <input type="hidden" name="page" value="<?php esc_attr_e($page);?>" />608 <input type="hidden" name="tab" value="list" />609 <!-- Now we can render the completed list table -->610 611 <?php if( $save_data == 0 ) :?>612 <br />613 <div class="smtpmail-icon-click">614 <div class="dashicons dashicons-arrow-right-alt"></div>615 </div>616 <strong><?php _e( 'You need enable option `Save data SendMail` in General tab.’, ‘smtp-mail’ ); ?></strong>617 <hr />618 <h3 align=center><?php _e( 'Example Data SendMail’, ‘smtp-mail’ ); ?></h3>619 <?php endif;?>620 621 <?php $SMTPMail_Data_Table->search_box( ‘Search’,’subject’); ?>622 <?php $SMTPMail_Data_Table->display() ?>623 </form>624 </div>625<?php 626627endif;628629}630631/**632 * Detail form633 *634 * @since 1.1.2635 *636 * @param object $item 637 */638function smtpmail_render_customer_detail_form( $item )639{640641 $subject = isset($item->subject) ? 'Reply to '.$item->subject : '’;642 $message = isset($item->message) ? $item->message : '’;643644 $whois_link = ‘’;645646 $params = json_decode($item->params);647 648 if( $params && is_object($params) && isset($params->ip) ) {649 $whois_link = esc_url( ‘http://whois.photoboxone.com/location/’ . $params->ip );650 }651 652 $page = sanitize_text_field( isset($_REQUEST[‘page’])?$_REQUEST[‘page’]:’’ );653654 ?>655 <form action="<?php echo esc_url( admin_url(‘options-general.php?page=’.$page.’&tab=detail&code=’ . $item->id ) );?>" method="post" class="smtpmail_detail_form">656 <h3><?php _e(‘Guest Message’, ‘smtp-mail’) ;?>:</h3>657 <div class="message-box"><?php esc_html_e( nl2br( $message ) );?></div>658 <?php if( $whois_link!=’’ ):?>659 <h3>660 <a href="<?php echo esc_url( $whois_link );?>" target="_blank" rel="help">661 <?php _e('Where is guest?’, ‘smtp-mail’) ;?>662 </a>663 </h3>664 <p>665 <div class="smtpmail-icon-click">666 <div class="dashicons dashicons-arrow-right-alt"></div>667 </div>668 <a href="<?php echo esc_url( $whois_link );?>" target="_blank" rel="help">669 <?php _e('View location and map!’, ‘smtp-mail’) ;?>670 </a>671 </p>672 <?php endif;?>673 <h3><?php _e('Reply form’, ‘smtp-mail’) ;?></h3>674 <p>675 <label><?php _e( 'Name’, ‘smtp-mail’ ); ?>:</label>676 <input name="name" type="text" autocomplete="false" class="inputbox required" />677 </p>678 <p>679 <label><?php _e( 'Email’, ‘smtp-mail’ ); ?>:</label>680 <input name="email" type="email" autocomplete="false" class="inputbox required" />681 </p>682 <p>683 <label><?php _e( 'Subject’, ‘smtp-mail’ ); ?>:</label>684 <input name="subject" type="text" value="<?php esc_attr_e( $subject ); ?>" class="inputbox required" />685 </p>686 <p>687 <label><?php _e( 'Message’, ‘smtp-mail’ ); ?>:</label>688 <textarea name="message" id="message" rows="8" cols="40" class="textareabox required" autocomplete="false" ></textarea>689 </p>690 <p class="buttons">691 <label> </label>692 <input type="submit" name="send_test" id="send_test" class="button button-primary" value="Send">693 <a class="button button-secondary" href="<?php echo esc_url( admin_url(‘options-general.php?page=’.$page.’&tab=list’ ) );?>"><?php _e('Cancel’, ‘smtp-mail’) ;?></a>694 </p>695 </form>696 <?php697 698}