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;