diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/admin/class-user-registrations-list-table.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/admin/class-user-registrations-list-table.php
index ce09ac3fc0..aa4495cd2c 100644
--- a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/admin/class-user-registrations-list-table.php
+++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/admin/class-user-registrations-list-table.php
@@ -229,6 +229,15 @@ protected function get_join_where_sql( $view = null ) {
}
}
+ // Optional purpose filter from the toolbar dropdown.
+ $purpose = $_REQUEST['purpose'] ?? '';
+ if ( $purpose && isset( wporg_login_purpose_options()[ $purpose ] ) && '' !== $purpose ) {
+ $where .= $wpdb->prepare(
+ ' AND registrations.meta LIKE %s',
+ '%' . $wpdb->esc_like( '"purpose":"' . $purpose . '"' ) . '%'
+ );
+ }
+
// Join if the view needs the users or description table.
if ( strpos( $where . $join, 'users.' ) || strpos( $where, 'description.' ) || ( 'banned-users' === $view ?: ( $_REQUEST['view'] ?? 'all' ) ) ) {
$join .= " LEFT JOIN {$wpdb->users} users ON registrations.created = 1 AND registrations.user_login = users.user_login";
@@ -347,6 +356,37 @@ protected function bulk_actions( $which = '' ) {
+
+
+
+
+
+ get_row_class( $item );
printf( '', esc_attr( implode( ' ', $classes ) ) );
@@ -503,7 +543,7 @@ function column_meta( $item ) {
echo '
';
- foreach ( [ 'url', 'from', 'occ', 'interests', 'source', 'bypass' ] as $field ) {
+ foreach ( [ 'url', 'from', 'occ', 'interests', 'purpose', 'source', 'bypass' ] as $field ) {
if ( !empty( $meta->$field ) ) {
printf( "%s: %s
", esc_html( $field ), $this->link_to_search( $meta->$field ) );
}
diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/functions-registration.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/functions-registration.php
index c097c1b040..6f98e06bba 100644
--- a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/functions-registration.php
+++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/functions-registration.php
@@ -1,5 +1,75 @@
__( 'Please select…', 'wporg' ),
+ 'contributing' => __( 'Contributing to WordPress', 'wporg' ),
+ 'learn' => __( 'Taking Learn.WordPress.org courses', 'wporg' ),
+ 'support' => __( 'Getting help in the support forums', 'wporg' ),
+ 'plugin_theme_author' => __( 'Publishing a plugin or theme as an individual', 'wporg' ),
+ 'event' => __( 'Attending a WordPress event', 'wporg' ),
+ 'create_site' => __( 'Create a WordPress site', 'wporg' ),
+ 'personal' => __( 'Personal use', 'wporg' ),
+ 'business' => __( 'Business / Company account', 'wporg' ),
+ 'other' => __( 'Other', 'wporg' ),
+ ];
+}
+
+/**
+ * Sanitize a user-supplied website URL for the profile field.
+ *
+ * Strips WordPress internal paths (wp-admin, wp-login.php, etc.) so the saved value points at
+ * the public site root, and rejects URLs hosted on wordpress.org subdomains since those refer
+ * to wp.org-managed properties rather than the user's own site.
+ *
+ * @param string $url Raw URL submitted by the user.
+ * @return string Cleaned URL, or an empty string if the URL is unusable.
+ */
+function wporg_login_sanitize_user_url( $url ) {
+ $url = trim( (string) $url );
+ if ( ! $url ) {
+ return '';
+ }
+
+ // Add a scheme if missing, otherwise wp_parse_url treats the input as a path.
+ if ( ! preg_match( '#^[a-z][a-z0-9+.\-]*://#i', $url ) ) {
+ $url = 'http://' . ltrim( $url, '/' );
+ }
+
+ $parts = wp_parse_url( $url );
+ if ( ! $parts || empty( $parts['host'] ) ) {
+ return '';
+ }
+
+ $host = strtolower( $parts['host'] );
+
+ // Reject wordpress.org and any subdomain of it — those aren't the user's own site.
+ if ( 'wordpress.org' === $host || str_ends_with( $host, '.wordpress.org' ) ) {
+ return '';
+ }
+
+ // Strip WordPress internal paths so we keep just the site root the user lives on.
+ $path = $parts['path'] ?? '';
+ $path = preg_replace(
+ '#/(wp-admin|wp-login\.php|wp-content|wp-includes|wp-json|wp-cron\.php|xmlrpc\.php|wp-signup\.php|wp-activate\.php)(/.*|$)#i',
+ '/',
+ $path
+ );
+
+ $rebuilt = $parts['scheme'] . '://' . $host;
+ if ( ! empty( $parts['port'] ) ) {
+ $rebuilt .= ':' . $parts['port'];
+ }
+ $rebuilt .= $path ?: '/';
+
+ return $rebuilt;
+}
+
function wporg_login_check_recapcha_status( $check_v3_action = false, $block_low_scores = true ) {
// Allow local installs to bypass
@@ -136,10 +206,10 @@ function wporg_login_create_pending_user( $user_login, $user_email, $meta = arra
);
// If the signup has a bypass-spam-checks token, approve it.
+ // The bypass token overrides every spam check — heuristics, reCaptcha, block-words, honeypot, etc.
if (
! $pending_user['cleared'] &&
- wporg_reg_has_signup_token( $pending_user ) &&
- 'block' !== ( $pending_user['meta']['heuristics'] ?? '' )
+ wporg_reg_has_signup_token( $pending_user )
) {
$pending_user['cleared'] = 1;
$pending_user['meta']['bypass'] = 'yes';
@@ -402,7 +472,7 @@ function wporg_login_create_user_from_pending( $pending_user, $password = false
$tos_meta_key = WPOrg_SSO::TOS_USER_META_KEY;
- foreach ( array( 'url', 'from', 'occ', 'interests', $tos_meta_key ) as $field ) {
+ foreach ( array( 'url', 'from', 'occ', 'interests', 'purpose', $tos_meta_key ) as $field ) {
if ( !empty( $pending_user['meta'][ $field ] ) ) {
$value = $pending_user['meta'][ $field ];
@@ -415,8 +485,9 @@ function wporg_login_create_user_from_pending( $pending_user, $password = false
];
if ( 'url' == $field ) {
- // If the URL contains WordPress.org, just skip it.
- if ( str_contains( strtolower( $value ), 'wordpress.org' ) ) {
+ // Re-run the sanitizer in case a legacy pending record predates the form-time cleanup.
+ $value = wporg_login_sanitize_user_url( $value );
+ if ( ! $value ) {
continue;
}
@@ -455,7 +526,16 @@ function wporg_login_save_profile_fields( $pending_user = false, $state = '' ) {
if ( ! $_POST || empty( $_POST['user_fields'] ) ) {
return false;
}
- $fields = array( 'url', 'from', 'occ', 'interests' );
+ $fields = array( 'url', 'from', 'occ', 'interests', 'purpose' );
+
+ $purpose_options = wporg_login_purpose_options();
+
+ // Honeypot: this field is hidden from real users via CSS — only bots fill it in.
+ $honeypot = trim( sanitize_text_field( wp_unslash( $_POST['user_fields']['biography'] ?? '' ) ) );
+ if ( $honeypot && $pending_user ) {
+ $pending_user['cleared'] = 0;
+ $pending_user['meta']['block_reason'] ??= 'Honeypot tripped (biography)';
+ }
foreach ( $fields as $field ) {
if ( isset( $_POST['user_fields'][ $field ] ) ) {
@@ -464,6 +544,9 @@ function wporg_login_save_profile_fields( $pending_user = false, $state = '' ) {
/** This filter is documented in wp-includes/user.php */
$value = apply_filters( 'pre_user_url', $value );
+ // Strip wp-admin/etc paths and reject .wordpress.org URLs.
+ $value = wporg_login_sanitize_user_url( $value );
+
if ( $pending_user ) {
$pending_user['meta'][ $field ] = esc_url_raw( $value );
} else {
@@ -472,6 +555,25 @@ function wporg_login_save_profile_fields( $pending_user = false, $state = '' ) {
'user_url' => esc_url_raw( $value ),
) );
}
+ } elseif ( 'purpose' == $field ) {
+ // Only accept known keys; silently drop anything else.
+ if ( ! isset( $purpose_options[ $value ] ) ) {
+ $value = '';
+ }
+
+ if ( $pending_user ) {
+ $pending_user['meta'][ $field ] = $value;
+
+ // Business / company accounts default to the spectator role on the support forums.
+ // wporg_login_create_user_from_pending() picks this up at account creation.
+ if ( 'business' === $value ) {
+ $pending_user['meta']['role'] ??= 'spectator';
+ }
+ } elseif ( $value ) {
+ update_user_meta( get_current_user_id(), $field, $value );
+ } else {
+ delete_user_meta( get_current_user_id(), $field );
+ }
} else {
if ( $pending_user ) {
$pending_user['meta'][ $field ] = $value;
@@ -532,10 +634,10 @@ function wporg_login_save_profile_fields( $pending_user = false, $state = '' ) {
}
// If the signup has a bypass-spam-checks token, approve it.
+ // The bypass token overrides every spam check — heuristics, reCaptcha, block-words, honeypot, etc.
if (
! $pending_user['cleared'] &&
- wporg_reg_has_signup_token( $pending_user ) &&
- 'block' !== ( $pending_user['meta']['heuristics'] ?? '' )
+ wporg_reg_has_signup_token( $pending_user )
) {
$pending_user['cleared'] = 1;
$pending_user['meta']['bypass'] = 'yes';
@@ -567,7 +669,7 @@ function wporg_login_has_blocked_word( $user ) {
return $word;
}
- foreach ( [ 'url', 'from', 'occ', 'interests' ] as $field ) {
+ foreach ( [ 'url', 'from', 'occ', 'interests', 'purpose' ] as $field ) {
if (
! empty( $user['meta'][ $field ] ) &&
false !== stripos( $user['meta'][ $field ], $word )
diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/partials/register-profilefields.php b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/partials/register-profilefields.php
index 05982ddf6d..b51c4e8601 100644
--- a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/partials/register-profilefields.php
+++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/partials/register-profilefields.php
@@ -15,18 +15,32 @@
'from' => $user->from ?: '',
'occ' => $user->occ ?: '',
'interests' => $user->interests ?: '',
+ 'purpose' => $user->purpose ?: '',
];
}
+$purpose_options = wporg_login_purpose_options();
+
?>
-
-
+
+
+
+
+
+
+
+
+
-
+
@@ -40,3 +54,8 @@
+
+
+
+
+
diff --git a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/stylesheets/login.css b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/stylesheets/login.css
index 91c836c1b9..aac021e99c 100644
--- a/wordpress.org/public_html/wp-content/themes/pub/wporg-login/stylesheets/login.css
+++ b/wordpress.org/public_html/wp-content/themes/pub/wporg-login/stylesheets/login.css
@@ -322,7 +322,9 @@ form .submit {
}
form input[type="text"],
-form input[type="password"] {
+form input[type="url"],
+form input[type="password"],
+form select.input {
width: 100%;
padding: 3px 10px;
margin: 2px 6px 16px 0;