|
32 | 32 | use stdClass; |
33 | 33 | use Throwable; |
34 | 34 | use WP_Error; |
35 | | - |
| 35 | +use WP_User; |
36 | 36 | class UserModel { |
37 | 37 | /** |
38 | 38 | * Auto increment, Primary key |
@@ -78,7 +78,7 @@ class UserModel { |
78 | 78 | // Meta keys |
79 | 79 | const META_KEY_IMAGE = '_lp_profile_picture'; |
80 | 80 | const META_KEY_COVER_IMAGE = '_lp_profile_cover_image'; |
81 | | - |
| 81 | + const META_KEY_PUBLIC_SLUG = '_lp_public_user_slug'; |
82 | 82 | // Roles |
83 | 83 | const ROLE_INSTRUCTOR = LP_TEACHER_ROLE; |
84 | 84 | const ROLE_ADMINISTRATOR = 'administrator'; |
@@ -108,12 +108,16 @@ public function __construct( $data = null ) { |
108 | 108 | * @return UserModel |
109 | 109 | */ |
110 | 110 | public function map_to_object( $data ): UserModel { |
| 111 | + |
| 112 | + if ( $data instanceof WP_User ) { |
| 113 | + $data = $data->data; |
| 114 | + } |
| 115 | + |
111 | 116 | foreach ( $data as $key => $value ) { |
112 | 117 | if ( property_exists( $this, $key ) ) { |
113 | 118 | $this->{$key} = $value; |
114 | 119 | } |
115 | 120 | } |
116 | | - |
117 | 121 | return $this; |
118 | 122 | } |
119 | 123 |
|
@@ -223,10 +227,265 @@ public function get_meta_value_by_key( string $key, $default_value = false ) { |
223 | 227 | * @version 1.0.0 |
224 | 228 | */ |
225 | 229 | public function set_meta_value_by_key( string $key, $value ) { |
| 230 | + |
226 | 231 | $this->meta_data->{$key} = $value; |
227 | 232 | update_user_meta( $this->ID, $key, $value ); |
228 | 233 | } |
229 | 234 |
|
| 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 | + } |
230 | 489 | /** |
231 | 490 | * Get upload profile src. |
232 | 491 | * |
@@ -348,15 +607,15 @@ public function get_display_name(): string { |
348 | 607 | * @since 4.2.3 |
349 | 608 | */ |
350 | 609 | public function get_url_instructor(): string { |
| 610 | + |
351 | 611 | $single_instructor_link = ''; |
352 | 612 |
|
353 | 613 | try { |
354 | | - $user_name = $this->user_nicename ?? ''; |
| 614 | + $user_name = $this->get_pretty_slug(); |
355 | 615 | $single_instructor_page_id = learn_press_get_page_id( 'single_instructor' ); |
356 | 616 | if ( ! $single_instructor_page_id ) { |
357 | 617 | return $single_instructor_link; |
358 | 618 | } |
359 | | - |
360 | 619 | $single_instructor_link = trailingslashit( |
361 | 620 | trailingslashit( get_page_link( $single_instructor_page_id ) ) . $user_name |
362 | 621 | ); |
|
0 commit comments