From ecb1e33d0d6917fbd0a8f6aedc50dad6196e6a48 Mon Sep 17 00:00:00 2001 From: Ryan McCue Date: Mon, 14 Nov 2016 07:12:31 +0000 Subject: [PATCH] REST API: Improve validation for usernames and passwords. Also improves the slashing of user data in the REST API to avoid data loss. Props jnylen0. Fixes #38739. git-svn-id: https://develop.svn.wordpress.org/trunk@39219 602fd350-edb4-49c9-b593-d223f7449a82 --- .../class-wp-rest-users-controller.php | 80 ++++- tests/phpunit/includes/functions.php | 21 ++ .../tests/rest-api/rest-users-controller.php | 322 +++++++++++++++++- 3 files changed, 408 insertions(+), 15 deletions(-) diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php index 949a66863d..4a693ba571 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-users-controller.php @@ -417,7 +417,16 @@ class WP_REST_Users_Controller extends WP_REST_Controller { $ret = wpmu_validate_user_signup( $user->user_login, $user->user_email ); if ( is_wp_error( $ret['errors'] ) && ! empty( $ret['errors']->errors ) ) { - return $ret['errors']; + $error = new WP_Error( 'rest_invalid_param', __( 'Invalid user parameter(s).' ), array( 'status' => 400 ) ); + foreach ( $ret['errors']->errors as $code => $messages ) { + foreach ( $messages as $message ) { + $error->add( $code, $message ); + } + if ( $error_data = $error->get_error_data( $code ) ) { + $error->add_data( $error_data, $code ); + } + } + return $error; } } @@ -429,7 +438,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller { } $user->ID = $user_id; - $user_id = wp_update_user( $user ); + $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; @@ -437,7 +446,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller { add_user_to_blog( get_site()->id, $user_id, '' ); } else { - $user_id = wp_insert_user( $user ); + $user_id = wp_insert_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; @@ -552,7 +561,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller { // Ensure we're operating on the same user we already checked. $user->ID = $id; - $user_id = wp_update_user( $user ); + $user_id = wp_update_user( wp_slash( (array) $user ) ); if ( is_wp_error( $user_id ) ) { return $user_id; @@ -996,6 +1005,61 @@ class WP_REST_Users_Controller extends WP_REST_Controller { return true; } + /** + * Check a username for the REST API. + * + * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. + * + * @since 4.7.0 + * + * @param mixed $value The username submitted in the request. + * @param WP_REST_Request $request Full details about the request. + * @param string $param The parameter name. + * @return WP_Error|string The sanitized username, if valid, otherwise an error. + */ + public function check_username( $value, $request, $param ) { + $username = (string) rest_sanitize_value_from_schema( $value, $request, $param ); + + if ( ! validate_username( $username ) ) { + return new WP_Error( 'rest_user_invalid_username', __( 'Username contains invalid characters.' ), array( 'status' => 400 ) ); + } + + /** This filter is documented in wp-includes/user.php */ + $illegal_logins = (array) apply_filters( 'illegal_user_logins', array() ); + + if ( in_array( strtolower( $username ), array_map( 'strtolower', $illegal_logins ) ) ) { + return new WP_Error( 'rest_user_invalid_username', __( 'Sorry, that username is not allowed.' ), array( 'status' => 400 ) ); + } + + return $username; + } + + /** + * Check a user password for the REST API. + * + * Performs a couple of checks like edit_user() in wp-admin/includes/user.php. + * + * @since 4.7.0 + * + * @param mixed $value The password submitted in the request. + * @param WP_REST_Request $request Full details about the request. + * @param string $param The parameter name. + * @return WP_Error|string The sanitized password, if valid, otherwise an error. + */ + public function check_user_password( $value, $request, $param ) { + $password = (string) rest_sanitize_value_from_schema( $value, $request, $param ); + + if ( empty( $password ) ) { + return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot be empty.' ), array( 'status' => 400 ) ); + } + + if ( false !== strpos( $password, "\\" ) ) { + return new WP_Error( 'rest_user_invalid_password', __( 'Passwords cannot contain the "\\" character.' ), array( 'status' => 400 ) ); + } + + return $password; + } + /** * Retrieves the user's schema, conforming to JSON Schema. * @@ -1022,7 +1086,7 @@ class WP_REST_Users_Controller extends WP_REST_Controller { 'context' => array( 'edit' ), 'required' => true, 'arg_options' => array( - 'sanitize_callback' => 'sanitize_user', + 'sanitize_callback' => array( $this, 'check_username' ), ), ), 'name' => array( @@ -1066,9 +1130,6 @@ class WP_REST_Users_Controller extends WP_REST_Controller { 'description' => __( 'Description of the resource.' ), 'type' => 'string', 'context' => array( 'embed', 'view', 'edit' ), - 'arg_options' => array( - 'sanitize_callback' => 'wp_filter_post_kses', - ), ), 'link' => array( 'description' => __( 'Author URL to the resource.' ), @@ -1119,6 +1180,9 @@ class WP_REST_Users_Controller extends WP_REST_Controller { 'type' => 'string', 'context' => array(), // Password is never displayed. 'required' => true, + 'arg_options' => array( + 'sanitize_callback' => array( $this, 'check_user_password' ), + ), ), 'capabilities' => array( 'description' => __( 'All capabilities assigned to the resource.' ), diff --git a/tests/phpunit/includes/functions.php b/tests/phpunit/includes/functions.php index a8702767ef..3a7c9bd98e 100644 --- a/tests/phpunit/includes/functions.php +++ b/tests/phpunit/includes/functions.php @@ -161,3 +161,24 @@ function _upload_dir_https( $uploads ) { return $uploads; } + +// Skip `setcookie` calls in auth_cookie functions due to warning: +// Cannot modify header information - headers already sent by ... + +function wp_set_auth_cookie( $user_id, $remember = false, $secure = '', $token = '' ) { + $auth_cookie = null; + $expire = null; + $expiration = null; + $user_id = null; + $scheme = null; + /** This action is documented in wp-inclues/pluggable.php */ + do_action( 'set_auth_cookie', $auth_cookie, $expire, $expiration, $user_id, $scheme ); + $logged_in_cookie = null; + /** This action is documented in wp-inclues/pluggable.php */ + do_action( 'set_logged_in_cookie', $logged_in_cookie, $expire, $expiration, $user_id, 'logged_in' ); +} + +function wp_clear_auth_cookie() { + /** This action is documented in wp-inclues/pluggable.php */ + do_action( 'clear_auth_cookie' ); +} diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index a6e05155c3..cd7eb41634 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -10,11 +10,16 @@ * @group restapi */ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { + protected static $superadmin; protected static $user; protected static $editor; protected static $site; public static function wpSetUpBeforeClass( $factory ) { + self::$superadmin = $factory->user->create( array( + 'role' => 'administrator', + 'user_login' => 'superadmin', + ) ); self::$user = $factory->user->create( array( 'role' => 'administrator', ) ); @@ -25,6 +30,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { if ( is_multisite() ) { self::$site = $factory->blog->create( array( 'domain' => 'rest.wordpress.org', 'path' => '/' ) ); + update_site_option( 'site_admins', array( 'superadmin' ) ); } } @@ -175,8 +181,8 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $response = $this->server->dispatch( $request ); $headers = $response->get_headers(); - $this->assertEquals( 50, $headers['X-WP-Total'] ); - $this->assertEquals( 5, $headers['X-WP-TotalPages'] ); + $this->assertEquals( 51, $headers['X-WP-Total'] ); + $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); $next_link = add_query_arg( array( 'page' => 2, ), rest_url( 'wp/v2/users' ) ); @@ -190,7 +196,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $request->set_param( 'page', 3 ); $response = $this->server->dispatch( $request ); $headers = $response->get_headers(); - $this->assertEquals( 51, $headers['X-WP-Total'] ); + $this->assertEquals( 52, $headers['X-WP-Total'] ); $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => 2, @@ -205,7 +211,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $request->set_param( 'page', 6 ); $response = $this->server->dispatch( $request ); $headers = $response->get_headers(); - $this->assertEquals( 51, $headers['X-WP-Total'] ); + $this->assertEquals( 52, $headers['X-WP-Total'] ); $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => 5, @@ -217,7 +223,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $request->set_param( 'page', 8 ); $response = $this->server->dispatch( $request ); $headers = $response->get_headers(); - $this->assertEquals( 51, $headers['X-WP-Total'] ); + $this->assertEquals( 52, $headers['X-WP-Total'] ); $this->assertEquals( 6, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => 6, @@ -393,7 +399,7 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'offset', 1 ); $response = $this->server->dispatch( $request ); - $this->assertCount( 3, $response->get_data() ); + $this->assertCount( 4, $response->get_data() ); // 'offset' works with 'per_page' $request->set_param( 'per_page', 2 ); $response = $this->server->dispatch( $request ); @@ -715,6 +721,88 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $this->check_add_edit_user_response( $response ); } + public function test_create_item_invalid_username() { + $this->allow_user_to_manage_multisite(); + wp_set_current_user( self::$user ); + + $params = array( + 'username' => '¯\_(ツ)_/¯', + 'password' => 'testpassword', + 'email' => 'test@example.com', + 'name' => 'Test User', + 'nickname' => 'testuser', + 'slug' => 'test-user', + 'roles' => array( 'editor' ), + 'description' => 'New API User', + 'url' => 'http://example.com', + ); + + // Username rules are different (more strict) for multisite; see `wpmu_validate_user_signup` + if ( is_multisite() ) { + $params['username'] = 'no-dashes-allowed'; + } + + $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); + $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( $params ); + + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + + $data = $response->get_data(); + if ( is_multisite() ) { + $this->assertInternalType( 'array', $data['additional_errors'] ); + $this->assertCount( 1, $data['additional_errors'] ); + $error = $data['additional_errors'][0]; + $this->assertEquals( 'user_name', $error['code'] ); + $this->assertEquals( 'Usernames can only contain lowercase letters (a-z) and numbers.', $error['message'] ); + } else { + $this->assertInternalType( 'array', $data['data']['params'] ); + $errors = $data['data']['params']; + $this->assertInternalType( 'string', $errors['username'] ); + $this->assertEquals( 'Username contains invalid characters.', $errors['username'] ); + } + } + + function get_illegal_user_logins() { + return array( 'nope' ); + } + + public function test_create_item_illegal_username() { + $this->allow_user_to_manage_multisite(); + wp_set_current_user( self::$user ); + + add_filter( 'illegal_user_logins', array( $this, 'get_illegal_user_logins' ) ); + + $params = array( + 'username' => 'nope', + 'password' => 'testpassword', + 'email' => 'test@example.com', + 'name' => 'Test User', + 'nickname' => 'testuser', + 'slug' => 'test-user', + 'roles' => array( 'editor' ), + 'description' => 'New API User', + 'url' => 'http://example.com', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); + $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->set_body_params( $params ); + + $response = $this->server->dispatch( $request ); + + remove_filter( 'illegal_user_logins', array( $this, 'get_illegal_user_logins' ) ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + + $data = $response->get_data(); + $this->assertInternalType( 'array', $data['data']['params'] ); + $errors = $data['data']['params']; + $this->assertInternalType( 'string', $errors['username'] ); + $this->assertEquals( 'Sorry, that username is not allowed.', $errors['username'] ); + } + public function test_create_new_network_user_on_site_does_not_add_user_to_sub_site() { if ( ! is_multisite() ) { $this->markTestSkipped( 'Test requires multisite.' ); @@ -810,7 +898,20 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { wpmu_delete_user( $user_id ); - $this->assertErrorResponse( 'user_name', $switched_response ); + $this->assertErrorResponse( 'rest_invalid_param', $switched_response, 400 ); + $data = $switched_response->get_data(); + $this->assertInternalType( 'array', $data['additional_errors'] ); + $this->assertCount( 2, $data['additional_errors'] ); + $errors = $data['additional_errors']; + foreach ( $errors as $error ) { + // Check the code matches one we know. + $this->assertContains( $error['code'], array( 'user_name', 'user_email' ) ); + if ( 'user_name' === $error['code'] ) { + $this->assertEquals( 'Sorry, that username already exists!', $error['message'] ); + } else { + $this->assertEquals( 'Sorry, that email address is already used!', $error['message'] ); + } + } } public function test_update_existing_network_user_on_sub_site_adds_user_to_site() { @@ -1305,6 +1406,213 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); } + public function test_update_item_invalid_password() { + $this->allow_user_to_manage_multisite(); + wp_set_current_user( self::$user ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', self::$editor ) ); + + $request->set_param( 'password', 'no\\backslashes\\allowed' ); + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + + $request->set_param( 'password', '' ); + $response = $this->server->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + public function verify_user_roundtrip( $input = array(), $expected_output = array() ) { + if ( isset( $input['id'] ) ) { + // Existing user; don't try to create one + $user_id = $input['id']; + } else { + // Create a new user + $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); + foreach ( $input as $name => $value ) { + $request->set_param( $name, $value ); + } + $request->set_param( 'email', 'cbg@androidsdungeon.com' ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 201, $response->get_status() ); + $actual_output = $response->get_data(); + + // Compare expected API output to actual API output + $this->assertEquals( $expected_output['username'] , $actual_output['username'] ); + $this->assertEquals( $expected_output['name'] , $actual_output['name'] ); + $this->assertEquals( $expected_output['first_name'] , $actual_output['first_name'] ); + $this->assertEquals( $expected_output['last_name'] , $actual_output['last_name'] ); + $this->assertEquals( $expected_output['url'] , $actual_output['url'] ); + $this->assertEquals( $expected_output['description'], $actual_output['description'] ); + $this->assertEquals( $expected_output['nickname'] , $actual_output['nickname'] ); + + // Compare expected API output to WP internal values + $user = get_userdata( $actual_output['id'] ); + $this->assertEquals( $expected_output['username'] , $user->user_login ); + $this->assertEquals( $expected_output['name'] , $user->display_name ); + $this->assertEquals( $expected_output['first_name'] , $user->first_name ); + $this->assertEquals( $expected_output['last_name'] , $user->last_name ); + $this->assertEquals( $expected_output['url'] , $user->user_url ); + $this->assertEquals( $expected_output['description'], $user->description ); + $this->assertEquals( $expected_output['nickname'] , $user->nickname ); + $this->assertTrue( wp_check_password( addslashes( $expected_output['password'] ), $user->user_pass ) ); + + $user_id = $actual_output['id']; + } + + // Update the user + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); + foreach ( $input as $name => $value ) { + if ( 'username' !== $name ) { + $request->set_param( $name, $value ); + } + } + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $actual_output = $response->get_data(); + + // Compare expected API output to actual API output + if ( isset( $expected_output['username'] ) ) { + $this->assertEquals( $expected_output['username'], $actual_output['username'] ); + } + $this->assertEquals( $expected_output['name'] , $actual_output['name'] ); + $this->assertEquals( $expected_output['first_name'] , $actual_output['first_name'] ); + $this->assertEquals( $expected_output['last_name'] , $actual_output['last_name'] ); + $this->assertEquals( $expected_output['url'] , $actual_output['url'] ); + $this->assertEquals( $expected_output['description'], $actual_output['description'] ); + $this->assertEquals( $expected_output['nickname'] , $actual_output['nickname'] ); + + // Compare expected API output to WP internal values + $user = get_userdata( $actual_output['id'] ); + if ( isset( $expected_output['username'] ) ) { + $this->assertEquals( $expected_output['username'], $user->user_login ); + } + $this->assertEquals( $expected_output['name'] , $user->display_name ); + $this->assertEquals( $expected_output['first_name'] , $user->first_name ); + $this->assertEquals( $expected_output['last_name'] , $user->last_name ); + $this->assertEquals( $expected_output['url'] , $user->user_url ); + $this->assertEquals( $expected_output['description'], $user->description ); + $this->assertEquals( $expected_output['nickname'] , $user->nickname ); + $this->assertTrue( wp_check_password( addslashes( $expected_output['password'] ), $user->user_pass ) ); + } + + public function test_user_roundtrip_as_editor() { + wp_set_current_user( self::$editor ); + $this->assertEquals( ! is_multisite(), current_user_can( 'unfiltered_html' ) ); + $this->verify_user_roundtrip( array( + 'id' => self::$editor, + 'name' => '\o/ ¯\_(ツ)_/¯', + 'first_name' => '\o/ ¯\_(ツ)_/¯', + 'last_name' => '\o/ ¯\_(ツ)_/¯', + 'url' => '\o/ ¯\_(ツ)_/¯', + 'description' => '\o/ ¯\_(ツ)_/¯', + 'nickname' => '\o/ ¯\_(ツ)_/¯', + 'password' => 'o/ ¯_(ツ)_/¯ \'"', + ), array( + 'name' => '\o/ ¯\_(ツ)_/¯', + 'first_name' => '\o/ ¯\_(ツ)_/¯', + 'last_name' => '\o/ ¯\_(ツ)_/¯', + 'url' => 'http://o/%20¯_(ツ)_/¯', + 'description' => '\o/ ¯\_(ツ)_/¯', + 'nickname' => '\o/ ¯\_(ツ)_/¯', + 'password' => 'o/ ¯_(ツ)_/¯ \'"', + ) ); + } + + public function test_user_roundtrip_as_editor_html() { + wp_set_current_user( self::$editor ); + if ( is_multisite() ) { + $this->assertFalse( current_user_can( 'unfiltered_html' ) ); + $this->verify_user_roundtrip( array( + 'id' => self::$editor, + 'name' => '
div
strong ', + 'first_name' => '
div
strong ', + 'last_name' => '
div
strong ', + 'url' => '
div
strong ', + 'description' => '
div
strong ', + 'nickname' => '
div
strong ', + 'password' => '
div
strong ', + ), array( + 'name' => 'div strong', + 'first_name' => 'div strong', + 'last_name' => 'div strong', + 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', + 'description' => 'div strong oh noes', + 'nickname' => 'div strong', + 'password' => '
div
strong ', + ) ); + } else { + $this->assertTrue( current_user_can( 'unfiltered_html' ) ); + $this->verify_user_roundtrip( array( + 'id' => self::$editor, + 'name' => '
div
strong ', + 'first_name' => '
div
strong ', + 'last_name' => '
div
strong ', + 'url' => '
div
strong ', + 'description' => '
div
strong ', + 'nickname' => '
div
strong ', + 'password' => '
div
strong ', + ), array( + 'name' => 'div strong', + 'first_name' => 'div strong', + 'last_name' => 'div strong', + 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', + 'description' => 'div strong oh noes', + 'nickname' => 'div strong', + 'password' => '
div
strong ', + ) ); + } + } + + public function test_user_roundtrip_as_superadmin() { + wp_set_current_user( self::$superadmin ); + $this->assertTrue( current_user_can( 'unfiltered_html' ) ); + $valid_username = is_multisite() ? 'noinvalidcharshere' : 'no-invalid-chars-here'; + $this->verify_user_roundtrip( array( + 'username' => $valid_username, + 'name' => '\\\&\\\ & &invalid; < < &lt;', + 'first_name' => '\\\&\\\ & &invalid; < < &lt;', + 'last_name' => '\\\&\\\ & &invalid; < < &lt;', + 'url' => '\\\&\\\ & &invalid; < < &lt;', + 'description' => '\\\&\\\ & &invalid; < < &lt;', + 'nickname' => '\\\&\\\ & &invalid; < < &lt;', + 'password' => '& & &invalid; < < &lt;', + ), array( + 'username' => $valid_username, + 'name' => '\\\&\\\ & &invalid; < < &lt;', + 'first_name' => '\\\&\\\ & &invalid; < < &lt;', + 'last_name' => '\\\&\\\ & &invalid; < < &lt;', + 'url' => 'http://&%20&%20&invalid;%20%20<%20&lt;', + 'description' => '\\\&\\\ & &invalid; < < &lt;', + 'nickname' => '\\\&\\\ & &invalid; < < &lt;', + 'password' => '& & &invalid; < < &lt;', + ) ); + } + + public function test_user_roundtrip_as_superadmin_html() { + wp_set_current_user( self::$superadmin ); + $this->assertTrue( current_user_can( 'unfiltered_html' ) ); + $valid_username = is_multisite() ? 'noinvalidcharshere' : 'no-invalid-chars-here'; + $this->verify_user_roundtrip( array( + 'username' => $valid_username, + 'name' => '
div
strong ', + 'first_name' => '
div
strong ', + 'last_name' => '
div
strong ', + 'url' => '
div
strong ', + 'description' => '
div
strong ', + 'nickname' => '
div
strong ', + 'password' => '
div
strong ', + ), array( + 'username' => $valid_username, + 'name' => 'div strong', + 'first_name' => 'div strong', + 'last_name' => 'div strong', + 'url' => 'http://divdiv/div%20strongstrong/strong%20scriptoh%20noes/script', + 'description' => 'div strong oh noes', + 'nickname' => 'div strong', + 'password' => '
div
strong ', + ) ); + } + public function test_delete_item() { $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) );