Skip to content

Commit 0897471

Browse files
committed
Merge remote-tracking branch 'origin/features/profile-slug' into develop
2 parents 6aaf32c + f52b582 commit 0897471

15 files changed

Lines changed: 411 additions & 26 deletions

config/settings/open-ai-admin.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
$settings = LP_Settings::instance();
1010
$user = wp_get_current_user();
11-
$username = $user->user_login;
11+
$user_model = new \LearnPress\Models\UserModel( $user );
12+
$username = $user_model->get_pretty_slug();
1213
$settings_slug = $settings->get( 'profile_endpoints.settings', 'settings' );
1314
$profile_slug = 'profile';
1415

config/settings/permalink.php

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88

99
$settings = LP_Settings::instance();
1010
$user = wp_get_current_user();
11-
$username = $user->user_login;
11+
$user_model = new \LearnPress\Models\UserModel( $user );
12+
$username = $user_model->get_pretty_slug();
1213
$settings_slug = $settings->get( 'profile_endpoints.settings', 'settings' );
1314
$profile_slug = 'profile';
1415

@@ -131,6 +132,16 @@
131132
'placeholder' => 'order-details',
132133
'desc' => sprintf( 'e.g. %s', "{$profile_url}/<code>" . $settings->get( 'profile_endpoints.order-details', 'order-details' ) . '</code>/123' ),
133134
),
135+
array(
136+
'title' => esc_html__( 'Generate user slug', 'learnpress' ),
137+
'id' => 'lp_generate_user_slug_row',
138+
'type' => 'html',
139+
'default' => sprintf(
140+
'<p>%s</p><p><button class="button" type="submit" name="lp_generate_user_slug" value="yes">%s</button></p>',
141+
esc_html__( 'Generate public user slugs for existing users on old sites. Existing pretty slugs will be kept unchanged.', 'learnpress' ),
142+
esc_html__( 'Generate user slug', 'learnpress' )
143+
),
144+
),
134145
),
135146
$this
136147
),

config/settings/profile.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
$settings = LP_Settings::instance();
99
$user = wp_get_current_user();
10-
$username = $user->user_login;
10+
$user_model = new \LearnPress\Models\UserModel( $user );
11+
$username = $user_model->get_pretty_slug();
1112
$settings_slug = $settings->get( 'profile_endpoints.settings', 'settings' );
1213
$profile_slug = 'profile';
1314

inc/Ajax/AbstractAjax.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public static function catch_lp_ajax() {
3030
LoadContentViaAjax::class,
3131
];
3232

33+
// Todo: should write separation check none login and none not login
3334
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
3435
if ( ! in_array( get_class( $class ), $class_no_nonce ) ) {
3536
wp_die( 'Invalid request!', 400 );

inc/Models/UserModel.php

Lines changed: 264 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
use stdClass;
3333
use Throwable;
3434
use WP_Error;
35-
35+
use WP_User;
3636
class UserModel {
3737
/**
3838
* Auto increment, Primary key
@@ -78,7 +78,7 @@ class UserModel {
7878
// Meta keys
7979
const META_KEY_IMAGE = '_lp_profile_picture';
8080
const META_KEY_COVER_IMAGE = '_lp_profile_cover_image';
81-
81+
const META_KEY_PUBLIC_SLUG = '_lp_public_user_slug';
8282
// Roles
8383
const ROLE_INSTRUCTOR = LP_TEACHER_ROLE;
8484
const ROLE_ADMINISTRATOR = 'administrator';
@@ -108,12 +108,16 @@ public function __construct( $data = null ) {
108108
* @return UserModel
109109
*/
110110
public function map_to_object( $data ): UserModel {
111+
112+
if ( $data instanceof WP_User ) {
113+
$data = $data->data;
114+
}
115+
111116
foreach ( $data as $key => $value ) {
112117
if ( property_exists( $this, $key ) ) {
113118
$this->{$key} = $value;
114119
}
115120
}
116-
117121
return $this;
118122
}
119123

@@ -223,10 +227,265 @@ public function get_meta_value_by_key( string $key, $default_value = false ) {
223227
* @version 1.0.0
224228
*/
225229
public function set_meta_value_by_key( string $key, $value ) {
230+
226231
$this->meta_data->{$key} = $value;
227232
update_user_meta( $this->ID, $key, $value );
228233
}
229234

235+
/**
236+
* Retrieve the pretty slug used instead of user_name.
237+
*
238+
* This value is used to build links such as instructor links and user profile links.
239+
* If a pretty slug has not been generated yet, it falls back to user_name when
240+
* $fallback_to_username is true.
241+
*
242+
* @param bool $fallback_to_username Whether to fallback to username if no pretty slug exists.
243+
*
244+
* @return string
245+
*/
246+
public function get_pretty_slug( bool $fallback_to_username = true ): string {
247+
248+
$slug = sanitize_title( (string) $this->get_meta_value_by_key( self::META_KEY_PUBLIC_SLUG, '' ) );
249+
250+
if ( '' !== $slug || ! $fallback_to_username ) {
251+
return $slug;
252+
}
253+
254+
$username = trim( (string) $this->get_username() );
255+
if ( '' !== $username ) {
256+
return $username;
257+
}
258+
259+
$wp_user = get_userdata( $this->get_id() );
260+
261+
return $wp_user instanceof WP_User ? (string) $wp_user->user_login : '';
262+
}
263+
264+
/**
265+
* Build base source for pretty slug generation.
266+
*
267+
* @return string
268+
*/
269+
public function get_pretty_slug_source(): string {
270+
271+
$user_id = $this->get_id();
272+
$user = get_userdata( $user_id );
273+
274+
if ( $user_id <= 0 || ! $user instanceof WP_User ) {
275+
return '';
276+
}
277+
278+
$first_name = trim( (string) get_user_meta( $user_id, 'first_name', true ) );
279+
$last_name = trim( (string) get_user_meta( $user_id, 'last_name', true ) );
280+
$full_name = trim( "{$first_name} {$last_name}" );
281+
282+
if ( '' !== $full_name ) {
283+
return $full_name;
284+
}
285+
286+
return trim( (string) $user->user_login );
287+
}
288+
289+
/**
290+
* Check if pretty slug exists for another user.
291+
*
292+
* @param string $slug
293+
* @param int $exclude_user_id
294+
*
295+
* @return int
296+
*/
297+
public function pretty_slug_exists( string $slug, int $exclude_user_id = 0 ): int {
298+
299+
$slug = sanitize_title( $slug );
300+
if ( '' === $slug ) {
301+
return 0;
302+
}
303+
304+
$user_id = 0;
305+
306+
try {
307+
$lp_user_db = LP_User_DB::instance();
308+
$filter = new LP_User_Filter();
309+
$filter->only_fields = [ 'u.ID' ];
310+
$filter->run_query_count = false;
311+
$filter->limit = 1;
312+
$filter->join[] = "INNER JOIN {$lp_user_db->wpdb->usermeta} AS um ON um.user_id = u.ID";
313+
$filter->where[] = $lp_user_db->wpdb->prepare( 'AND um.meta_key = %s', self::META_KEY_PUBLIC_SLUG );
314+
$filter->where[] = $lp_user_db->wpdb->prepare( 'AND um.meta_value = %s', $slug );
315+
316+
if ( $exclude_user_id > 0 ) {
317+
$filter->where[] = $lp_user_db->wpdb->prepare( 'AND u.ID != %d', $exclude_user_id );
318+
}
319+
320+
$users = $lp_user_db->get_users( $filter );
321+
if ( ! empty( $users ) && isset( $users[0]->ID ) ) {
322+
$user_id = (int) $users[0]->ID;
323+
}
324+
} catch ( Throwable $e ) {
325+
error_log( __METHOD__ . ': ' . $e->getMessage() );
326+
}
327+
328+
return $user_id;
329+
}
330+
331+
/**
332+
* Create a unique pretty slug for user.
333+
*
334+
* @return string|WP_Error
335+
*/
336+
public function generate_pretty_slug() {
337+
338+
$user_id = $this->get_id();
339+
$user = get_userdata( $user_id );
340+
341+
if ( $user_id <= 0 || ! $user instanceof WP_User ) {
342+
return new WP_Error( 'lp_user_slug_invalid_user', esc_html__( 'The user is invalid.', 'learnpress' ) );
343+
}
344+
345+
$existing_slug = $this->get_pretty_slug( false );
346+
if ( '' !== $existing_slug ) {
347+
return $existing_slug;
348+
}
349+
350+
$base_source = $this->get_pretty_slug_source();
351+
$base_slug = sanitize_title( $base_source );
352+
353+
if ( '' === $base_slug ) {
354+
$base_slug = sanitize_title( $user->user_login );
355+
}
356+
357+
if ( '' === $base_slug ) {
358+
return new WP_Error( 'lp_user_slug_empty_source', esc_html__( 'Unable to generate a public user slug.', 'learnpress' ) );
359+
}
360+
361+
for ( $attempt = 0; $attempt < 10; $attempt++ ) {
362+
$random_suffix = strtolower( wp_generate_password( 4, false, false ) );
363+
$candidate = sanitize_title( "{$base_slug}-{$random_suffix}" );
364+
365+
if ( '' === $candidate ) {
366+
continue;
367+
}
368+
369+
if ( ! $this->pretty_slug_exists( $candidate, $user_id ) ) {
370+
$this->set_meta_value_by_key( self::META_KEY_PUBLIC_SLUG, $candidate );
371+
372+
return $candidate;
373+
}
374+
}
375+
376+
return new WP_Error( 'lp_user_slug_not_unique', esc_html__( 'Unable to generate a unique public user slug.', 'learnpress' ) );
377+
}
378+
379+
/**
380+
* Validate and update pretty slug manually.
381+
*
382+
* @param string $slug
383+
*
384+
* @return string|WP_Error
385+
*/
386+
public function update_pretty_slug( string $slug ) {
387+
388+
$user_id = $this->get_id();
389+
$user = get_userdata( $user_id );
390+
391+
if ( $user_id <= 0 || ! $user instanceof WP_User ) {
392+
return new WP_Error( 'lp_user_slug_invalid_user', esc_html__( 'The user is invalid.', 'learnpress' ) );
393+
}
394+
395+
$slug = sanitize_title( wp_unslash( $slug ) );
396+
397+
if ( '' === $slug ) {
398+
delete_user_meta( $user_id, self::META_KEY_PUBLIC_SLUG );
399+
$this->meta_data->{self::META_KEY_PUBLIC_SLUG} = '';
400+
401+
return '';
402+
}
403+
404+
if ( $this->pretty_slug_exists( $slug, $user_id ) ) {
405+
return new WP_Error( 'lp_user_slug_exists', esc_html__( 'This user slug already exists.', 'learnpress' ) );
406+
}
407+
408+
$this->set_meta_value_by_key( self::META_KEY_PUBLIC_SLUG, $slug );
409+
410+
return $slug;
411+
}
412+
413+
/**
414+
* Resolve user by pretty slug with legacy username fallback.
415+
*
416+
* @param string $identifier
417+
*
418+
* @return WP_User|false
419+
*/
420+
public function resolve_user_by_public_identifier( string $identifier ) {
421+
422+
$identifier_raw = trim( urldecode( $identifier ) );
423+
$identifier_slug = sanitize_title( $identifier_raw );
424+
425+
if ( '' === $identifier_raw ) {
426+
return false;
427+
}
428+
429+
$user_id = $this->pretty_slug_exists( $identifier_slug );
430+
if ( $user_id > 0 ) {
431+
return get_user_by( 'ID', $user_id );
432+
}
433+
434+
$user = get_user_by( 'login', $identifier_raw );
435+
if ( $user instanceof WP_User ) {
436+
return $user;
437+
}
438+
439+
return get_user_by( 'slug', $identifier_slug );
440+
}
441+
442+
/**
443+
* Generate pretty slug for users that still miss one (old sites support).
444+
*
445+
* @return array{processed:int,generated:int,skipped:int,failed:int}
446+
*/
447+
public function generate_missing_pretty_slugs(): array {
448+
449+
$user_ids = get_users(
450+
[
451+
'fields' => 'ids',
452+
'number' => -1,
453+
]
454+
);
455+
456+
$result = [
457+
'processed' => 0,
458+
'generated' => 0,
459+
'skipped' => 0,
460+
'failed' => 0,
461+
];
462+
463+
foreach ( $user_ids as $user_id ) {
464+
$user_id = (int) $user_id;
465+
++$result['processed'];
466+
467+
$wp_user = get_userdata( $user_id );
468+
if ( ! $wp_user instanceof WP_User ) {
469+
++$result['failed'];
470+
continue;
471+
}
472+
473+
$user_model = new UserModel( $wp_user );
474+
if ( '' !== $user_model->get_pretty_slug( false ) ) {
475+
++$result['skipped'];
476+
continue;
477+
}
478+
479+
$generated = $user_model->generate_pretty_slug();
480+
if ( is_wp_error( $generated ) ) {
481+
++$result['failed'];
482+
} else {
483+
++$result['generated'];
484+
}
485+
}
486+
487+
return $result;
488+
}
230489
/**
231490
* Get upload profile src.
232491
*
@@ -348,15 +607,15 @@ public function get_display_name(): string {
348607
* @since 4.2.3
349608
*/
350609
public function get_url_instructor(): string {
610+
351611
$single_instructor_link = '';
352612

353613
try {
354-
$user_name = $this->user_nicename ?? '';
614+
$user_name = $this->get_pretty_slug();
355615
$single_instructor_page_id = learn_press_get_page_id( 'single_instructor' );
356616
if ( ! $single_instructor_page_id ) {
357617
return $single_instructor_link;
358618
}
359-
360619
$single_instructor_link = trailingslashit(
361620
trailingslashit( get_page_link( $single_instructor_page_id ) ) . $user_name
362621
);

0 commit comments

Comments
 (0)