Skip to content

Commit a8889ee

Browse files
authored
Merge pull request #4 from spacedmonkey/feature/40365-rest-sites-endpoint-check-exists
Check if domain exists before creating / updating.
2 parents cee5f71 + 9ca95a1 commit a8889ee

2 files changed

Lines changed: 538 additions & 0 deletions

File tree

‎src/wp-includes/rest-api/endpoints/class-wp-rest-sites-controller.php‎

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -987,6 +987,11 @@ protected function prepare_item_for_database( $request ) {
987987
$prepared_site['domain'] = $request['domain'];
988988
}
989989

990+
$domain_check = $this->check_domain_is_available( $prepared_site, $request );
991+
if ( is_wp_error( $domain_check ) ) {
992+
return $domain_check;
993+
}
994+
990995
/**
991996
* Filters a site after it is prepared for the database.
992997
*
@@ -1000,6 +1005,209 @@ protected function prepare_item_for_database( $request ) {
10001005
return apply_filters( 'rest_preprocess_site', $prepared_site, $request );
10011006
}
10021007

1008+
/**
1009+
* Validates that the `domain` parameter is a valid hostname or IP address.
1010+
*
1011+
* @since 7.2.0
1012+
*
1013+
* @param mixed $value Value of the `domain` parameter.
1014+
* @param WP_REST_Request $request The current request.
1015+
* @param string $param The parameter name.
1016+
* @return true|WP_Error True if the value is valid, otherwise a WP_Error object.
1017+
*/
1018+
public function validate_domain( $value, $request, $param ) {
1019+
$valid = rest_validate_request_arg( $value, $request, $param );
1020+
if ( is_wp_error( $valid ) ) {
1021+
return $valid;
1022+
}
1023+
1024+
if ( $this->is_valid_domain( $value ) ) {
1025+
return true;
1026+
}
1027+
1028+
return new WP_Error(
1029+
'rest_invalid_param',
1030+
__( 'domain must be a valid hostname or IP address (IPv4 or IPv6), optionally followed by a port. Bracketed IPv6 addresses are not supported.' ),
1031+
array(
1032+
'status' => 400,
1033+
'param' => $param,
1034+
)
1035+
);
1036+
}
1037+
1038+
/**
1039+
* Checks whether a string is a valid domain: a hostname or bare IP address
1040+
* (IPv4 or IPv6, unbracketed), optionally followed by a port number.
1041+
* Brackets are not supported, so an IPv6 address can't carry a port.
1042+
*
1043+
* @since 7.2.0
1044+
*
1045+
* @param string $domain The domain to check.
1046+
* @return bool True if `$domain` is a valid domain, otherwise false.
1047+
*/
1048+
private function is_valid_domain( $domain ) {
1049+
if ( ! is_string( $domain ) || '' === $domain ) {
1050+
return false;
1051+
}
1052+
1053+
// Bare IPv4 or IPv6 address, no port.
1054+
if ( $this->is_ip_address_candidate( $domain ) && rest_is_ip_address( $domain ) ) {
1055+
return true;
1056+
}
1057+
1058+
// hostname[:port] or ipv4[:port]. A second colon means this isn't a
1059+
// single port suffix but an (invalid, since it failed the check above)
1060+
// unbracketed IPv6 address, so only split off a port when there's exactly one colon.
1061+
$host = $domain;
1062+
$port = null;
1063+
1064+
if ( 1 === substr_count( $domain, ':' ) ) {
1065+
$colon_pos = strpos( $domain, ':' );
1066+
$host = substr( $domain, 0, $colon_pos );
1067+
$port = substr( $domain, $colon_pos + 1 );
1068+
}
1069+
1070+
if ( null !== $port && ! $this->is_valid_port( $port ) ) {
1071+
return false;
1072+
}
1073+
1074+
if ( $this->is_ip_address_candidate( $host ) && rest_is_ip_address( $host ) ) {
1075+
return true;
1076+
}
1077+
1078+
return 1 === preg_match(
1079+
'/^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*$/',
1080+
$host
1081+
) && strlen( $host ) <= 253;
1082+
}
1083+
1084+
/**
1085+
* Checks whether a string is a valid TCP/UDP port number (1-65535).
1086+
*
1087+
* @since 7.2.0
1088+
*
1089+
* @param string $port The port to check.
1090+
* @return bool True if `$port` is a valid port number, otherwise false.
1091+
*/
1092+
private function is_valid_port( $port ) {
1093+
return 1 === preg_match( '/^[0-9]{1,5}$/', $port ) && (int) $port >= 1 && (int) $port <= 65535;
1094+
}
1095+
1096+
/**
1097+
* Checks whether a string is made up only of characters that can appear
1098+
* in an IPv4 or IPv6 (including IPv4-mapped) address, i.e. it's safe to
1099+
* pass to rest_is_ip_address(). That underlying check parses its input
1100+
* as hexadecimal, and PHP emits a warning for any other character, so
1101+
* this keeps arbitrary domain input from tripping that on the way in.
1102+
*
1103+
* @since 7.2.0
1104+
*
1105+
* @param string $value The value to check.
1106+
* @return bool True if `$value` only contains valid IP address characters, otherwise false.
1107+
*/
1108+
private function is_ip_address_candidate( $value ) {
1109+
return 1 === preg_match( '/^[0-9A-Fa-f:.]+$/', $value );
1110+
}
1111+
1112+
/**
1113+
* Validates that the `path` parameter is a valid site path.
1114+
*
1115+
* @since 7.2.0
1116+
*
1117+
* @param mixed $value Value of the `path` parameter.
1118+
* @param WP_REST_Request $request The current request.
1119+
* @param string $param The parameter name.
1120+
* @return true|WP_Error True if the value is valid, otherwise a WP_Error object.
1121+
*/
1122+
public function validate_path( $value, $request, $param ) {
1123+
$valid = rest_validate_request_arg( $value, $request, $param );
1124+
if ( is_wp_error( $valid ) ) {
1125+
return $valid;
1126+
}
1127+
1128+
if ( $this->is_valid_path( $value ) ) {
1129+
return true;
1130+
}
1131+
1132+
return new WP_Error(
1133+
'rest_invalid_param',
1134+
__( 'path must be a valid site path, starting and ending with a forward slash.' ),
1135+
array(
1136+
'status' => 400,
1137+
'param' => $param,
1138+
)
1139+
);
1140+
}
1141+
1142+
/**
1143+
* Checks whether a string is a valid site path: a forward slash, or a
1144+
* sequence of `/`-delimited segments each starting and ending with a
1145+
* forward slash, made up of characters allowed in a URL path segment
1146+
* (RFC 3986 `pchar`).
1147+
*
1148+
* @since 7.2.0
1149+
*
1150+
* @param string $path The path to check.
1151+
* @return bool True if `$path` is a valid site path, otherwise false.
1152+
*/
1153+
private function is_valid_path( $path ) {
1154+
if ( ! is_string( $path ) || '' === $path ) {
1155+
return false;
1156+
}
1157+
1158+
return 1 === preg_match(
1159+
'#^/(?:(?:[A-Za-z0-9._~!$&\'()*+,;=:@-]|%[0-9A-Fa-f]{2})+/)*$#',
1160+
$path
1161+
);
1162+
}
1163+
1164+
/**
1165+
* Checks that the domain and path for a prepared site are not already in use.
1166+
*
1167+
* When creating a site, the domain and path must not belong to any existing
1168+
* site. When updating a site, they may only belong to the site being updated.
1169+
*
1170+
* @since 7.2.0
1171+
*
1172+
* @param array $prepared_site The prepared site data for `wp_insert_site()`.
1173+
* @param WP_REST_Request $request The current request.
1174+
* @return true|WP_Error True if the domain and path are available, WP_Error otherwise.
1175+
*/
1176+
protected function check_domain_is_available( $prepared_site, $request ) {
1177+
$id = (int) $request['id'];
1178+
$domain = isset( $prepared_site['domain'] ) ? $prepared_site['domain'] : '';
1179+
$path = isset( $prepared_site['path'] ) ? $prepared_site['path'] : '/';
1180+
$network_id = isset( $prepared_site['network_id'] ) ? $prepared_site['network_id'] : get_current_network_id();
1181+
1182+
if ( ! empty( $id ) ) {
1183+
// Updating a site: fall back to the current values for anything the request left out.
1184+
$current_site = $this->get_site( $id );
1185+
1186+
if ( is_wp_error( $current_site ) ) {
1187+
return $current_site;
1188+
}
1189+
1190+
if ( ! isset( $prepared_site['domain'] ) ) {
1191+
$domain = $current_site->domain;
1192+
}
1193+
if ( ! isset( $prepared_site['path'] ) ) {
1194+
$path = $current_site->path;
1195+
}
1196+
if ( ! isset( $prepared_site['network_id'] ) ) {
1197+
$network_id = (int) $current_site->network_id;
1198+
}
1199+
}
1200+
1201+
$existing_site_id = domain_exists( $domain, $path, $network_id );
1202+
1203+
// The domain and path may only belong to the site being updated.
1204+
if ( $existing_site_id && (int) $existing_site_id !== $id ) {
1205+
return new WP_Error( 'rest_site_taken', __( 'Sorry, that site already exists!' ), array( 'status' => 400 ) );
1206+
}
1207+
1208+
return true;
1209+
}
1210+
10031211
/**
10041212
* Retrieves the site's schema, conforming to JSON Schema.
10051213
*
@@ -1027,18 +1235,25 @@ public function get_item_schema() {
10271235
'description' => __( 'The site\'s network ID. Default is the current network ID.' ),
10281236
'type' => 'integer',
10291237
'context' => array( 'view', 'edit', 'embed' ),
1238+
'default' => get_current_network_id(),
10301239
),
10311240
'domain' => array(
10321241
'description' => __( 'Site domain.' ),
10331242
'type' => 'string',
10341243
'context' => array( 'view', 'edit', 'embed' ),
10351244
'default' => '',
1245+
'arg_options' => array(
1246+
'validate_callback' => array( $this, 'validate_domain' ),
1247+
),
10361248
),
10371249
'path' => array(
10381250
'description' => __( 'Site path.' ),
10391251
'type' => 'string',
10401252
'context' => array( 'view', 'edit', 'embed' ),
10411253
'default' => '/',
1254+
'arg_options' => array(
1255+
'validate_callback' => array( $this, 'validate_path' ),
1256+
),
10421257
),
10431258
'registered' => array(
10441259
'description' => __( 'When the site was registered, in the site\'s timezone.' ),

0 commit comments

Comments
 (0)