Changeset 39219
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php
r39177 r39219 418 418 419 419 if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) { 420 return $ret['errors']; 420 $error = new WP_Error( 'rest_invalid_param', __( 'Invalid user parameter(s).' ), array( 'status' => 400 ) ); 421 foreach ( $ret['errors']->errors as $code => $messages ) { 422 foreach ( $messages as $message ) { 423 $error->add( $code, $message ); 424 } 425 if ( $error_data = $error->get_error_data( $code ) ) { 426 $error->add_data( $error_data, $code ); 427 } 428 } 429 return $error; 421 430 } 422 431 } … … 430 439 431 440 $user->ID = $user_id; 432 $user_id = wp_update_user( $user);441 $user_id = wp_update_user( wp_slash( (array) $user ) ); 433 442 434 443 if ( is_wp_error( $user_id ) ) { … … 438 447 add_user_to_blog( get_site()->id, $user_id, '' ); 439 448 } else { 440 $user_id = wp_insert_user( $user);449 $user_id = wp_insert_user( wp_slash( (array) $user ) ); 441 450 442 451 if ( is_wp_error( $user_id ) ) { … … 553 562 $user->ID = $id; 554 563 555 $user_id = wp_update_user( $user);564 $user_id = wp_update_user( wp_slash( (array) $user ) ); 556 565 557 566 if ( is_wp_error( $user_id ) ) { … … 995 1004 996 1005 return true; 1006 } 1007 1008 /** 1009 * Check a username for the REST API. 1010 * 1011 * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. 1012 * 1013 * @since 4.7.0 1014 * 1015 * @param mixed $value The username submitted in the request. 1016 * @param WP_REST_Request $request Full details about the request. 1017 * @param string $param The parameter name. 1018 * @return WP_Error|string The sanitized username, if valid, otherwise an error. 1019 */ 1020 public function check_username( $value, $request, $param ) { 1021 $username = (string) rest_sanitize_value_from_schema( $value, $request, $param ); 1022 1023 if ( ! validate_username( $username ) ) { 1024 return new WP_Error( 'rest_user_invalid_username', __( 'Username contains invalid characters.' ), array( 'status' => 400 ) ); 1025 } 1026 1027 /** This filter is documented in wp-includes/user.php */ 1028 $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); 1029 1030 if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ) ) ) { 1031 return new WP_Error( 'rest_user_invalid_username', __( 'Sorry, that username is not allowed.' ), array( 'status' => 400 ) ); 1032 } 1033 1034 return $username; 1035 } 1036 1037 /** 1038 * Check a user password for the REST API. 1039 * 1040 * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. 1041 * 1042 * @since 4.7.0 1043 * 1044 * @param mixed $value The password submitted in the request. 1045 * @param WP_REST_Request $request Full details about the request. 1046 * @param string $param The parameter name. 1047 * @return WP_Error|string The sanitized password, if valid, otherwise an error. 1048 */ 1049 public function check_user_password( $value, $request, $param ) { 1050 $password = (string) rest_sanitize_value_from_schema( $value, $request, $param ); 1051 1052 if ( empty( $password ) ) { 1053 return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot be empty.' ), array( 'status' => 400 ) ); 1054 } 1055 1056 if ( false !== strpos( $password, "\\" ) ) { 1057 return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot contain the "\\" character.' ), array( 'status' => 400 ) ); 1058 } 1059 1060 return $password; 997 1061 } 998 1062 … … 1023 1087 'required' => true, 1024 1088 'arg_options' => array( 1025 'sanitize_callback' => 'sanitize_user',1089 'sanitize_callback' => array( $this, 'check_username' ), 1026 1090 ), 1027 1091 ), … … 1067 1131 'type' => 'string', 1068 1132 'context' => array( 'embed', 'view', 'edit' ), 1069 'arg_options' => array(1070 'sanitize_callback' => 'wp_filter_post_kses',1071 ),1072 1133 ), 1073 1134 'link' => array( … … 1120 1181 'context' => array(), // Password is never displayed. 1121 1182 'required' => true, 1183 'arg_options' => array( 1184 'sanitize_callback' => array( $this, 'check_user_password' ), 1185 ), 1122 1186 ), 1123 1187 'capabilities' => array( -
trunk/tests/phpunit/includes/functions.php
r38907 r39219 162 162 return $uploads; 163 163 } 164 165 // Skip `setcookie` calls in auth_cookie functions due to warning: 166 // Cannot modify header information - headers already sent by ... 167 168 function wp_set_auth_cookie( $user_id, $remember = false, $secure = '', $token = '' ) { 169 $auth_cookie = null; 170 $expire = null; 171 $expiration = null; 172 $user_id = null; 173 $scheme = null; 174 /** This action is documented in wp-inclues/pluggable.php */ 175 do_action( 'set_auth_cookie', $auth_cookie, $expire, $expiration, $user_id, $scheme ); 176 $logged_in_cookie = null; 177 /** This action is documented in wp-inclues/pluggable.php */ 178 do_action( 'set_logged_in_cookie', $logged_in_cookie, $expire, $expiration, $user_id, 'logged_in' ); 179 } 180 181 function wp_clear_auth_cookie() { 182 /** This action is documented in wp-inclues/pluggable.php */ 183 do_action( 'clear_auth_cookie' ); 184 } -
trunk/tests/phpunit/tests/rest-api/rest-users-controller.php
r39177 r39219 11 11 */ 12 12 class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { 13 protected static $superadmin; 13 14 protected static $user; 14 15 protected static $editor; … … 16 17 17 18 public static function wpSetUpBeforeClass( $factory ) { 19 self::$superadmin = $factory->user->create( array( 20 'role' => 'administrator', 21 'user_login' => 'superadmin', 22 ) ); 18 23 self::$user = $factory->user->create( array( 19 24 'role' => 'administrator', … … 26 31 if ( is_multisite() ) { 27 32 self::$site = $factory->blog->create( array( 'domain' => 'rest.wordpress.org', 'path' => '/' ) ); 33 update_site_option( 'site_admins', array( 'superadmin' ) ); 28 34 } 29 35 } … … 176 182 $response = $this->server->dispatch( $request ); 177 183 $headers = $response->get_headers(); 178 $this->assertEquals( 5 0, $headers['X-WP-Total'] );179 $this->assertEquals( 5, $headers['X-WP-TotalPages'] );184 $this->assertEquals( 51, $headers['X-WP-Total'] ); 185 $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); 180 186 $next_link = add_query_arg( array( 181 187 'page' => 2, … … 191 197 $response = $this->server->dispatch( $request ); 192 198 $headers = $response->get_headers(); 193 $this->assertEquals( 5 1, $headers['X-WP-Total'] );199 $this->assertEquals( 52, $headers['X-WP-Total'] ); 194 200 $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); 195 201 $prev_link = add_query_arg( array( … … 206 212 $response = $this->server->dispatch( $request ); 207 213 $headers = $response->get_headers(); 208 $this->assertEquals( 5 1, $headers['X-WP-Total'] );214 $this->assertEquals( 52, $headers['X-WP-Total'] ); 209 215 $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); 210 216 $prev_link = add_query_arg( array( … … 218 224 $response = $this->server->dispatch( $request ); 219 225 $headers = $response->get_headers(); 220 $this->assertEquals( 5 1, $headers['X-WP-Total'] );226 $this->assertEquals( 52, $headers['X-WP-Total'] ); 221 227 $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); 222 228 $prev_link = add_query_arg( array( … … 394 400 $request->set_param( 'offset', 1 ); 395 401 $response = $this->server->dispatch( $request ); 396 $this->assertCount( 3, $response->get_data() );402 $this->assertCount( 4, $response->get_data() ); 397 403 // 'offset' works with 'per_page' 398 404 $request->set_param( 'per_page', 2 ); … … 716 722 } 717 723 724 public function test_create_item_invalid_username() { 725 $this->allow_user_to_manage_multisite(); 726 wp_set_current_user( self::$user ); 727 728 $params = array( 729 'username' => '¯\_(ツ)_/¯', 730 'password' => 'testpassword', 731 'email' => 'test@example.com', 732 'name' => 'Test User', 733 'nickname' => 'testuser', 734 'slug' => 'test-user', 735 'roles' => array( 'editor' ), 736 'description' => 'New API User', 737 'url' => 'http://example.com', 738 ); 739 740 // Username rules are different (more strict) for multisite; see `wpmu_validate_user_signup` 741 if ( is_multisite() ) { 742 $params['username'] = 'no-dashes-allowed'; 743 } 744 745 $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); 746 $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); 747 $request->set_body_params( $params ); 748 749 $response = $this->server->dispatch( $request ); 750 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 751 752 $data = $response->get_data(); 753 if ( is_multisite() ) { 754 $this->assertInternalType( 'array', $data['additional_errors'] ); 755 $this->assertCount( 1, $data['additional_errors'] ); 756 $error = $data['additional_errors'][0]; 757 $this->assertEquals( 'user_name', $error['code'] ); 758 $this->assertEquals( 'Usernames can only contain lowercase letters (a-z) and numbers.', $error['message'] ); 759 } else { 760 $this->assertInternalType( 'array', $data['data']['params'] ); 761 $errors = $data['data']['params']; 762 $this->assertInternalType( 'string', $errors['username'] ); 763 $this->assertEquals( 'Username contains invalid characters.', $errors['username'] ); 764 } 765 } 766 767 function get_illegal_user_logins() { 768 return array( 'nope' ); 769 } 770 771 public function test_create_item_illegal_username() { 772 $this->allow_user_to_manage_multisite(); 773 wp_set_current_user( self::$user ); 774 775 add_filter( 'illegal_user_logins', array( $this, 'get_illegal_user_logins' ) ); 776 777 $params = array( 778 'username' => 'nope', 779 'password' => 'testpassword', 780 'email' => 'test@example.com', 781 'name' => 'Test User', 782 'nickname' => 'testuser', 783 'slug' => 'test-user', 784 'roles' => array( 'editor' ), 785 'description' => 'New API User', 786 'url' => 'http://example.com', 787 ); 788 789 $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); 790 $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); 791 $request->set_body_params( $params ); 792 793 $response = $this->server->dispatch( $request ); 794 795 remove_filter( 'illegal_user_logins', array( $this, 'get_illegal_user_logins' ) ); 796 797 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 798 799 $data = $response->get_data(); 800 $this->assertInternalType( 'array', $data['data']['params'] ); 801 $errors = $data['data']['params']; 802 $this->assertInternalType( 'string', $errors['username'] ); 803 $this->assertEquals( 'Sorry, that username is not allowed.', $errors['username'] ); 804 } 805 718 806 public function test_create_new_network_user_on_site_does_not_add_user_to_sub_site() { 719 807 if ( ! is_multisite() ) { … … 811 899 wpmu_delete_user( $user_id ); 812 900 813 $this->assertErrorResponse( 'user_name', $switched_response ); 901 $this->assertErrorResponse( 'rest_invalid_param', $switched_response, 400 ); 902 $data = $switched_response->get_data(); 903 $this->assertInternalType( 'array', $data['additional_errors'] ); 904 $this->assertCount( 2, $data['additional_errors'] ); 905 $errors = $data['additional_errors']; 906 foreach ( $errors as $error ) { 907 // Check the code matches one we know. 908 $this->assertContains( $error['code'], array( 'user_name', 'user_email' ) ); 909 if ( 'user_name' === $error['code'] ) { 910 $this->assertEquals( 'Sorry, that username already exists!', $error['message'] ); 911 } else { 912 $this->assertEquals( 'Sorry, that email address is already used!', $error['message'] ); 913 } 914 } 814 915 } 815 916 … … 1304 1405 1305 1406 $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); 1407 } 1408 1409 public function test_update_item_invalid_password() { 1410 $this->allow_user_to_manage_multisite(); 1411 wp_set_current_user( self::$user ); 1412 1413 $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', self::$editor ) ); 1414 1415 $request->set_param( 'password', 'no\\backslashes\\allowed' ); 1416 $response = $this->server->dispatch( $request ); 1417 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 1418 1419 $request->set_param( 'password', '' ); 1420 $response = $this->server->dispatch( $request ); 1421 $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); 1422 } 1423 1424 public function verify_user_roundtrip( $input = array(), $expected_output = array() ) { 1425 if ( isset( $input['id'] ) ) { 1426 // Existing user; don't try to create one 1427 $user_id = $input['id']; 1428 } else { 1429 // Create a new user 1430 $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); 1431 foreach ( $input as $name => $value ) { 1432 $request->set_param( $name, $value ); 1433 } 1434 $request->set_param( 'email', 'cbg@androidsdungeon.com' ); 1435 $response = $this->server->dispatch( $request ); 1436 $this->assertEquals( 201, $response->get_status() ); 1437 $actual_output = $response->get_data(); 1438 1439 // Compare expected API output to actual API output 1440 $this->assertEquals( $expected_output['username'] , $actual_output['username'] ); 1441 $this->assertEquals( $expected_output['name'] , $actual_output['name'] ); 1442 $this->assertEquals( $expected_output['first_name'] , $actual_output['first_name'] ); 1443 $this->assertEquals( $expected_output['last_name'] , $actual_output['last_name'] ); 1444 $this->assertEquals( $expected_output['url'] , $actual_output['url'] ); 1445 $this->assertEquals( $expected_output['description'], $actual_output['description'] ); 1446 $this->assertEquals( $expected_output['nickname'] , $actual_output['nickname'] ); 1447 1448 // Compare expected API output to WP internal values 1449 $user = get_userdata( $actual_output['id'] ); 1450 $this->assertEquals( $expected_output['username'] , $user->user_login ); 1451 $this->assertEquals( $expected_output['name'] , $user->display_name ); 1452 $this->assertEquals( $expected_output['first_name'] , $user->first_name ); 1453 $this->assertEquals( $expected_output['last_name'] , $user->last_name ); 1454 $this->assertEquals( $expected_output['url'] , $user->user_url ); 1455 $this->assertEquals( $expected_output['description'], $user->description ); 1456 $this->assertEquals( $expected_output['nickname'] , $user->nickname ); 1457 $this->assertTrue( wp_check_password( addslashes( $expected_output['password'] ), $user->user_pass ) ); 1458 1459 $user_id = $actual_output['id']; 1460 } 1461 1462 // Update the user 1463 $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); 1464 foreach ( $input as $name => $value ) { 1465 if ( 'username' !== $name ) { 1466 $request->set_param( $name, $value ); 1467 } 1468 } 1469 $response = $this->server->dispatch( $request ); 1470 $this->assertEquals( 200, $response->get_status() ); 1471 $actual_output = $response->get_data(); 1472 1473 // Compare expected API output to actual API output 1474 if ( isset( $expected_output['username'] ) ) { 1475 $this->assertEquals( $expected_output['username'], $actual_output['username'] ); 1476 } 1477 $this->assertEquals( $expected_output['name'] , $actual_output['name'] ); 1478 $this->assertEquals( $expected_output['first_name'] , $actual_output['first_name'] ); 1479 $this->assertEquals( $expected_output['last_name'] , $actual_output['last_name'] ); 1480 $this->assertEquals( $expected_output['url'] , $actual_output['url'] ); 1481 $this->assertEquals( $expected_output['description'], $actual_output['description'] ); 1482 $this->assertEquals( $expected_output['nickname'] , $actual_output['nickname'] ); 1483 1484 // Compare expected API output to WP internal values 1485 $user = get_userdata( $actual_output['id'] ); 1486 if ( isset( $expected_output['username'] ) ) { 1487 $this->assertEquals( $expected_output['username'], $user->user_login ); 1488 } 1489 $this->assertEquals( $expected_output['name'] , $user->display_name ); 1490 $this->assertEquals( $expected_output['first_name'] , $user->first_name ); 1491 $this->assertEquals( $expected_output['last_name'] , $user->last_name ); 1492 $this->assertEquals( $expected_output['url'] , $user->user_url ); 1493 $this->assertEquals( $expected_output['description'], $user->description ); 1494 $this->assertEquals( $expected_output['nickname'] , $user->nickname ); 1495 $this->assertTrue( wp_check_password( addslashes( $expected_output['password'] ), $user->user_pass ) ); 1496 } 1497 1498 public function test_user_roundtrip_as_editor() { 1499 wp_set_current_user( self::$editor ); 1500 $this->assertEquals( ! is_multisite(), current_user_can( 'unfiltered_html' ) ); 1501 $this->verify_user_roundtrip( array( 1502 'id' => self::$editor, 1503 'name' => '\o/ ¯\_(ツ)_/¯', 1504 'first_name' => '\o/ ¯\_(ツ)_/¯', 1505 'last_name' => '\o/ ¯\_(ツ)_/¯', 1506 'url' => '\o/ ¯\_(ツ)_/¯', 1507 'description' => '\o/ ¯\_(ツ)_/¯', 1508 'nickname' => '\o/ ¯\_(ツ)_/¯', 1509 'password' => 'o/ ¯_(ツ)_/¯ \'"', 1510 ), array( 1511 'name' => '\o/ ¯\_(ツ)_/¯', 1512 'first_name' => '\o/ ¯\_(ツ)_/¯', 1513 'last_name' => '\o/ ¯\_(ツ)_/¯', 1514 'url' => 'http://o/%20¯_(ツ)_/¯', 1515 'description' => '\o/ ¯\_(ツ)_/¯', 1516 'nickname' => '\o/ ¯\_(ツ)_/¯', 1517 'password' => 'o/ ¯_(ツ)_/¯ \'"', 1518 ) ); 1519 } 1520 1521 public function test_user_roundtrip_as_editor_html() { 1522 wp_set_current_user( self::$editor ); 1523 if ( is_multisite() ) { 1524 $this->assertFalse( current_user_can( 'unfiltered_html' ) ); 1525 $this->verify_user_roundtrip( array( 1526 'id' => self::$editor, 1527 'name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1528 'first_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1529 'last_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1530 'url' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1531 'description' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1532 'nickname' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1533 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1534 ), array( 1535 'name' => 'div strong', 1536 'first_name' => 'div strong', 1537 'last_name' => 'div strong', 1538 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', 1539 'description' => 'div <strong>strong</strong> oh noes', 1540 'nickname' => 'div strong', 1541 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1542 ) ); 1543 } else { 1544 $this->assertTrue( current_user_can( 'unfiltered_html' ) ); 1545 $this->verify_user_roundtrip( array( 1546 'id' => self::$editor, 1547 'name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1548 'first_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1549 'last_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1550 'url' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1551 'description' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1552 'nickname' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1553 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1554 ), array( 1555 'name' => 'div strong', 1556 'first_name' => 'div strong', 1557 'last_name' => 'div strong', 1558 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', 1559 'description' => 'div <strong>strong</strong> oh noes', 1560 'nickname' => 'div strong', 1561 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1562 ) ); 1563 } 1564 } 1565 1566 public function test_user_roundtrip_as_superadmin() { 1567 wp_set_current_user( self::$superadmin ); 1568 $this->assertTrue( current_user_can( 'unfiltered_html' ) ); 1569 $valid_username = is_multisite() ? 'noinvalidcharshere' : 'no-invalid-chars-here'; 1570 $this->verify_user_roundtrip( array( 1571 'username' => $valid_username, 1572 'name' => '\\\&\\\ & &invalid; < < &lt;', 1573 'first_name' => '\\\&\\\ & &invalid; < < &lt;', 1574 'last_name' => '\\\&\\\ & &invalid; < < &lt;', 1575 'url' => '\\\&\\\ & &invalid; < < &lt;', 1576 'description' => '\\\&\\\ & &invalid; < < &lt;', 1577 'nickname' => '\\\&\\\ & &invalid; < < &lt;', 1578 'password' => '& & &invalid; < < &lt;', 1579 ), array( 1580 'username' => $valid_username, 1581 'name' => '\\\&\\\ & &invalid; < < &lt;', 1582 'first_name' => '\\\&\\\ & &invalid; < < &lt;', 1583 'last_name' => '\\\&\\\ & &invalid; < < &lt;', 1584 'url' => 'http://&%20&%20&invalid;%20%20<%20&lt;', 1585 'description' => '\\\&\\\ & &invalid; < < &lt;', 1586 'nickname' => '\\\&\\\ & &invalid; < < &lt;', 1587 'password' => '& & &invalid; < < &lt;', 1588 ) ); 1589 } 1590 1591 public function test_user_roundtrip_as_superadmin_html() { 1592 wp_set_current_user( self::$superadmin ); 1593 $this->assertTrue( current_user_can( 'unfiltered_html' ) ); 1594 $valid_username = is_multisite() ? 'noinvalidcharshere' : 'no-invalid-chars-here'; 1595 $this->verify_user_roundtrip( array( 1596 'username' => $valid_username, 1597 'name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1598 'first_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1599 'last_name' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1600 'url' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1601 'description' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1602 'nickname' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1603 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1604 ), array( 1605 'username' => $valid_username, 1606 'name' => 'div strong', 1607 'first_name' => 'div strong', 1608 'last_name' => 'div strong', 1609 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', 1610 'description' => 'div <strong>strong</strong> oh noes', 1611 'nickname' => 'div strong', 1612 'password' => '<div>div</div> <strong>strong</strong> <script>oh noes</script>', 1613 ) ); 1306 1614 } 1307 1615
Note: See TracChangeset
for help on using the changeset viewer.