Skip to content
Open
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -347,6 +356,37 @@ protected function bulk_actions( $which = '' ) {
<?php
}

protected function extra_tablenav( $which ) {
if ( 'top' !== $which ) {
return;
}

$current = $_REQUEST['purpose'] ?? '';
$options = wporg_login_purpose_options();
?>
<div class="alignleft actions">
<label class="screen-reader-text" for="filter-by-purpose">Filter by account purpose</label>
<select name="purpose" id="filter-by-purpose">
<option value=""><?php echo esc_html( 'All purposes' ); ?></option>
<?php
foreach ( $options as $key => $label ) {
if ( '' === $key ) {
continue;
}
printf(
'<option value="%s" %s>%s</option>',
esc_attr( $key ),
selected( $current, $key, false ),
esc_html( $label )
);
}
?>
</select>
<input type="submit" name="filter_action" class="button" value="Filter" />
</div>
<?php
}

function single_row( $item ) {
$classes = $this->get_row_class( $item );
printf( '<tr class="%s">', esc_attr( implode( ' ', $classes ) ) );
Expand Down Expand Up @@ -503,7 +543,7 @@ function column_meta( $item ) {

echo '<hr>';

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<br>", esc_html( $field ), $this->link_to_search( $meta->$field ) );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,75 @@
<?php

/**
* Returns the available "Account purpose" dropdown options.
*
* Keyed by the internal value stored in user meta; values are the translated labels.
*/
function wporg_login_purpose_options() {
return [
'' => __( '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 the wp-admin / wp-login.php paths 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 the wp-admin / wp-login.php paths so we keep just the site root the user lives on.
$path = $parts['path'] ?? '';
$path = preg_replace(
'#/(wp-admin|wp-login\.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
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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 ];

Expand All @@ -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;
}

Expand Down Expand Up @@ -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)';
Comment thread
dd32 marked this conversation as resolved.
}
Comment thread
dd32 marked this conversation as resolved.

foreach ( $fields as $field ) {
if ( isset( $_POST['user_fields'][ $field ] ) ) {
Expand All @@ -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 / wp-login.php 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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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 )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,32 @@
'from' => $user->from ?: '',
'occ' => $user->occ ?: '',
'interests' => $user->interests ?: '',
'purpose' => $user->purpose ?: '',
];
}

$purpose_options = wporg_login_purpose_options();

?>
<p class="login-website">
<label for="user_website"><?php _e( 'Website', 'wporg' ); ?></label>
<input type="text" name="user_fields[url]" id="user_url" class="input" value="<?php echo esc_attr( $fields['url'] ?? '' ); ?>" size="20" placeholder="https://" data-pattern-after-blur="(https?:\/\/)?([a-zA-Z0-9\-]+\.\S+)?" />
<label for="user_url"><?php esc_html_e( 'Your WordPress site', 'wporg' ); ?></label>
<input type="url" name="user_fields[url]" id="user_url" class="input" value="<?php echo esc_attr( $fields['url'] ?? '' ); ?>" size="20" placeholder="https://example.com" data-pattern-after-blur="(https?:\/\/)?([a-zA-Z0-9\-]+\.\S+)?" />
<span class="small"><?php esc_html_e( 'The address of your own WordPress site, if you have one. Leave blank if you don’t.', 'wporg' ); ?></span>
<span class="invalid-message"><?php _e( 'That URL appears to be invalid.', 'wporg' ); ?></span>
</p>

<p class="login-purpose">
<label for="user_purpose"><?php esc_html_e( 'Account purpose', 'wporg' ); ?></label>
<select name="user_fields[purpose]" id="user_purpose" class="input">
<?php foreach ( $purpose_options as $key => $label ) : ?>
<option value="<?php echo esc_attr( $key ); ?>" <?php selected( $fields['purpose'] ?? '', $key ); ?>><?php echo esc_html( $label ); ?></option>
<?php endforeach; ?>
</select>
<span class="small"><?php esc_html_e( 'Tell us what you’ll use this account for. This helps us tailor your experience.', 'wporg' ); ?></span>
</p>

<p class="login-location">
<label for="user_location"><?php _e( 'Location', 'wporg' ); ?></label>
<label for="user_location"><?php esc_html_e( 'Public Location', 'wporg' ); ?></label>
<input type="text" name="user_fields[from]" id="user_location" class="input" value="<?php echo esc_attr( $fields['from'] ?? '' ); ?>" size="20" />
</p>

Expand All @@ -40,3 +54,8 @@
<input type="text" name="user_fields[interests]" id="user_interests" class="input" value="<?php echo esc_attr( $fields['interests'] ?? '' ); ?>" size="20" />
</p>

<p class="login-biography" aria-hidden="true" style="position:absolute;left:-10000px;top:auto;width:1px;height:1px;overflow:hidden;">
<label for="user_biography"><?php esc_html_e( 'Biography', 'wporg' ); ?></label>
<input type="text" name="user_fields[biography]" id="user_biography" value="" size="20" autocomplete="off" tabindex="-1" />
</p>

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading