[ Index ]

PHP Cross Reference of WordPress Trunk (Updated Daily)

Search

title

Body

[close]

/wp-admin/js/ -> user-profile.js (source)

   1  /* global ajaxurl, pwsL10n, userProfileL10n, ClipboardJS */
   2  
   3  /**
   4   * @output wp-admin/js/user-profile.js
   5   */
   6  
   7  /**
   8   * Handles the user profile functionality.
   9   *
  10   * @param {JQueryStatic} $ The jQuery object.
  11   */
  12  (function($) {
  13      var updateLock = false,
  14          isSubmitting = false,
  15          __ = wp.i18n.__,
  16          clipboard = new ClipboardJS( '.application-password-display .copy-button' ),
  17          $pass1Row,
  18          $pass1,
  19          $pass2,
  20          $weakRow,
  21          $weakCheckbox,
  22          $toggleButton,
  23          $submitButtons,
  24          $submitButton,
  25          currentPass,
  26          $form,
  27          originalFormContent,
  28          $passwordWrapper,
  29          successTimeout,
  30          isMac = window.navigator.platform ? window.navigator.platform.indexOf( 'Mac' ) !== -1 : false,
  31          ua = navigator.userAgent.toLowerCase(),
  32          isSafari = window.safari !== 'undefined' && typeof window.safari === 'object',
  33          isFirefox = ua.indexOf( 'firefox' ) !== -1;
  34  
  35      /**
  36       * Generates a password and updates the password input field.
  37       *
  38       * If zxcvbn is not yet loaded, it will wait and try again. If the password
  39       * input field is empty or the password wrapper is open, it will set the
  40       * password to the generated value. If the user has already entered a
  41       * password, it will check the strength of that password. It also binds
  42       * the toggle button to show or hide the password and sets the initial state
  43       * of the toggle button based on whether the password should be masked or
  44       * not. Finally, it updates the label for confirming weak passwords.
  45       */
  46  	function generatePassword() {
  47          if ( typeof zxcvbn !== 'function' ) {
  48              setTimeout( generatePassword, 50 );
  49              return;
  50          } else if ( ! $pass1.val() || $passwordWrapper.hasClass( 'is-open' ) ) {
  51              // zxcvbn loaded before user entered password, or generating new password.
  52              $pass1.val( $pass1.data( 'pw' ) );
  53              $pass1.trigger( 'pwupdate' );
  54              showOrHideWeakPasswordCheckbox();
  55          } else {
  56              // zxcvbn loaded after the user entered password, check strength.
  57              check_pass_strength();
  58              showOrHideWeakPasswordCheckbox();
  59          }
  60  
  61          /*
  62           * This works around a race condition when zxcvbn loads quickly and
  63           * causes `generatePassword()` to run prior to the toggle button being
  64           * bound.
  65           */
  66          bindToggleButton();
  67  
  68          // Install screen.
  69          if ( 1 !== parseInt( $toggleButton.data( 'start-masked' ), 10 ) ) {
  70              // Show the password not masked if admin_password hasn't been posted yet.
  71              $pass1.attr( 'type', 'text' );
  72          } else {
  73              // Otherwise, mask the password.
  74              $toggleButton.trigger( 'click' );
  75          }
  76  
  77          // Once zxcvbn loads, passwords strength is known.
  78          $( '#pw-weak-text-label' ).text( __( 'Confirm use of weak password' ) );
  79      }
  80  
  81      /**
  82       * Binds the password input field to update the current password and refresh the password strength area when the user types in the field.
  83       */
  84  	function bindPass1() {
  85          currentPass = $pass1.val();
  86  
  87          if ( 1 === parseInt( $pass1.data( 'reveal' ), 10 ) ) {
  88              generatePassword();
  89          }
  90  
  91          $pass1.on( 'input' + ' pwupdate', function () {
  92              if ( $pass1.val() === currentPass ) {
  93                  return;
  94              }
  95  
  96              currentPass = $pass1.val();
  97  
  98              // Refresh password strength area.
  99              $pass1.removeClass( 'short bad good strong' );
 100              showOrHideWeakPasswordCheckbox();
 101          } );
 102  
 103          bindCapsLockWarning( $pass1 );
 104      }
 105  
 106      /**
 107       * Resets the toggle button to show or hide the password.
 108       *
 109       * @param {boolean} show Whether to show the password or not.
 110       */
 111  	function resetToggle( show ) {
 112          $toggleButton
 113              .attr({
 114                  'aria-label': show ? __( 'Show password' ) : __( 'Hide password' )
 115              })
 116              .find( '.text' )
 117                  .text( show ? __( 'Show' ) : __( 'Hide' ) )
 118              .end()
 119              .find( '.dashicons' )
 120                  .removeClass( show ? 'dashicons-hidden' : 'dashicons-visibility' )
 121                  .addClass( show ? 'dashicons-visibility' : 'dashicons-hidden' );
 122      }
 123  
 124      /**
 125       * Binds the toggle button to show or hide the password. Also ensures that the password input type is set to password when the form is submitted.
 126       */
 127  	function bindToggleButton() {
 128          if ( !! $toggleButton ) {
 129              // Do not rebind.
 130              return;
 131          }
 132          $toggleButton = $pass1Row.find('.wp-hide-pw');
 133  
 134          // Toggle between showing and hiding the password.
 135          $toggleButton.show().on( 'click', function () {
 136              if ( 'password' === $pass1.attr( 'type' ) ) {
 137                  $pass1.attr( 'type', 'text' );
 138                  resetToggle( false );
 139              } else {
 140                  $pass1.attr( 'type', 'password' );
 141                  resetToggle( true );
 142              }
 143          });
 144  
 145          // Ensure the password input type is set to password when the form is submitted.
 146          $pass1Row.closest( 'form' ).on( 'submit', function() {
 147              if ( $pass1.attr( 'type' ) === 'text' ) {
 148                  $pass1.attr( 'type', 'password' );
 149                  resetToggle( true );
 150              }
 151          } );
 152      }
 153  
 154      /**
 155       * Handle the password reset button. Sets up an ajax callback to trigger sending
 156       * a password reset email.
 157       */
 158  	function bindPasswordResetLink() {
 159          $( '#generate-reset-link' ).on( 'click', function() {
 160              var $this  = $(this),
 161                  data = {
 162                      'user_id': userProfileL10n.user_id, // The user to send a reset to.
 163                      'nonce':   userProfileL10n.nonce    // Nonce to validate the action.
 164                  };
 165  
 166                  // Remove any previous error messages.
 167                  $this.parent().find( '.notice-error' ).remove();
 168  
 169                  // Send the reset request.
 170                  var resetAction =  wp.ajax.post( 'send-password-reset', data );
 171  
 172                  // Handle reset success.
 173                  resetAction.done( function( response ) {
 174                      addInlineNotice( $this, true, response );
 175                  } );
 176  
 177                  // Handle reset failure.
 178                  resetAction.fail( function( response ) {
 179                      addInlineNotice( $this, false, response );
 180                  } );
 181  
 182          });
 183  
 184      }
 185  
 186      /**
 187       * Helper function to insert an inline notice of success or failure.
 188       *
 189       * @param {jQuery}  $this   The button element: the message will be inserted
 190       *                          above this button
 191       * @param {boolean} success Whether the message is a success message.
 192       * @param {string}  message The message to insert.
 193       */
 194  	function addInlineNotice( $this, success, message ) {
 195          var resultDiv = $( '<div />', {
 196              role: 'alert'
 197          } );
 198  
 199          // Set up the notice div.
 200          resultDiv.addClass( 'notice inline' );
 201  
 202          // Add a class indicating success or failure.
 203          resultDiv.addClass( 'notice-' + ( success ? 'success' : 'error' ) );
 204  
 205          // Add the message, wrapping in a p tag, with a fadein to highlight each message.
 206          resultDiv.text( $( $.parseHTML( message ) ).text() ).wrapInner( '<p />');
 207  
 208          // Disable the button when the callback has succeeded.
 209          $this.prop( 'disabled', success );
 210  
 211          // Remove any previous notices.
 212          $this.siblings( '.notice' ).remove();
 213  
 214          // Insert the notice.
 215          $this.before( resultDiv );
 216      }
 217  
 218      /**
 219       * Initializes the password form, including the password strength meter, weak password checkbox, and show/hide password toggle button.
 220       */
 221  	function bindPasswordForm() {
 222          var $generateButton,
 223              $cancelButton;
 224  
 225          $pass1Row = $( '.user-pass1-wrap, .user-pass-wrap, .mailserver-pass-wrap, .reset-pass-submit' );
 226  
 227          // Hide the confirm password field when JavaScript support is enabled.
 228          $('.user-pass2-wrap').hide();
 229  
 230          $submitButton = $( '#submit, #wp-submit' ).on( 'click', function () {
 231              updateLock = false;
 232          });
 233  
 234          $submitButtons = $submitButton.add( ' #createusersub' );
 235  
 236          $weakRow = $( '.pw-weak' );
 237          $weakCheckbox = $weakRow.find( '.pw-checkbox' );
 238          $weakCheckbox.on( 'change', function() {
 239              $submitButtons.prop( 'disabled', ! $weakCheckbox.prop( 'checked' ) );
 240          } );
 241  
 242          $pass1 = $('#pass1, #mailserver_pass');
 243          if ( $pass1.length ) {
 244              bindPass1();
 245          } else {
 246              // Password field for the login form.
 247              $pass1 = $( '#user_pass' );
 248  
 249              bindCapsLockWarning( $pass1 );
 250          }
 251  
 252          /*
 253           * Fix a LastPass mismatch issue, LastPass only changes pass2.
 254           *
 255           * This fixes the issue by copying any changes from the hidden
 256           * pass2 field to the pass1 field, then running check_pass_strength.
 257           */
 258          $pass2 = $( '#pass2' ).on( 'input', function () {
 259              if ( $pass2.val().length > 0 ) {
 260                  $pass1.val( $pass2.val() );
 261                  $pass2.val('');
 262                  currentPass = '';
 263                  $pass1.trigger( 'pwupdate' );
 264              }
 265          } );
 266  
 267          // Disable hidden inputs to prevent autofill and submission.
 268          if ( $pass1.is( ':hidden' ) ) {
 269              $pass1.prop( 'disabled', true );
 270              $pass2.prop( 'disabled', true );
 271          }
 272  
 273          $passwordWrapper = $pass1Row.find( '.wp-pwd' );
 274          $generateButton  = $pass1Row.find( 'button.wp-generate-pw' );
 275  
 276          bindToggleButton();
 277  
 278          $generateButton.show();
 279          $generateButton.on( 'click', function () {
 280              updateLock = true;
 281  
 282              // Make sure the password fields are shown.
 283              $generateButton.not( '.skip-aria-expanded' ).attr( 'aria-expanded', 'true' );
 284              $passwordWrapper
 285                  .show()
 286                  .addClass( 'is-open' );
 287  
 288              // Enable the inputs when showing.
 289              $pass1.attr( 'disabled', false );
 290              $pass2.attr( 'disabled', false );
 291  
 292              // Set the password to the generated value.
 293              generatePassword();
 294  
 295              // Show generated password in plaintext by default.
 296              resetToggle ( false );
 297  
 298              // Generate the next password and cache.
 299              wp.ajax.post( 'generate-password' )
 300                  .done( function( data ) {
 301                      $pass1.data( 'pw', data );
 302                  } );
 303          } );
 304  
 305          $cancelButton = $pass1Row.find( 'button.wp-cancel-pw' );
 306          $cancelButton.on( 'click', function () {
 307              updateLock = false;
 308  
 309              // Disable the inputs when hiding to prevent autofill and submission.
 310              $pass1.prop( 'disabled', true );
 311              $pass2.prop( 'disabled', true );
 312  
 313              // Clear password field and update the UI.
 314              $pass1.val( '' ).trigger( 'pwupdate' );
 315              resetToggle( false );
 316  
 317              // Hide password controls.
 318              $passwordWrapper
 319                  .hide()
 320                  .removeClass( 'is-open' );
 321  
 322              // Stop an empty password from being submitted as a change.
 323              $submitButtons.prop( 'disabled', false );
 324  
 325              $generateButton.attr( 'aria-expanded', 'false' );
 326          } );
 327  
 328          $pass1Row.closest( 'form' ).on( 'submit', function () {
 329              updateLock = false;
 330  
 331              $pass1.prop( 'disabled', false );
 332              $pass2.prop( 'disabled', false );
 333              $pass2.val( $pass1.val() );
 334          });
 335      }
 336  
 337      /**
 338       * Sets CSS classes to the password strength results based on the password strength.
 339       */
 340  	function check_pass_strength() {
 341          var pass1 = $('#pass1').val(), strength;
 342  
 343          $('#pass-strength-result').removeClass('short bad good strong empty');
 344          if ( ! pass1 || '' ===  pass1.trim() ) {
 345              $( '#pass-strength-result' ).addClass( 'empty' ).html( '&nbsp;' );
 346              return;
 347          }
 348  
 349          strength = wp.passwordStrength.meter( pass1, wp.passwordStrength.userInputDisallowedList(), pass1 );
 350  
 351          switch ( strength ) {
 352              case -1:
 353                  $( '#pass-strength-result' ).addClass( 'bad' ).html( pwsL10n.unknown );
 354                  break;
 355              case 2:
 356                  $('#pass-strength-result').addClass('bad').html( pwsL10n.bad );
 357                  break;
 358              case 3:
 359                  $('#pass-strength-result').addClass('good').html( pwsL10n.good );
 360                  break;
 361              case 4:
 362                  $('#pass-strength-result').addClass('strong').html( pwsL10n.strong );
 363                  break;
 364              case 5:
 365                  $('#pass-strength-result').addClass('short').html( pwsL10n.mismatch );
 366                  break;
 367              default:
 368                  $('#pass-strength-result').addClass('short').html( pwsL10n.short );
 369          }
 370      }
 371  
 372      /**
 373       * Bind Caps Lock detection to a password input field.
 374       *
 375       * @param {jQuery} $input The password input field.
 376       */
 377  	function bindCapsLockWarning( $input ) {
 378          var $capsWarning,
 379              $capsIcon,
 380              $capsText,
 381              capsLockOn = false;
 382  
 383          // Skip warning on macOS Safari + Firefox (they show native indicators).
 384          if ( isMac && ( isSafari || isFirefox ) ) {
 385              return;
 386          }
 387  
 388          $capsWarning = $( '<div id="caps-warning" class="caps-warning"></div>' );
 389          $capsIcon    = $( '<span class="caps-icon" aria-hidden="true"><svg viewBox="0 0 24 26" xmlns="http://www.w3.org/2000/svg" fill="#3c434a" stroke="#3c434a" stroke-width="0.5"><path d="M12 5L19 15H16V19H8V15H5L12 5Z"/><rect x="8" y="21" width="8" height="1.5" rx="0.75"/></svg></span>' );
 390          $capsText    = $( '<span>', { 'class': 'caps-warning-text', text: __( 'Caps lock is on.' ) } );
 391          $capsWarning.append( $capsIcon, $capsText );
 392  
 393          $input.parent( 'div' ).append( $capsWarning );
 394  
 395          $input.on( 'keydown', function( jqEvent ) {
 396              var event = jqEvent.originalEvent;
 397  
 398              // Skip if key is not a printable character.
 399              // Key length > 1 usually means non-printable (e.g., "Enter", "Tab").
 400              if ( event.ctrlKey || event.metaKey || event.altKey || ! event.key || event.key.length !== 1 ) {
 401                  return;
 402              }
 403  
 404              var state = isCapsLockOn( event );
 405  
 406              // React when the state changes or if caps lock is on when the user starts typing.
 407              if ( state !== capsLockOn ) {
 408                  capsLockOn = state;
 409  
 410                  if ( capsLockOn ) {
 411                      $capsWarning.show();
 412                      // Don't duplicate existing screen reader Caps lock notifications.
 413                      if ( event.key !== 'CapsLock' ) {
 414                          wp.a11y.speak( __( 'Caps lock is on.' ), 'assertive' );
 415                      }
 416                  } else {
 417                      $capsWarning.hide();
 418                  }
 419              }
 420          } );
 421  
 422          $input.on( 'blur', function() {
 423              if ( ! document.hasFocus() ) {
 424                  return;
 425              }
 426              capsLockOn = false;
 427              $capsWarning.hide();
 428          } );
 429      }
 430  
 431      /**
 432       * Determines if Caps Lock is currently enabled.
 433       *
 434       * On macOS Safari and Firefox, the native warning is preferred,
 435       * so this function returns false to suppress custom warnings.
 436       *
 437       * @param {KeyboardEvent} event The keydown event object.
 438       *
 439       * @return {boolean} True if Caps Lock is on, false otherwise.
 440       */
 441  	function isCapsLockOn( event ) {
 442          return event.getModifierState( 'CapsLock' );
 443      }
 444  
 445      /**
 446       * Toggles the visibility of the weak password checkbox.
 447       */
 448  	function showOrHideWeakPasswordCheckbox() {
 449          var passStrengthResult = $('#pass-strength-result');
 450  
 451          if ( passStrengthResult.length ) {
 452              var passStrength = passStrengthResult[0];
 453  
 454              if ( passStrength.className ) {
 455                  $pass1.addClass( passStrength.className );
 456                  if ( $( passStrength ).is( '.short, .bad' ) ) {
 457                      if ( ! $weakCheckbox.prop( 'checked' ) ) {
 458                          $submitButtons.prop( 'disabled', true );
 459                      }
 460                      $weakRow.show();
 461                  } else {
 462                      if ( $( passStrength ).is( '.empty' ) ) {
 463                          $submitButtons.prop( 'disabled', true );
 464                          $weakCheckbox.prop( 'checked', false );
 465                      } else {
 466                          $submitButtons.prop( 'disabled', false );
 467                      }
 468                      $weakRow.hide();
 469                  }
 470              }
 471          }
 472      }
 473  
 474      // Debug information copy section.
 475      clipboard.on( 'success', function( e ) {
 476          var triggerElement = $( e.trigger ),
 477              successElement = $( '.success', triggerElement.closest( '.application-password-display' ) );
 478  
 479          // Clear the selection and move focus back to the trigger.
 480          e.clearSelection();
 481  
 482          // Show success visual feedback.
 483          clearTimeout( successTimeout );
 484          successElement.removeClass( 'hidden' );
 485  
 486          // Hide success visual feedback after 3 seconds since last success.
 487          successTimeout = setTimeout( function() {
 488              successElement.addClass( 'hidden' );
 489          }, 3000 );
 490  
 491          // Handle success audible feedback.
 492          wp.a11y.speak( __( 'Application password has been copied to your clipboard.' ) );
 493      } );
 494  
 495      $( function() {
 496          var $colorpicker, $stylesheet, user_id, current_user_id,
 497              select       = $( '#display_name' ),
 498              current_name = select.val(),
 499              greeting     = $( '#wp-admin-bar-my-account' ).find( '.display-name' );
 500  
 501          $( '#pass1' ).val( '' ).on( 'input' + ' pwupdate', check_pass_strength );
 502          $('#pass-strength-result').show();
 503          $('.color-palette').on( 'click', function() {
 504              $(this).siblings('input[name="admin_color"]').prop('checked', true);
 505          });
 506  
 507          if ( select.length ) {
 508              $('#first_name, #last_name, #nickname').on( 'blur.user_profile', function() {
 509                  var dub = [],
 510                      inputs = {
 511                          display_nickname  : $('#nickname').val() || '',
 512                          display_username  : $('#user_login').val() || '',
 513                          display_firstname : $('#first_name').val() || '',
 514                          display_lastname  : $('#last_name').val() || ''
 515                      };
 516  
 517                  if ( inputs.display_firstname && inputs.display_lastname ) {
 518                      inputs.display_firstlast = inputs.display_firstname + ' ' + inputs.display_lastname;
 519                      inputs.display_lastfirst = inputs.display_lastname + ' ' + inputs.display_firstname;
 520                  }
 521  
 522                  $.each( $('option', select), function( i, el ){
 523                      dub.push( el.value );
 524                  });
 525  
 526                  $.each(inputs, function( id, value ) {
 527                      if ( ! value ) {
 528                          return;
 529                      }
 530  
 531                      var val = value.replace(/<\/?[a-z][^>]*>/gi, '');
 532  
 533                      if ( inputs[id].length && $.inArray( val, dub ) === -1 ) {
 534                          dub.push(val);
 535                          $('<option />', {
 536                              'text': val
 537                          }).appendTo( select );
 538                      }
 539                  });
 540              });
 541  
 542              /**
 543               * Replaces "Howdy, *" in the admin toolbar whenever the display name dropdown is updated for one's own profile.
 544               */
 545              select.on( 'change', function() {
 546                  if ( user_id !== current_user_id ) {
 547                      return;
 548                  }
 549  
 550                  var display_name = this.value.trim() || current_name;
 551  
 552                  greeting.text( display_name );
 553              } );
 554          }
 555  
 556          $colorpicker = $( '#color-picker' );
 557          $stylesheet = $( '#colors-css' );
 558          user_id = $( 'input#user_id' ).val();
 559          current_user_id = $( 'input[name="checkuser_id"]' ).val();
 560  
 561          $colorpicker.on( 'click.colorpicker', '.color-option', function() {
 562              var colors,
 563                  $this = $(this);
 564  
 565              if ( $this.hasClass( 'selected' ) ) {
 566                  return;
 567              }
 568  
 569              $this.siblings( '.selected' ).removeClass( 'selected' );
 570              $this.addClass( 'selected' ).find( 'input[type="radio"]' ).prop( 'checked', true );
 571  
 572              // Set color scheme.
 573              if ( user_id === current_user_id ) {
 574                  // Load the colors stylesheet.
 575                  // The default color scheme won't have one, so we'll need to create an element.
 576                  if ( 0 === $stylesheet.length ) {
 577                      $stylesheet = $( '<link rel="stylesheet" />' ).appendTo( 'head' );
 578                  }
 579                  $stylesheet.attr( 'href', $this.children( '.css_url' ).val() );
 580  
 581                  // Repaint icons.
 582                  if ( typeof wp !== 'undefined' && wp.svgPainter ) {
 583                      try {
 584                          colors = JSON.parse( $this.children( '.icon_colors' ).val() );
 585                      } catch ( error ) {}
 586  
 587                      if ( colors ) {
 588                          wp.svgPainter.setColors( colors );
 589                          wp.svgPainter.paint();
 590                      }
 591                  }
 592  
 593                  // Update user option.
 594                  $.post( ajaxurl, {
 595                      action:       'save-user-color-scheme',
 596                      color_scheme: $this.children( 'input[name="admin_color"]' ).val(),
 597                      nonce:        $('#color-nonce').val()
 598                  }).done( function( response ) {
 599                      if ( response.success ) {
 600                          $( 'body' ).removeClass( response.data.previousScheme ).addClass( response.data.currentScheme );
 601                      }
 602                  });
 603              }
 604          });
 605  
 606          bindPasswordForm();
 607          bindPasswordResetLink();
 608          $submitButtons.on( 'click', function() {
 609              isSubmitting = true;
 610          });
 611  
 612          $form = $( '#your-profile, #createuser' );
 613          originalFormContent = $form.serialize();
 614      });
 615  
 616      $( '#destroy-sessions' ).on( 'click', function( e ) {
 617          var $this = $(this);
 618  
 619          wp.ajax.post( 'destroy-sessions', {
 620              nonce: $( '#_wpnonce' ).val(),
 621              user_id: $( '#user_id' ).val()
 622          }).done( function( response ) {
 623              $this.prop( 'disabled', true );
 624              $this.siblings( '.notice' ).remove();
 625              $this.before( '<div class="notice notice-success inline" role="alert"><p>' + response.message + '</p></div>' );
 626          }).fail( function( response ) {
 627              $this.siblings( '.notice' ).remove();
 628              $this.before( '<div class="notice notice-error inline" role="alert"><p>' + response.message + '</p></div>' );
 629          });
 630  
 631          e.preventDefault();
 632      });
 633  
 634      window.generatePassword = generatePassword;
 635  
 636      // Warn the user if password was generated but not saved.
 637      $( window ).on( 'beforeunload', function () {
 638          if ( true === updateLock ) {
 639              return __( 'Your new password has not been saved.' );
 640          }
 641          if ( originalFormContent !== $form.serialize() && ! isSubmitting ) {
 642              return __( 'The changes you made will be lost if you navigate away from this page.' );
 643          }
 644      });
 645  
 646      /*
 647       * We need to generate a password as soon as the Reset Password page is loaded,
 648       * to avoid double clicking the button to retrieve the first generated password.
 649       * See ticket #39638.
 650       */
 651      $( function() {
 652          if ( $( '.reset-pass-submit' ).length ) {
 653              $( '.reset-pass-submit button.wp-generate-pw' ).trigger( 'click' );
 654          }
 655      });
 656  
 657  })(jQuery);


Generated : Wed Sep 23 08:20:35 2026 Cross-referenced by PHPXref