Bez popisu

class-email.php 24KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927
  1. <?php
  2. /**
  3. * Email text field.
  4. *
  5. * @since 1.0.0
  6. */
  7. class WPForms_Field_Email extends WPForms_Field {
  8. /**
  9. * Encoding.
  10. *
  11. * @since 1.6.9
  12. */
  13. const ENCODING = 'UTF-8';
  14. /**
  15. * Primary class constructor.
  16. *
  17. * @since 1.0.0
  18. */
  19. public function init() {
  20. // Define field type information.
  21. $this->name = esc_html__( 'Email', 'wpforms-lite' );
  22. $this->type = 'email';
  23. $this->icon = 'fa-envelope-o';
  24. $this->order = 170;
  25. // Define additional field properties.
  26. add_filter( 'wpforms_field_properties_email', array( $this, 'field_properties' ), 5, 3 );
  27. // Set field to default to required.
  28. add_filter( 'wpforms_field_new_required', array( $this, 'default_required' ), 10, 2 );
  29. // Set confirmation status to option wrapper class.
  30. add_filter( 'wpforms_builder_field_option_class', array( $this, 'field_option_class' ), 10, 2 );
  31. add_action( 'wp_ajax_wpforms_restricted_email', [ $this, 'ajax_check_restricted_email' ] );
  32. add_action( 'wp_ajax_nopriv_wpforms_restricted_email', [ $this, 'ajax_check_restricted_email' ] );
  33. add_action( 'wp_ajax_wpforms_sanitize_restricted_rules', [ $this, 'ajax_sanitize_restricted_rules' ] );
  34. add_filter( 'wpforms_save_form_args', [ $this, 'save_form_args' ], 11, 3 );
  35. }
  36. /**
  37. * Define additional field properties.
  38. *
  39. * @since 1.3.7
  40. *
  41. * @param array $properties List field properties.
  42. * @param array $field Field data and settings.
  43. * @param array $form_data Form data and settings.
  44. *
  45. * @return array
  46. */
  47. public function field_properties( $properties, $field, $form_data ) {
  48. if ( ! empty( $field['confirmation'] ) ) {
  49. $properties = $this->confirmation_field_properties( $properties, $field, $form_data );
  50. }
  51. if ( ! empty( $field['filter_type'] ) ) {
  52. $properties = $this->filter_type_field_properties( $properties, $field, $form_data );
  53. }
  54. return $properties;
  55. }
  56. /**
  57. * Define the confirmation field properties.
  58. *
  59. * @since 1.6.3
  60. *
  61. * @param array $properties List field properties.
  62. * @param array $field Field data and settings.
  63. * @param array $form_data Form data and settings.
  64. *
  65. * @return array
  66. */
  67. public function confirmation_field_properties( $properties, $field, $form_data ) {
  68. $form_id = absint( $form_data['id'] );
  69. $field_id = absint( $field['id'] );
  70. // Email confirmation setting enabled.
  71. $props = array(
  72. 'inputs' => array(
  73. 'primary' => array(
  74. 'block' => array(
  75. 'wpforms-field-row-block',
  76. 'wpforms-one-half',
  77. 'wpforms-first',
  78. ),
  79. 'class' => array(
  80. 'wpforms-field-email-primary',
  81. ),
  82. 'sublabel' => array(
  83. 'hidden' => ! empty( $field['sublabel_hide'] ),
  84. 'value' => esc_html__( 'Email', 'wpforms-lite' ),
  85. ),
  86. ),
  87. 'secondary' => array(
  88. 'attr' => array(
  89. 'name' => "wpforms[fields][{$field_id}][secondary]",
  90. 'value' => '',
  91. 'placeholder' => ! empty( $field['confirmation_placeholder'] ) ? $field['confirmation_placeholder'] : '',
  92. ),
  93. 'block' => array(
  94. 'wpforms-field-row-block',
  95. 'wpforms-one-half',
  96. ),
  97. 'class' => array(
  98. 'wpforms-field-email-secondary',
  99. ),
  100. 'data' => array(
  101. 'rule-confirm' => '#' . $properties['inputs']['primary']['id'],
  102. ),
  103. 'id' => "wpforms-{$form_id}-field_{$field_id}-secondary",
  104. 'required' => ! empty( $field['required'] ) ? 'required' : '',
  105. 'sublabel' => array(
  106. 'hidden' => ! empty( $field['sublabel_hide'] ),
  107. 'value' => esc_html__( 'Confirm Email', 'wpforms-lite' ),
  108. ),
  109. 'value' => '',
  110. ),
  111. ),
  112. );
  113. $properties = array_merge_recursive( $properties, $props );
  114. // Input Primary: adjust name.
  115. $properties['inputs']['primary']['attr']['name'] = "wpforms[fields][{$field_id}][primary]";
  116. // Input Primary: remove size and error classes.
  117. $properties['inputs']['primary']['class'] = array_diff(
  118. $properties['inputs']['primary']['class'],
  119. array(
  120. 'wpforms-field-' . sanitize_html_class( $field['size'] ),
  121. 'wpforms-error',
  122. )
  123. );
  124. // Input Primary: add error class if needed.
  125. if ( ! empty( $properties['error']['value']['primary'] ) ) {
  126. $properties['inputs']['primary']['class'][] = 'wpforms-error';
  127. }
  128. // Input Secondary: add error class if needed.
  129. if ( ! empty( $properties['error']['value']['secondary'] ) ) {
  130. $properties['inputs']['secondary']['class'][] = 'wpforms-error';
  131. }
  132. // Input Secondary: add required class if needed.
  133. if ( ! empty( $field['required'] ) ) {
  134. $properties['inputs']['secondary']['class'][] = 'wpforms-field-required';
  135. }
  136. return $properties;
  137. }
  138. /**
  139. * Define the filter field properties.
  140. *
  141. * @since 1.6.3
  142. *
  143. * @param array $properties List field properties.
  144. * @param array $field Field data and settings.
  145. * @param array $form_data Form data and settings.
  146. *
  147. * @return array
  148. */
  149. public function filter_type_field_properties( $properties, $field, $form_data ) {
  150. if ( ! empty( $field['filter_type'] ) && ! empty( $field[ $field['filter_type'] ] ) ) {
  151. $properties['inputs']['primary']['data']['rule-restricted-email'] = true;
  152. }
  153. return $properties;
  154. }
  155. /**
  156. * Field should default to being required.
  157. *
  158. * @since 1.0.9
  159. * @param bool $required
  160. * @param array $field
  161. * @return bool
  162. */
  163. public function default_required( $required, $field ) {
  164. if ( 'email' === $field['type'] ) {
  165. return true;
  166. }
  167. return $required;
  168. }
  169. /**
  170. * Add class to field options wrapper to indicate if field confirmation is
  171. * enabled.
  172. *
  173. * @since 1.3.0
  174. *
  175. * @param string $class Class strings.
  176. * @param array $field Current field.
  177. *
  178. * @return string
  179. */
  180. public function field_option_class( $class, $field ) {
  181. if ( 'email' !== $field['type'] ) {
  182. return $class;
  183. }
  184. $class .= isset( $field['confirmation'] ) ? ' wpforms-confirm-enabled' : ' wpforms-confirm-disabled';
  185. if ( ! empty( $field['filter_type'] ) ) {
  186. $class .= ' wpforms-filter-' . $field['filter_type'];
  187. }
  188. return $class;
  189. }
  190. /**
  191. * Field options panel inside the builder.
  192. *
  193. * @since 1.0.0
  194. *
  195. * @param array $field
  196. */
  197. public function field_options( $field ) {
  198. /*
  199. * Basic field options.
  200. */
  201. // Options open markup.
  202. $args = array(
  203. 'markup' => 'open',
  204. );
  205. $this->field_option( 'basic-options', $field, $args );
  206. // Label.
  207. $this->field_option( 'label', $field );
  208. // Description.
  209. $this->field_option( 'description', $field );
  210. // Required toggle.
  211. $this->field_option( 'required', $field );
  212. // Confirmation toggle.
  213. $fld = $this->field_element(
  214. 'toggle',
  215. $field,
  216. array(
  217. 'slug' => 'confirmation',
  218. 'value' => isset( $field['confirmation'] ) ? '1' : '0',
  219. 'desc' => esc_html__( 'Enable Email Confirmation', 'wpforms-lite' ),
  220. 'tooltip' => esc_html__( 'Check this option to ask users to provide an email address twice.', 'wpforms-lite' ),
  221. ),
  222. false
  223. );
  224. $args = array(
  225. 'slug' => 'confirmation',
  226. 'content' => $fld,
  227. );
  228. $this->field_element( 'row', $field, $args );
  229. // Options close markup.
  230. $args = array(
  231. 'markup' => 'close',
  232. );
  233. $this->field_option( 'basic-options', $field, $args );
  234. /*
  235. * Advanced field options.
  236. */
  237. // Options open markup.
  238. $args = array(
  239. 'markup' => 'open',
  240. );
  241. $this->field_option( 'advanced-options', $field, $args );
  242. // Size.
  243. $this->field_option( 'size', $field );
  244. // Placeholder.
  245. $this->field_option( 'placeholder', $field );
  246. // Confirmation Placeholder.
  247. $lbl = $this->field_element(
  248. 'label',
  249. $field,
  250. array(
  251. 'slug' => 'confirmation_placeholder',
  252. 'value' => esc_html__( 'Confirmation Placeholder Text', 'wpforms-lite' ),
  253. 'tooltip' => esc_html__( 'Enter text for the confirmation field placeholder.', 'wpforms-lite' ),
  254. ),
  255. false
  256. );
  257. $fld = $this->field_element(
  258. 'text',
  259. $field,
  260. array(
  261. 'slug' => 'confirmation_placeholder',
  262. 'value' => ! empty( $field['confirmation_placeholder'] ) ? esc_attr( $field['confirmation_placeholder'] ) : '',
  263. ),
  264. false
  265. );
  266. $args = array(
  267. 'slug' => 'confirmation_placeholder',
  268. 'content' => $lbl . $fld,
  269. );
  270. $this->field_element( 'row', $field, $args );
  271. // Default value.
  272. $this->field_option( 'default_value', $field );
  273. $filter_type_label = $this->field_element(
  274. 'label',
  275. $field,
  276. [
  277. 'slug' => 'filter_type',
  278. 'value' => esc_html__( 'Allowlist / Denylist', 'wpforms-lite' ),
  279. 'tooltip' => esc_html__( 'Restrict which email addresses are allowed. Be sure to separate each email address with a comma.', 'wpforms-lite' ),
  280. ],
  281. false
  282. );
  283. $filter_type_field = $this->field_element(
  284. 'select',
  285. $field,
  286. [
  287. 'slug' => 'filter_type',
  288. 'value' => ! empty( $field['filter_type'] ) ? esc_attr( $field['filter_type'] ) : '',
  289. 'options' => [
  290. '' => esc_html__( 'None', 'wpforms-lite' ),
  291. 'allowlist' => esc_html__( 'Allowlist', 'wpforms-lite' ),
  292. 'denylist' => esc_html__( 'Denylist', 'wpforms-lite' ),
  293. ],
  294. ],
  295. false
  296. );
  297. $this->field_element(
  298. 'row',
  299. $field,
  300. [
  301. 'slug' => 'filter_type',
  302. 'content' => $filter_type_label . $filter_type_field,
  303. ]
  304. );
  305. $this->field_element(
  306. 'row',
  307. $field,
  308. [
  309. 'slug' => 'allowlist',
  310. 'content' => $this->field_element(
  311. 'textarea',
  312. $field,
  313. [
  314. 'slug' => 'allowlist',
  315. 'value' => ! empty( $field['allowlist'] ) ? esc_attr( $this->decode_email_patterns_rules_list( $field['allowlist'] ) ) : '',
  316. ],
  317. false
  318. ),
  319. ]
  320. );
  321. $this->field_element(
  322. 'row',
  323. $field,
  324. [
  325. 'slug' => 'denylist',
  326. 'content' => $this->field_element(
  327. 'textarea',
  328. $field,
  329. [
  330. 'slug' => 'denylist',
  331. 'value' => ! empty( $field['denylist'] ) ? esc_attr( $this->decode_email_patterns_rules_list( $field['denylist'] ) ) : '',
  332. ],
  333. false
  334. ),
  335. ]
  336. );
  337. // Custom CSS classes.
  338. $this->field_option( 'css', $field );
  339. // Hide Label.
  340. $this->field_option( 'label_hide', $field );
  341. // Hide sublabels.
  342. $this->field_option( 'sublabel_hide', $field );
  343. // Options close markup.
  344. $args = [
  345. 'markup' => 'close',
  346. ];
  347. $this->field_option( 'advanced-options', $field, $args );
  348. }
  349. /**
  350. * Field preview inside the builder.
  351. *
  352. * @since 1.0.0
  353. * @param array $field
  354. */
  355. public function field_preview( $field ) {
  356. // Define data.
  357. $placeholder = ! empty( $field['placeholder'] ) ? esc_attr( $field['placeholder'] ) : '';
  358. $confirm_placeholder = ! empty( $field['confirmation_placeholder'] ) ? esc_attr( $field['confirmation_placeholder'] ) : '';
  359. $confirm = ! empty( $field['confirmation'] ) ? 'enabled' : 'disabled';
  360. // Label.
  361. $this->field_preview_option( 'label', $field );
  362. ?>
  363. <div class="wpforms-confirm wpforms-confirm-<?php echo sanitize_html_class( $confirm ); ?>">
  364. <div class="wpforms-confirm-primary">
  365. <input type="email" placeholder="<?php echo esc_attr( $placeholder ); ?>" class="primary-input" readonly>
  366. <label class="wpforms-sub-label"><?php esc_html_e( 'Email', 'wpforms-lite' ); ?></label>
  367. </div>
  368. <div class="wpforms-confirm-confirmation">
  369. <input type="email" placeholder="<?php echo esc_attr( $confirm_placeholder ); ?>" class="secondary-input" readonly>
  370. <label class="wpforms-sub-label"><?php esc_html_e( 'Confirm Email', 'wpforms-lite' ); ?></label>
  371. </div>
  372. </div>
  373. <?php
  374. // Description.
  375. $this->field_preview_option( 'description', $field );
  376. }
  377. /**
  378. * Field display on the form front-end.
  379. *
  380. * @since 1.0.0
  381. * @param array $field
  382. * @param array $deprecated
  383. * @param array $form_data
  384. */
  385. public function field_display( $field, $deprecated, $form_data ) {
  386. // Define data.
  387. $form_id = absint( $form_data['id'] );
  388. $confirmation = ! empty( $field['confirmation'] );
  389. $primary = $field['properties']['inputs']['primary'];
  390. $secondary = ! empty( $field['properties']['inputs']['secondary'] ) ? $field['properties']['inputs']['secondary'] : '';
  391. // Standard email field.
  392. if ( ! $confirmation ) {
  393. // Primary field.
  394. printf(
  395. '<input type="email" %s %s>',
  396. wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ),
  397. esc_attr( $primary['required'] )
  398. );
  399. $this->field_display_error( 'primary', $field );
  400. // Confirmation email field configuration.
  401. } else {
  402. // Row wrapper.
  403. echo '<div class="wpforms-field-row wpforms-field-' . sanitize_html_class( $field['size'] ) . '">';
  404. // Primary field.
  405. echo '<div ' . wpforms_html_attributes( false, $primary['block'] ) . '>';
  406. $this->field_display_sublabel( 'primary', 'before', $field );
  407. printf(
  408. '<input type="email" %s %s>',
  409. wpforms_html_attributes( $primary['id'], $primary['class'], $primary['data'], $primary['attr'] ),
  410. $primary['required']
  411. );
  412. $this->field_display_sublabel( 'primary', 'after', $field );
  413. $this->field_display_error( 'primary', $field );
  414. echo '</div>';
  415. // Secondary field.
  416. echo '<div ' . wpforms_html_attributes( false, $secondary['block'] ) . '>';
  417. $this->field_display_sublabel( 'secondary', 'before', $field );
  418. printf(
  419. '<input type="email" %s %s>',
  420. wpforms_html_attributes( $secondary['id'], $secondary['class'], $secondary['data'], $secondary['attr'] ),
  421. $secondary['required']
  422. );
  423. $this->field_display_sublabel( 'secondary', 'after', $field );
  424. $this->field_display_error( 'secondary', $field );
  425. echo '</div>';
  426. echo '</div>';
  427. } // End if().
  428. }
  429. /**
  430. * Format and sanitize field.
  431. *
  432. * @since 1.3.0
  433. * @param int $field_id Field ID.
  434. * @param mixed $field_submit Field value that was submitted.
  435. * @param array $form_data Form data and settings.
  436. */
  437. public function format( $field_id, $field_submit, $form_data ) {
  438. // Define data.
  439. if ( is_array( $field_submit ) ) {
  440. $value = ! empty( $field_submit['primary'] ) ? $field_submit['primary'] : '';
  441. } else {
  442. $value = ! empty( $field_submit ) ? $field_submit : '';
  443. }
  444. $name = ! empty( $form_data['fields'][ $field_id ] ['label'] ) ? $form_data['fields'][ $field_id ]['label'] : '';
  445. // Set final field details.
  446. wpforms()->process->fields[ $field_id ] = array(
  447. 'name' => sanitize_text_field( $name ),
  448. 'value' => sanitize_text_field( $this->decode_punycode( $value ) ),
  449. 'id' => absint( $field_id ),
  450. 'type' => $this->type,
  451. );
  452. }
  453. /**
  454. * Validate field on form submit.
  455. *
  456. * @since 1.0.0
  457. *
  458. * @param int $field_id Field ID.
  459. * @param mixed $field_submit Field value that was submitted.
  460. * @param array $form_data Form data and settings.
  461. */
  462. public function validate( $field_id, $field_submit, $form_data ) { // phpcs:ignore Generic.Metrics.CyclomaticComplexity.TooHigh
  463. $form_id = (int) $form_data['id'];
  464. parent::validate( $field_id, $field_submit, $form_data );
  465. if ( ! is_array( $field_submit ) && ! empty( $field_submit ) ) {
  466. $field_submit = [
  467. 'primary' => $field_submit,
  468. ];
  469. }
  470. if ( ! empty( $field_submit['primary'] ) ) {
  471. $field_submit['primary'] = $this->encode_punycode( $field_submit['primary'] );
  472. }
  473. // Validate email field with confirmation.
  474. if ( isset( $form_data['fields'][ $field_id ]['confirmation'] ) && ! empty( $field_submit['primary'] ) && ! empty( $field_submit['secondary'] ) ) {
  475. if ( ! is_email( $field_submit['primary'] ) ) {
  476. wpforms()->process->errors[ $form_id ][ $field_id ] = esc_html__( 'The provided email is not valid.', 'wpforms-lite' );
  477. } elseif ( $field_submit['primary'] !== $this->encode_punycode( $field_submit['secondary'] ) ) {
  478. wpforms()->process->errors[ $form_id ][ $field_id ] = esc_html__( 'The provided emails do not match.', 'wpforms-lite' );
  479. } elseif ( ! $this->is_restricted_email( $field_submit['primary'], $form_data['fields'][ $field_id ] ) ) {
  480. wpforms()->process->errors[ $form_id ][ $field_id ] = wpforms_setting( 'validation-email-restricted', esc_html__( 'This email address is not allowed.', 'wpforms-lite' ) );
  481. }
  482. }
  483. // Validate regular email field, without confirmation.
  484. if ( ! isset( $form_data['fields'][ $field_id ]['confirmation'] ) && ! empty( $field_submit['primary'] ) ) {
  485. if ( ! is_email( $field_submit['primary'] ) ) {
  486. wpforms()->process->errors[ $form_id ][ $field_id ] = esc_html__( 'The provided email is not valid.', 'wpforms-lite' );
  487. } elseif ( ! $this->is_restricted_email( $field_submit['primary'], $form_data['fields'][ $field_id ] ) ) {
  488. wpforms()->process->errors[ $form_id ][ $field_id ] = wpforms_setting( 'validation-email-restricted', esc_html__( 'This email address is not allowed.', 'wpforms-lite' ) );
  489. }
  490. }
  491. }
  492. /**
  493. * Ajax handler to detect restricted email.
  494. *
  495. * @since 1.6.3
  496. */
  497. public function ajax_check_restricted_email() {
  498. $form_id = filter_input( INPUT_POST, 'form_id', FILTER_SANITIZE_NUMBER_INT );
  499. $field_id = filter_input( INPUT_POST, 'field_id', FILTER_SANITIZE_NUMBER_INT );
  500. $email = filter_input( INPUT_POST, 'email', FILTER_SANITIZE_STRING );
  501. if ( ! $form_id || ! $field_id || ! $email ) {
  502. wp_send_json_error();
  503. }
  504. $form_data = wpforms()->form->get(
  505. $form_id,
  506. [ 'content_only' => true ]
  507. );
  508. if ( empty( $form_data['fields'][ $field_id ] ) ) {
  509. wp_send_json_error();
  510. }
  511. wp_send_json_success(
  512. $this->is_restricted_email( $email, $form_data['fields'][ $field_id ] )
  513. );
  514. }
  515. /**
  516. * Sanitize restricted rules.
  517. *
  518. * @since 1.6.3
  519. */
  520. public function ajax_sanitize_restricted_rules() {
  521. // Run a security check.
  522. check_ajax_referer( 'wpforms-builder', 'nonce' );
  523. $content = filter_input( INPUT_GET, 'content', FILTER_SANITIZE_STRING );
  524. if ( ! $content ) {
  525. wp_send_json_error();
  526. }
  527. $rules = $this->sanitize_restricted_rules( $content );
  528. wp_send_json_success(
  529. implode(
  530. PHP_EOL,
  531. array_map(
  532. function ( $rule ) {
  533. return $this->decode_punycode( $rule );
  534. },
  535. $rules
  536. )
  537. )
  538. );
  539. }
  540. /**
  541. * Sanitize restricted rules.
  542. *
  543. * @since 1.6.3
  544. *
  545. * @param string $content Content.
  546. *
  547. * @return array
  548. */
  549. private function sanitize_restricted_rules( $content ) {
  550. $patterns = array_filter( preg_split( '/\r\n|\r|\n|,/', $content ) );
  551. foreach ( $patterns as $key => $pattern ) {
  552. $pattern = trim( $pattern );
  553. if ( ! $pattern ) {
  554. unset( $patterns[ $key ] );
  555. }
  556. $pattern = $this->encode_punycode( mb_strtolower( $pattern, self::ENCODING ) );
  557. $email_parts = explode( '@', $pattern );
  558. $local_part = preg_replace(
  559. /**
  560. * Allowed characters in the "name" part of the email:
  561. *
  562. * - latin letters, lowercase
  563. * - numbers
  564. * - underscore
  565. * - period <.>
  566. * - asterisk <*> (used for wildcards)
  567. * - hyphen
  568. *
  569. * @todo Synchronize regex with WordPress' `is_email()` function.
  570. */
  571. '/[^a-z0-9_.*-]/',
  572. '',
  573. array_shift( $email_parts )
  574. );
  575. if ( empty( $email_parts ) ) {
  576. $patterns[ $key ] = $local_part;
  577. continue;
  578. }
  579. $domain_part = preg_replace(
  580. /**
  581. * Allowed characters in the "domain" part of the email:
  582. *
  583. * - latin letters, lowercase
  584. * - numbers
  585. * - period <.>
  586. * - asterisk <*> (used for wildcards)
  587. * - hyphen
  588. */
  589. '/[^a-z0-9.*-]/',
  590. '',
  591. $email_parts[0]
  592. );
  593. $patterns[ $key ] = $local_part . '@' . $domain_part;
  594. }
  595. return ! empty( $patterns ) ? array_filter( $patterns ) : [];
  596. }
  597. /**
  598. * The check is a restricted email.
  599. *
  600. * @since 1.6.3
  601. *
  602. * @param string $email Email string.
  603. * @param array $field Field data.
  604. *
  605. * @return bool
  606. */
  607. private function is_restricted_email( $email, $field ) {
  608. if ( empty( $field['filter_type'] ) || empty( $field[ $field['filter_type'] ] ) ) {
  609. return true;
  610. }
  611. $email = $this->encode_punycode( mb_strtolower( $email, self::ENCODING ) );
  612. $patterns = $this->sanitize_restricted_rules( $field[ $field['filter_type'] ] );
  613. $patterns = array_unique( array_map( [ $this, 'sanitize_email_pattern' ], $patterns ) );
  614. $check = $field['filter_type'] === 'allowlist';
  615. foreach ( $patterns as $pattern ) {
  616. if ( (bool) preg_match( '/' . $pattern . '/', $email ) === true ) {
  617. return $check;
  618. }
  619. }
  620. return ! $check;
  621. }
  622. /**
  623. * Sanitize from email patter a REGEX pattern.
  624. *
  625. * @since 1.6.3
  626. *
  627. * @param string $pattern Pattern line.
  628. *
  629. * @return string
  630. */
  631. private function sanitize_email_pattern( $pattern ) {
  632. // Create regex pattern from a string.
  633. return '^' . str_replace( [ '.', '*' ], [ '\.', '.*' ], $pattern ) . '$';
  634. }
  635. /**
  636. * Sanitize allow/deny list before saving.
  637. *
  638. * @since 1.6.8
  639. *
  640. * @param array $form Form array which is usable with `wp_update_post()`.
  641. * @param array $data Data retrieved from $_POST and processed.
  642. * @param array $args Empty by default, may contain custom data not intended to be saved, but used for processing.
  643. *
  644. * @return array
  645. */
  646. public function save_form_args( $form, $data, $args ) {
  647. // Get a filtered form content.
  648. $form_data = json_decode( stripslashes( $form['post_content'] ), true );
  649. foreach ( $form_data['fields'] as $key => $field ) {
  650. if ( $field['type'] !== 'email' ) {
  651. continue;
  652. }
  653. $form_data['fields'][ $key ]['allowlist'] = ! empty( $field['allowlist'] ) ? implode( PHP_EOL, $this->sanitize_restricted_rules( $field['allowlist'] ) ) : '';
  654. $form_data['fields'][ $key ]['denylist'] = ! empty( $field['denylist'] ) ? implode( PHP_EOL, $this->sanitize_restricted_rules( $field['denylist'] ) ) : '';
  655. }
  656. $form['post_content'] = wpforms_encode( $form_data );
  657. return $form;
  658. }
  659. /**
  660. * Get Punycode lib class.
  661. *
  662. * @since 1.6.9
  663. *
  664. * @return \TrueBV\Punycode
  665. */
  666. private function get_punycode() {
  667. static $punycode;
  668. if ( ! $punycode ) {
  669. $punycode = new \TrueBV\Punycode();
  670. }
  671. return $punycode;
  672. }
  673. /**
  674. * Get email patterns parts splitted by @ and *.
  675. *
  676. * @since 1.6.9
  677. *
  678. * @param string $email_pattern Email pattern.
  679. *
  680. * @return array
  681. */
  682. private function get_email_pattern_parts( $email_pattern ) {
  683. $parts = preg_split( '/[*@.]/', $email_pattern, - 1, PREG_SPLIT_OFFSET_CAPTURE );
  684. if ( empty( $parts ) ) {
  685. return [];
  686. }
  687. foreach ( $parts as $key => $part ) {
  688. // Replace split symbol position to the split symbol.
  689. $part[1] = $part[1] > 0 ? $email_pattern[ $part[1] - 1 ] : '';
  690. $parts[ $key ] = $part;
  691. }
  692. return $parts;
  693. }
  694. /**
  695. * Glue email patterns parts.
  696. *
  697. * @since 1.6.9
  698. *
  699. * @param array $parts Email pattern parts.
  700. *
  701. * @return string
  702. */
  703. private function glue_email_pattern_parts( $parts ) {
  704. $email_pattern = '';
  705. foreach ( $parts as $part ) {
  706. $email_pattern .= $part[1] . $part[0];
  707. }
  708. return $email_pattern;
  709. }
  710. /**
  711. * Decode email patterns rules list.
  712. *
  713. * @since 1.6.9
  714. *
  715. * @param string $rules Patterns rules list.
  716. *
  717. * @return string
  718. */
  719. private function decode_email_patterns_rules_list( $rules ) {
  720. return implode(
  721. PHP_EOL,
  722. array_map(
  723. function ( $rule ) {
  724. return $this->decode_punycode( $rule );
  725. },
  726. array_filter( preg_split( '/\r\n|\r|\n|,/', $rules ) )
  727. )
  728. );
  729. }
  730. /**
  731. * Encode email pattern.
  732. *
  733. * @since 1.6.9
  734. *
  735. * @param string $email_pattern Email pattern.
  736. *
  737. * @return string
  738. */
  739. private function encode_punycode( $email_pattern ) {
  740. return $this->transform_punycode( $email_pattern, [ $this->get_punycode(), 'encode' ] );
  741. }
  742. /**
  743. * Decode email pattern.
  744. *
  745. * @since 1.6.9
  746. *
  747. * @param string $email_pattern Email pattern.
  748. *
  749. * @return string
  750. */
  751. private function decode_punycode( $email_pattern ) {
  752. return $this->transform_punycode( $email_pattern, [ $this->get_punycode(), 'decode' ] );
  753. }
  754. /**
  755. * Transform email pattern.
  756. *
  757. * @since 1.6.9
  758. *
  759. * @param string $email_pattern Email pattern.
  760. * @param callable $callback Punycode callback.
  761. *
  762. * @return string
  763. */
  764. private function transform_punycode( $email_pattern, callable $callback ) {
  765. $parts = $this->get_email_pattern_parts( $email_pattern );
  766. foreach ( $parts as $key => $part ) {
  767. if ( ! $part[0] ) {
  768. continue;
  769. }
  770. $parts[ $key ][0] = call_user_func( $callback, $part[0] );
  771. }
  772. return $this->glue_email_pattern_parts( $parts );
  773. }
  774. }