PluginProbe ʕ •ᴥ•ʔ
WP 2FA – Two-factor authentication for WordPress / 2.6.0
WP 2FA – Two-factor authentication for WordPress v2.6.0
4.0.0 1.7.1 2.0.0 2.0.1 2.1.0 2.2.0 2.2.1 2.3.0 2.4.0 2.4.1 2.4.2 2.5.0 2.6.0 2.6.1 2.6.2 2.6.3 2.6.4 2.7.0 2.8.0 2.9.0 2.9.1 2.9.2 2.9.3 3.0.0 3.0.1 3.1.0 3.1.1 3.1.1.2 trunk 1.2.0 1.3.0 1.4.0 1.4.1 1.4.2 1.5.0 1.5.1 1.5.2 1.6.0 1.6.1 1.6.2 1.7.0
wp-2fa / includes / classes / Admin / SettingsPages / class-settings-page-email.php
wp-2fa / includes / classes / Admin / SettingsPages Last commit date
class-settings-page-email.php 2 years ago class-settings-page-general.php 2 years ago class-settings-page-policies.php 2 years ago class-settings-page-render.php 2 years ago class-settings-page-white-label.php 2 years ago index.php 2 years ago
class-settings-page-email.php
500 lines
1 <?php
2 /**
3 * Email settings class.
4 *
5 * @package wp2fa
6 * @subpackage settings-pages
7 * @copyright %%YEAR%% Melapress
8 * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0
9 * @link https://wordpress.org/plugins/wp-2fa/
10 */
11
12 namespace WP2FA\Admin\SettingsPages;
13
14 use WP2FA\Email_Template;
15 use WP2FA\WP2FA;
16 use WP2FA\Utils\Debugging;
17 use WP2FA\Admin\Helpers\WP_Helper;
18 use WP2FA\Admin\Controllers\Settings;
19 use WP2FA\Utils\Settings_Utils;
20
21 /**
22 * Email settings tab
23 */
24 if ( ! class_exists( '\WP2FA\Admin\SettingsPages\Settings_Page_Email' ) ) {
25 /**
26 * Settings_Page_Email - Class for handling email settings
27 *
28 * @since 2.0.0
29 */
30 class Settings_Page_Email {
31
32 /**
33 * Render the settings
34 *
35 * @return void
36 *
37 * @since 2.0.0
38 */
39 public static function render() {
40 settings_fields( WP_2FA_EMAIL_SETTINGS_NAME );
41 self::email_from_settings();
42 self::email_settings();
43 submit_button( esc_html__( 'Save email settings and templates', 'wp-2fa' ) );
44 }
45
46 /**
47 * Handle saving email options to the network main site options.
48 *
49 * @return void
50 *
51 * @since 2.0.0
52 *
53 * @SuppressWarnings(PHPMD.ExitExpressions)
54 */
55 public static function update_wp2fa_network_options() {
56 if ( isset( $_POST['email_from_setting'] ) ) { // phpcs:ignore
57 $options = self::validate_and_sanitize( wp_unslash( $_POST ) ); // phpcs:ignore
58
59 if ( isset( $_POST['email_from_setting'] ) && 'use-custom-email' === $_POST['email_from_setting'] && isset( $_POST['custom_from_display_name'] ) && empty( $_POST['custom_from_display_name'] ) || isset( $_POST['email_from_setting'] ) && 'use-custom-email' === $_POST['email_from_setting'] && isset( $_POST['custom_from_email_address'] ) && empty( $_POST['custom_from_email_address'] ) ) { // phpcs:ignore
60 // redirect back to our options page.
61 wp_safe_redirect(
62 add_query_arg(
63 array(
64 'page' => 'wp-2fa-settings',
65 'wp_2fa_network_settings_updated' => 'false',
66 'tab' => 'email-settings',
67 ),
68 network_admin_url( 'admin.php' )
69 )
70 );
71 exit;
72 }
73
74 Settings_Utils::update_option( WP_2FA_EMAIL_SETTINGS_NAME, $options );
75 }
76
77 // redirect back to our options page.
78 wp_safe_redirect(
79 add_query_arg(
80 array(
81 'page' => 'wp-2fa-settings',
82 'wp_2fa_network_settings_updated' => 'true',
83 'tab' => 'email-settings',
84 ),
85 network_admin_url( 'admin.php' )
86 )
87 );
88 exit;
89 }
90
91 /**
92 * Email settings
93 *
94 * @return void
95 *
96 * @since 2.0.0
97 */
98 private static function email_from_settings() {
99 ?>
100 <h3><?php esc_html_e( 'Which email address should the plugin use as a from address?', 'wp-2fa' ); ?></h3>
101 <p class="description">
102 <?php esc_html_e( 'Use these settings to customize the "from" name and email address for all correspondence sent from our plugin.', 'wp-2fa' ); ?>
103 </p>
104 <table class="form-table">
105 <tbody>
106 <tr>
107 <th><label for="2fa-method"><?php esc_html_e( 'From email & name', 'wp-2fa' ); ?></label>
108 </th>
109 <td>
110 <fieldset class="contains-hidden-inputs">
111 <label for="use-defaults">
112 <input type="radio" name="email_from_setting" id="use-defaults" value="use-defaults"
113 <?php checked( WP2FA::get_wp2fa_email_templates( 'email_from_setting' ), 'use-defaults' ); ?>
114 >
115 <span><?php esc_html_e( 'Use the email address from the WordPress general settings.', 'wp-2fa' ); ?></span>
116 </label>
117 <br/>
118 <label for="use-custom-email">
119 <input type="radio" name="email_from_setting" id="use-custom-email" value="use-custom-email"
120 <?php checked( WP2FA::get_wp2fa_email_templates( 'email_from_setting' ), 'use-custom-email' ); ?>
121 data-unhide-when-checked=".custom-from-inputs">
122 <span><?php esc_html_e( 'Use another email address', 'wp-2fa' ); ?></span>
123 </label>
124 <fieldset class="hidden custom-from-inputs">
125 <p class="description">
126 <?php esc_html_e( 'A \'From email\' address with a domain different than that of your website domain name, or with a domain that the hosting does not relay might cause the notification emails to be blocked, marked as spam, or not delivered at all. If you are not 100% sure about this change, consult with your web host.', 'wp-2fa' ); ?>
127 </p>
128 <br/>
129 <span><?php esc_html_e( 'Email Address:', 'wp-2fa' ); ?></span> <input type="text" id="custom_from_email_address" name="custom_from_email_address" value="<?php echo esc_attr( WP2FA::get_wp2fa_email_templates( 'custom_from_email_address' ) ); ?>"><br><br>
130 <span><?php esc_html_e( 'Display Name:', 'wp-2fa' ); ?></span> <input type="text" id="custom_from_display_name" name="custom_from_display_name" value="<?php echo esc_attr( WP2FA::get_wp2fa_email_templates( 'custom_from_display_name' ) ); ?>">
131 </fieldset>
132
133 </fieldset>
134 </td>
135 </tr>
136 </tbody>
137 </table>
138 <div class="description"><i><?php esc_html_e( 'Tip: The \'From email\' address should match your website domain. If the "from address" does not match your website domain, the emails may be blocked or marked as spam. If you are not sure about this please consult with your website administrator / developer or ', 'wp-2fa' ); ?><a href="<?php echo esc_url( 'https://melapress.com/contact/?utm_source=plugins&utm_medium=link&utm_campaign=wp2fa' ); ?>" target="_blank"><?php esc_html_e( 'contact us', 'wp-2fa' ); ?></a> <?php esc_html_e( 'for more information.', 'wp-2fa' ); ?></i></div>
139 <br>
140 <hr>
141
142 <h3><?php esc_html_e( 'Email delivery test', 'wp-2fa' ); ?></h3>
143 <p class="description">
144 <?php esc_html_e( 'The plugin sends emails with one-time codes, blocked account notifications and more. Use the button below to confirm the plugin can successfully send emails.', 'wp-2fa' ); ?>
145 </p>
146 <p>
147 <button type="button" name="test_email_config_test"
148 class="button js-button-test-email-trigger"
149 data-email-id="config_test"
150 <?php echo WP_Helper::create_data_nonce( 'wp-2fa-email-test-config_test' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
151 <?php esc_html_e( 'Test email delivery', 'wp-2fa' ); ?>
152 </button>
153 </p>
154
155 <br>
156 <hr>
157
158 <?php
159 }
160
161 /**
162 * Creates the email notification definitions.
163 *
164 * @return Email_Template[]
165 *
166 * @since 2.0.0
167 */
168 public static function get_email_notification_definitions() {
169
170 $backup_codes = new Email_Template(
171 'user_backup_codes',
172 esc_html__( 'User backup codes email', 'wp-2fa' ),
173 esc_html__( 'This email can be sent a user once backup codes are generated.', 'wp-2fa' )
174 );
175 $backup_codes->set_can_be_toggled( false );
176
177 $result = array(
178 new Email_Template(
179 'login_code_setup',
180 esc_html__( '2FA setup code email', 'wp-2fa' ),
181 esc_html__( 'This is the email sent to a user when setting up 2FA via email.', 'wp-2fa' )
182 ),
183 new Email_Template(
184 'login_code',
185 esc_html__( 'Login code email', 'wp-2fa' ),
186 esc_html__( 'This is the email sent to a user when a login code is required.', 'wp-2fa' )
187 ),
188 new Email_Template(
189 'account_locked',
190 esc_html__( 'User account locked email', 'wp-2fa' ),
191 esc_html__( 'This is the email sent to a user upon grace period expiry.', 'wp-2fa' )
192 ),
193 new Email_Template(
194 'account_unlocked',
195 esc_html__( 'User account unlocked email', 'wp-2fa' ),
196 esc_html__( 'This is the email sent to a user when the user\'s account has been unlocked.', 'wp-2fa' )
197 ),
198 new Email_Template(
199 'reset_password_code',
200 esc_html__( 'User reset password code email', 'wp-2fa' ),
201 esc_html__( 'This is the email sent to a user when a password reset is requested.', 'wp-2fa' )
202 ),
203 $backup_codes,
204 );
205
206 /**
207 * Add an option for external providers to implement their own email template settings for the settings tab.
208 *
209 * @param array $result - The array with all the email templates.
210 *
211 * @since 2.0.0
212 */
213 $result = apply_filters( WP_2FA_PREFIX . 'email_notification_definitions', $result );
214
215 if ( count( $result ) > 6 ) {
216 $result[0]->set_can_be_toggled( false );
217 $result[1]->set_can_be_toggled( false );
218 $result[2]->set_can_be_toggled( false );
219 $result[3]->set_email_content_id( 'user_account_locked' );
220 $result[4]->set_email_content_id( 'user_account_unlocked' );
221 $result[5]->set_can_be_toggled( false );
222 } else {
223 $result[0]->set_can_be_toggled( false );
224 $result[1]->set_can_be_toggled( false );
225 $result[2]->set_email_content_id( 'user_account_locked' );
226 $result[3]->set_email_content_id( 'user_account_unlocked' );
227 $result[4]->set_can_be_toggled( false );
228 }
229 return $result;
230 }
231
232 /**
233 * Validate email templates before saving
234 *
235 * @since 2.0.0
236 *
237 * @SuppressWarnings(PHPMD.ExitExpressions)
238 */
239 public static function validate_and_sanitize() {
240
241 // Bail if user doesn't have permissions to be here.
242 if ( ! current_user_can( 'manage_options' ) ) {
243 return;
244 }
245
246 Debugging::log( 'The following settings will be processed (E-mail): ' . "\n" . wp_json_encode( $_POST ) ); // phpcs:ignore
247
248 if ( empty( $_POST ) || ! isset( $_POST['_wpnonce'] ) || empty( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], WP_2FA_PREFIX . 'email_settings-options' ) && ! wp_verify_nonce( $_POST['_wpnonce'], WP_2FA_PREFIX . 'settings-options' ) || ! wp_verify_nonce( $_POST['_wpnonce'], WP_2FA_PREFIX . 'email_settings-options' ) && ! wp_verify_nonce( $_POST['_wpnonce'], WP_2FA_PREFIX . 'settings-options' ) ) { // phpcs:ignore
249 die( esc_html__( 'Nonce verification failed.', 'wp-2fa' ) );
250 }
251
252 $output = array();
253
254 if ( isset( $_POST['email_from_setting'] ) && 'use-defaults' === $_POST['email_from_setting'] || isset( $_POST['email_from_setting'] ) && 'use-custom-email' === $_POST['email_from_setting'] ) {
255 $output['email_from_setting'] = sanitize_text_field( wp_unslash( $_POST['email_from_setting'] ) );
256 }
257
258 if ( isset( $_POST['email_from_setting'] ) && 'use-custom-email' === $_POST['email_from_setting'] && isset( $_POST['custom_from_email_address'] ) && empty( $_POST['custom_from_email_address'] ) ) {
259 add_settings_error(
260 WP_2FA_SETTINGS_NAME,
261 esc_attr( 'email_from_settings_error' ),
262 esc_html__( 'Please provide an email address', 'wp-2fa' ),
263 'error'
264 );
265 $output['custom_from_email_address'] = '';
266 }
267
268 if ( isset( $_POST['email_from_setting'] ) && 'use-custom-email' === $_POST['email_from_setting'] && isset( $_POST['custom_from_display_name'] ) && empty( $_POST['custom_from_display_name'] ) ) {
269 add_settings_error(
270 WP_2FA_SETTINGS_NAME,
271 esc_attr( 'display_name_settings_error' ),
272 esc_html__( 'Please provide a display name.', 'wp-2fa' ),
273 'error'
274 );
275 $output['custom_from_email_address'] = '';
276 }
277
278 if ( isset( $_POST['custom_from_email_address'] ) && ! empty( $_POST['custom_from_email_address'] ) ) {
279 if ( ! filter_var( wp_unslash( $_POST['custom_from_email_address'] ), FILTER_VALIDATE_EMAIL ) ) {
280 add_settings_error(
281 WP_2FA_SETTINGS_NAME,
282 esc_attr( 'email_invalid_settings_error' ),
283 esc_html__( 'Please provide a valid email address. Your email address has not been updated.', 'wp-2fa' ),
284 'error'
285 );
286 }
287 $output['custom_from_email_address'] = sanitize_email( wp_unslash( $_POST['custom_from_email_address'] ) );
288
289 Settings_Utils::delete_option( 'dismiss_notice_mail_domain' );
290 }
291
292 if ( ! isset( $_POST['email_from_setting'] ) ) {
293 Settings_Utils::delete_option( 'dismiss_notice_mail_domain' );
294 }
295
296 if ( isset( $_POST['custom_from_display_name'] ) && ! empty( $_POST['custom_from_display_name'] ) ) {
297 // Check if the string contains HTML/tags.
298 preg_match( "/<\/?\w+((\s+\w+(\s*=\s*(?:\".*?\"|'.*?'|[^'\">\s]+))?)+\s*|\s*)\/?>/", sanitize_text_field( wp_unslash( $_POST['custom_from_display_name'] ) ), $matches );
299 if ( count( $matches ) > 0 ) {
300 add_settings_error(
301 WP_2FA_SETTINGS_NAME,
302 esc_attr( 'display_name_invalid_settings_error' ),
303 esc_html__( 'Please only use alphanumeric text. Your display name has not been updated.', 'wp-2fa' ),
304 'error'
305 );
306 } else {
307 $output['custom_from_display_name'] = sanitize_text_field( wp_unslash( $_POST['custom_from_display_name'] ) );
308 }
309 }
310
311 if ( isset( $_POST['login_code_email_subject'] ) ) {
312 $output['login_code_email_subject'] = wp_kses_post( wp_unslash( $_POST['login_code_email_subject'] ) );
313 }
314
315 if ( isset( $_POST['login_code_email_body'] ) ) {
316 $output['login_code_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['login_code_email_body'] ) ) );
317 }
318
319 if ( isset( $_POST['login_code_setup_email_subject'] ) ) {
320 $output['login_code_setup_email_subject'] = wp_kses_post( wp_unslash( $_POST['login_code_setup_email_subject'] ) );
321 }
322
323 if ( isset( $_POST['login_code_setup_email_body'] ) ) {
324 $output['login_code_setup_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['login_code_setup_email_body'] ) ) );
325 }
326
327 if ( isset( $_POST['user_account_locked_email_subject'] ) ) {
328 $output['user_account_locked_email_subject'] = wp_kses_post( wp_unslash( $_POST['user_account_locked_email_subject'] ) );
329 }
330
331 if ( isset( $_POST['user_account_locked_email_body'] ) ) {
332 $output['user_account_locked_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['user_account_locked_email_body'] ) ) );
333 }
334
335 if ( isset( $_POST['user_account_unlocked_email_subject'] ) ) {
336 $output['user_account_unlocked_email_subject'] = wp_kses_post( wp_unslash( $_POST['user_account_unlocked_email_subject'] ) );
337 }
338
339 if ( isset( $_POST['user_account_unlocked_email_body'] ) ) {
340 $output['user_account_unlocked_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['user_account_unlocked_email_body'] ) ) );
341 }
342
343 if ( isset( $_POST['reset_password_code_email_body'] ) ) {
344 $output['reset_password_code_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['reset_password_code_email_body'] ) ) );
345 }
346
347 if ( isset( $_POST['reset_password_code_email_subject'] ) ) {
348 $output['reset_password_code_email_subject'] = wp_kses_post( wp_unslash( $_POST['reset_password_code_email_subject'] ) );
349 }
350
351 $output['send_account_locked_email'] = '';
352 if ( isset( $_POST['send_account_locked_email'] ) && 'enable_account_locked_email' === $_POST['send_account_locked_email'] ) {
353 $output['send_account_locked_email'] = sanitize_text_field( wp_unslash( $_POST['send_account_locked_email'] ) );
354 }
355
356 $output['send_account_unlocked_email'] = '';
357 if ( isset( $_POST['send_account_unlocked_email'] ) && 'enable_account_unlocked_email' === $_POST['send_account_unlocked_email'] ) {
358 $output['send_account_unlocked_email'] = sanitize_text_field( wp_unslash( $_POST['send_account_unlocked_email'] ) );
359 }
360
361 $output['send_login_code_email'] = '';
362 if ( isset( $_POST['send_login_code_email'] ) && 'enable_send_login_code_email' === $_POST['send_login_code_email'] ) {
363 $output['send_login_code_email'] = sanitize_text_field( wp_unslash( $_POST['send_login_code_email'] ) );
364 }
365
366 if ( isset( $_POST['user_backup_codes_email_subject'] ) ) {
367 $output['user_backup_codes_email_subject'] = wp_kses_post( wp_unslash( $_POST['user_backup_codes_email_subject'] ) );
368 }
369
370 if ( isset( $_POST['user_backup_codes_email_body'] ) ) {
371 $output['user_backup_codes_email_body'] = wpautop( wp_kses_post( wp_unslash( $_POST['user_backup_codes_email_body'] ) ) );
372 }
373
374 /**
375 * Filter the values we are about to store in the plugin settings.
376 *
377 * @param array $output - The output array with all the data we will store in the settings.
378 *
379 * @since 2.0.0
380 */
381 $output = apply_filters( WP_2FA_PREFIX . 'filter_output_email_template_content', $output );
382
383 Debugging::log( 'The following settings are being saved (E-mail): ' . "\n" . wp_json_encode( $output ) );
384
385 // Remove duplicates from settings errors. We do this as this sanitization callback is actually fired twice, so we end up with duplicates when saving the settings for the FIRST TIME only. The issue is not present once the settings are in the DB as the sanitization wont fire again. For details on this core issue - https://core.trac.wordpress.org/ticket/21989.
386 global $wp_settings_errors;
387 if ( isset( $wp_settings_errors ) ) {
388 $errors = array_map( 'unserialize', array_unique( array_map( 'serialize', $wp_settings_errors ) ) );
389 $wp_settings_errors = $errors; // phpcs:ignore
390 }
391
392 if ( isset( $output ) ) {
393 return $output;
394 } else {
395 return;
396 }
397 }
398
399 /**
400 * Email settings
401 *
402 * @return void
403 *
404 * @since 2.0.0
405 */
406 private static function email_settings() {
407 $custom_user_page_id = Settings::check_setting_in_all_roles( 'custom-user-page-id' );
408 if ( empty( $custom_user_page_id ) ) {
409 $custom_user_page_id = Settings::check_setting_in_all_roles( 'custom-user-page-url' );
410 }
411 $email_template_definitions = self::get_email_notification_definitions();
412 ?>
413 <h1><?php esc_html_e( 'Email Templates', 'wp-2fa' ); ?></h1>
414 <?php foreach ( $email_template_definitions as $email_template ) : ?>
415 <?php $template_id = $email_template->get_id(); ?>
416 <h3><?php echo esc_html( $email_template->get_title() ); ?></h3>
417 <p class="description"><?php echo $email_template->get_description(); // phpcs:ignore ?></p>
418 <table class="form-table">
419 <tbody>
420 <?php if ( $email_template->can_be_toggled() ) : ?>
421 <tr>
422 <th><label for="send_<?php echo esc_attr( $template_id ); ?>_email"><?php esc_html_e( 'Send this email', 'wp-2fa' ); ?></label></th>
423 <td>
424 <fieldset>
425 <input type="checkbox" id="send_<?php echo esc_attr( $template_id ); ?>_email" name="send_<?php echo esc_attr( $template_id ); ?>_email" value="enable_<?php echo esc_attr( $template_id ); ?>_email"
426 <?php checked( 'enable_' . $template_id . '_email', WP2FA::get_wp2fa_email_templates( 'send_' . $template_id . '_email' ) ); ?>
427 >
428 <label for="send_<?php echo esc_attr( $template_id ); ?>_email"><?php esc_html_e( 'Uncheck to disable this message.', 'wp-2fa' ); ?></label>
429 </fieldset>
430 </td>
431 </tr>
432 <?php endif; ?>
433 <?php $template_id = $email_template->get_email_content_id(); ?>
434 <tr>
435 <th><label for="<?php echo esc_attr( $template_id ); ?>_email_subject"><?php esc_html_e( 'Email subject', 'wp-2fa' ); ?></label></th>
436 <td>
437 <fieldset>
438 <input type="text" id="<?php echo esc_attr( $template_id ); ?>_email_subject" name="<?php echo esc_attr( $template_id ); ?>_email_subject" class="large-text" value="<?php echo esc_attr( WP2FA::get_wp2fa_email_templates( $template_id . '_email_subject' ) ); ?>">
439 </fieldset>
440 </td>
441 </tr>
442 <tr>
443 <th>
444 <label for="<?php echo esc_attr( $template_id ); ?>_email_body"><?php esc_html_e( 'Email body', 'wp-2fa' ); ?></label>
445 </br>
446 <label for="<?php echo esc_attr( $template_id ); ?>_email_tags" style="font-weight: 400;"><?php esc_html_e( 'Available template tags:', 'wp-2fa' ); ?></label>
447 </br>
448 </br>
449 <span style="font-weight: 400;">
450 {site_url}</br>
451 {site_name}</br>
452 {grace_period}</br>
453 {user_login_name}</br>
454 {user_first_name}</br>
455 {user_last_name}</br>
456 {user_display_name}</br>
457 {login_code}</br>
458 {user_ip_address}</br>
459 {backup_codes}
460 <?php
461 if ( ! empty( $custom_user_page_id ) ) {
462 echo '</br>{2fa_settings_page_url}';
463 }
464 ?>
465 </span>
466 </th>
467 <td>
468 <fieldset>
469 <?php
470 $message = WP2FA::get_wp2fa_email_templates( $template_id . '_email_body' );
471 $content = $message;
472 $editor_id = $template_id . '_email_body';
473 $settings = array(
474 'media_buttons' => false,
475 'editor_height' => 200,
476 );
477 wp_editor( $content, $editor_id, $settings );
478 ?>
479 </fieldset>
480 <p>
481 <button type="button" name="test_email_<?php echo esc_attr( $template_id ); ?>"
482 class="button js-button-test-email-trigger"
483 data-email-id="<?php echo esc_attr( $template_id ); ?>"
484 <?php echo WP_Helper::create_data_nonce( 'wp-2fa-email-test-' . $template_id ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
485 <?php esc_html_e( 'Send test email', 'wp-2fa' ); ?>
486 </button>
487 </p>
488 </td>
489 </tr>
490 </tbody>
491 </table>
492
493 <br>
494 <hr>
495 <?php endforeach; ?>
496 <?php
497 }
498 }
499 }
500