├── .coveralls.yml ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .travis.yml ├── Vagrantfile ├── application-passwords.css ├── application-passwords.js ├── application-passwords.php ├── assets ├── icon-128x128.png ├── icon-256x256.png ├── icon.svg ├── screenshot-1.png └── screenshot-2.png ├── auth-app.js ├── class.application-passwords-list-table.php ├── class.application-passwords.php ├── composer.json ├── composer.lock ├── docker-compose.yml ├── gruntfile.js ├── package-lock.json ├── package.json ├── phpcs.xml.dist ├── phpunit.xml.dist ├── readme.md ├── readme.txt └── tests ├── logs └── .gitignore └── test-class.application-passwords.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | indent_style = tab 9 | 10 | [*.{json,yml,yaml}] 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /node_modules/ 3 | **/*.min.js 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true 4 | }, 5 | "extends": [ 6 | "plugin:@wordpress/eslint-plugin/es5" 7 | ], 8 | "rules": { 9 | "camelcase": "off", 10 | "vars-on-top": "warn", 11 | "comma-dangle": "off" 12 | }, 13 | "globals": { 14 | "jQuery": "readonly" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vagrant/ 2 | /vendor/ 3 | /node_modules/ 4 | /dist/ 5 | /phpcs.xml 6 | /phpunit.xml 7 | /console.log 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: php 4 | 5 | php: 6 | - "5.6" 7 | - "7.0" 8 | - "7.2" 9 | - "7.3" 10 | - "7.4" 11 | 12 | services: 13 | - mysql 14 | 15 | addons: 16 | apt: 17 | packages: 18 | - libxml2-utils # For xmllint. 19 | 20 | env: 21 | - WP_VERSION=latest WP_MULTISITE=0 22 | - WP_VERSION=latest WP_MULTISITE=1 23 | - WP_VERSION=trunk WP_MULTISITE=0 24 | - WP_VERSION=trunk WP_MULTISITE=1 25 | 26 | install: 27 | - composer self-update --1 28 | - npm install 29 | - export DEV_LIB_PATH="vendor/xwp/wp-dev-lib/scripts" 30 | - source "$DEV_LIB_PATH/travis.install.sh" 31 | 32 | script: 33 | - npm run lint-js 34 | - source "$DEV_LIB_PATH/travis.script.sh" 35 | 36 | after_script: 37 | - source "$DEV_LIB_PATH/travis.after_script.sh" 38 | 39 | cache: 40 | directories: 41 | - $HOME/.composer/cache 42 | - $HOME/.npm 43 | 44 | branches: 45 | only: 46 | - master 47 | 48 | notifications: 49 | email: false 50 | -------------------------------------------------------------------------------- /Vagrantfile: -------------------------------------------------------------------------------- 1 | load File.join( 2 | File.dirname(__FILE__), 3 | "vendor/wpsh/local/Vagrantfile" 4 | ) 5 | 6 | Vagrant.configure(2) do |config| 7 | config.vm.hostname = "application-passwords" 8 | end 9 | -------------------------------------------------------------------------------- /application-passwords.css: -------------------------------------------------------------------------------- 1 | .app-pass-dialog-background { 2 | opacity: 1.0; 3 | background: rgba(0,0,0,0.7); 4 | } 5 | 6 | .app-pass-dialog { 7 | padding: 20px; 8 | } 9 | 10 | .new-application-password-content { 11 | padding-bottom: 20px; 12 | padding-top: 10px; 13 | } -------------------------------------------------------------------------------- /application-passwords.js: -------------------------------------------------------------------------------- 1 | /* global appPass, wp */ 2 | /* eslint-disable no-alert */ 3 | ( function( $, appPass ) { 4 | var $appPassSection = $( '#application-passwords-section' ), 5 | $newAppPassForm = $appPassSection.find( '.create-application-password' ), 6 | $newAppPassField = $newAppPassForm.find( '.input' ), 7 | $newAppPassButton = $newAppPassForm.find( '.button' ), 8 | $appPassTwrapper = $appPassSection.find( '.application-passwords-list-table-wrapper' ), 9 | $appPassTbody = $appPassSection.find( 'tbody' ), 10 | $appPassTrNoItems = $appPassTbody.find( '.no-items' ), 11 | $removeAllBtn = $( '#revoke-all-application-passwords' ), 12 | tmplNewAppPass = wp.template( 'new-application-password' ), 13 | tmplAppPassRow = wp.template( 'application-password-row' ), 14 | tmplNotice = wp.template( 'application-password-notice' ), 15 | testBasicAuthUser = Math.random().toString( 36 ).replace( /[^a-z]+/g, '' ), 16 | testBasicAuthPassword = Math.random().toString( 36 ).replace( /[^a-z]+/g, '' ); 17 | 18 | $.ajax( { 19 | url: appPass.root + appPass.namespace + '/test-basic-authorization-header', 20 | method: 'POST', 21 | beforeSend: function( xhr ) { 22 | xhr.setRequestHeader( 'Authorization', 'Basic ' + btoa( testBasicAuthUser + ':' + testBasicAuthPassword ) ); 23 | }, 24 | error: function( jqXHR ) { 25 | if ( 404 === jqXHR.status ) { 26 | $newAppPassForm.before( tmplNotice( { 27 | type: 'error', 28 | message: appPass.text.no_credentials 29 | } ) ); 30 | } 31 | } 32 | } ).done( function( response ) { 33 | if ( response.PHP_AUTH_USER === testBasicAuthUser && response.PHP_AUTH_PW === testBasicAuthPassword ) { 34 | // Save the success in SessionStorage or the like, so we don't do it on every page load? 35 | } else { 36 | $newAppPassForm.before( tmplNotice( { 37 | type: 'error', 38 | message: appPass.text.no_credentials 39 | } ) ); 40 | } 41 | } ); 42 | 43 | $newAppPassButton.click( function( e ) { 44 | e.preventDefault(); 45 | var name = $newAppPassField.val(); 46 | 47 | if ( 0 === name.length ) { 48 | $newAppPassField.focus(); 49 | return; 50 | } 51 | 52 | $newAppPassField.prop( 'disabled', true ); 53 | $newAppPassButton.prop( 'disabled', true ); 54 | 55 | $.ajax( { 56 | url: appPass.root + appPass.namespace + '/application-passwords/' + appPass.user_id + '/add', 57 | method: 'POST', 58 | beforeSend: function( xhr ) { 59 | xhr.setRequestHeader( 'X-WP-Nonce', appPass.nonce ); 60 | }, 61 | data: { 62 | name: name 63 | } 64 | } ).done( function( response ) { 65 | $newAppPassField.prop( 'disabled', false ).val( '' ); 66 | $newAppPassButton.prop( 'disabled', false ); 67 | 68 | $newAppPassForm.after( tmplNewAppPass( { 69 | name: name, 70 | password: response.password 71 | } ) ); 72 | 73 | $appPassTbody.prepend( tmplAppPassRow( response.row ) ); 74 | 75 | $appPassTwrapper.show(); 76 | $appPassTrNoItems.remove(); 77 | } ); 78 | } ); 79 | 80 | $appPassTbody.on( 'click', '.delete', function( e ) { 81 | e.preventDefault(); 82 | 83 | if ( ! confirm( appPass.text.revoke_password ) ) { 84 | return; 85 | } 86 | 87 | var $tr = $( e.target ).closest( 'tr' ), 88 | slug = $tr.data( 'slug' ); 89 | 90 | $.ajax( { 91 | url: appPass.root + appPass.namespace + '/application-passwords/' + appPass.user_id + '/' + slug, 92 | method: 'DELETE', 93 | beforeSend: function( xhr ) { 94 | xhr.setRequestHeader( 'X-WP-Nonce', appPass.nonce ); 95 | } 96 | } ).done( function( response ) { 97 | if ( response ) { 98 | if ( 0 === $tr.siblings().length ) { 99 | $appPassTwrapper.hide(); 100 | } 101 | $tr.remove(); 102 | } 103 | } ); 104 | } ); 105 | 106 | $removeAllBtn.on( 'click', function( e ) { 107 | e.preventDefault(); 108 | 109 | if ( ! confirm( appPass.text.revoke_all_passwords ) ) { 110 | return; 111 | } 112 | 113 | $.ajax( { 114 | url: appPass.root + appPass.namespace + '/application-passwords/' + appPass.user_id, 115 | method: 'DELETE', 116 | beforeSend: function( xhr ) { 117 | xhr.setRequestHeader( 'X-WP-Nonce', appPass.nonce ); 118 | } 119 | } ).done( function( response ) { 120 | if ( parseInt( response, 10 ) > 0 ) { 121 | $appPassTbody.children().remove(); 122 | $appPassSection.children( '.new-application-password' ).remove(); 123 | $appPassTwrapper.hide(); 124 | } 125 | } ); 126 | } ); 127 | 128 | $( document ).on( 'click', '.application-password-modal-dismiss', function( e ) { 129 | e.preventDefault(); 130 | 131 | $( '.new-application-password.notification-dialog-wrap' ).hide(); 132 | } ); 133 | 134 | // If there are no items, don't display the table yet. If there are, show it. 135 | if ( 0 === $appPassTbody.children( 'tr' ).not( $appPassTrNoItems ).length ) { 136 | $appPassTwrapper.hide(); 137 | } 138 | }( jQuery, appPass ) ); 139 | -------------------------------------------------------------------------------- /application-passwords.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /assets/screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/application-passwords/2e6c19ab51daefe442e5b5b8e695a12c5982f6c4/assets/screenshot-1.png -------------------------------------------------------------------------------- /assets/screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WordPress/application-passwords/2e6c19ab51daefe442e5b5b8e695a12c5982f6c4/assets/screenshot-2.png -------------------------------------------------------------------------------- /auth-app.js: -------------------------------------------------------------------------------- 1 | /* global authApp */ 2 | ( function( $, authApp ) { 3 | var $appNameField = $( '#app_name' ), 4 | $approveBtn = $( '#approve' ), 5 | $rejectBtn = $( '#reject' ), 6 | $form = $appNameField.closest( 'form' ); 7 | 8 | $approveBtn.click( function( e ) { 9 | var name = $appNameField.val(); 10 | 11 | e.preventDefault(); 12 | 13 | if ( 0 === name.length ) { 14 | $appNameField.focus(); 15 | return; 16 | } 17 | 18 | $appNameField.prop( 'disabled', true ); 19 | $approveBtn.prop( 'disabled', true ); 20 | 21 | $.ajax( { 22 | url: authApp.root + authApp.namespace + '/application-passwords/' + authApp.user_id + '/add', 23 | method: 'POST', 24 | beforeSend: function( xhr ) { 25 | xhr.setRequestHeader( 'X-WP-Nonce', authApp.nonce ); 26 | }, 27 | data: { 28 | name: name 29 | } 30 | } ).done( function( response ) { 31 | var raw = authApp.success, 32 | url, $display; 33 | 34 | if ( raw ) { 35 | url = raw + ( -1 === raw.indexOf( '?' ) ? '?' : '&' ) + 36 | 'user_login=' + encodeURIComponent( authApp.user_login ) + 37 | '&password=' + encodeURIComponent( response.password ); 38 | 39 | window.location = url; 40 | } else { 41 | // Should we maybe just reuse the js template modal from the profile page? 42 | $form.replaceWith( '

' + 43 | authApp.strings.new_pass 44 | .replace( '%1$s', '' ) 45 | .replace( '%2$s', '' ) + 46 | '

' ); 47 | 48 | $display = $( '.js-password-display' ); 49 | 50 | // We're using .text() to write the variables to avoid any chance of XSS. 51 | $display.find( 'strong' ).text( name ); 52 | $display.find( 'kbd' ).text( response.password ); 53 | } 54 | } ); 55 | } ); 56 | 57 | $rejectBtn.click( function( e ) { 58 | e.preventDefault(); 59 | 60 | // @todo: Make a better way to do this so it feels like less of a semi-open redirect. 61 | window.location = authApp.reject; 62 | } ); 63 | 64 | $form.on( 'submit', function( e ) { 65 | e.preventDefault(); 66 | } ); 67 | }( jQuery, authApp ) ); 68 | -------------------------------------------------------------------------------- /class.application-passwords-list-table.php: -------------------------------------------------------------------------------- 1 | wp_strip_all_tags( __( 'Name' ) ), 27 | 'created' => wp_strip_all_tags( __( 'Created' ) ), 28 | 'last_used' => wp_strip_all_tags( __( 'Last Used' ) ), 29 | 'last_ip' => wp_strip_all_tags( __( 'Last IP' ) ), 30 | 'revoke' => wp_strip_all_tags( __( 'Revoke' ) ), 31 | ); 32 | } 33 | 34 | /** 35 | * Prepares the list of items for displaying. 36 | * 37 | * @since 0.1-dev 38 | */ 39 | public function prepare_items() { 40 | $columns = $this->get_columns(); 41 | $hidden = array(); 42 | $sortable = array(); 43 | $primary = 'name'; 44 | $this->_column_headers = array( $columns, $hidden, $sortable, $primary ); 45 | } 46 | 47 | /** 48 | * Generates content for a single row of the table 49 | * 50 | * @since 0.1-dev 51 | * @access protected 52 | * 53 | * @param object $item The current item. 54 | * @param string $column_name The current column name. 55 | */ 56 | protected function column_default( $item, $column_name ) { 57 | switch ( $column_name ) { 58 | case 'name': 59 | return esc_html( $item['name'] ); 60 | case 'created': 61 | if ( empty( $item['created'] ) ) { 62 | return '—'; 63 | } 64 | return date( get_option( 'date_format', 'r' ), $item['created'] ); 65 | case 'last_used': 66 | if ( empty( $item['last_used'] ) ) { 67 | return '—'; 68 | } 69 | return date( get_option( 'date_format', 'r' ), $item['last_used'] ); 70 | case 'last_ip': 71 | if ( empty( $item['last_ip'] ) ) { 72 | return '—'; 73 | } 74 | return $item['last_ip']; 75 | case 'revoke': 76 | return get_submit_button( __( 'Revoke' ), 'delete', 'revoke-application-password', false ); 77 | default: 78 | return 'WTF^^?'; 79 | } 80 | } 81 | 82 | /** 83 | * Generates custom table navigation to prevent conflicting nonces. 84 | * 85 | * @since 0.1-dev 86 | * @access protected 87 | * 88 | * @param string $which The location of the bulk actions: 'top' or 'bottom'. 89 | */ 90 | protected function display_tablenav( $which ) { 91 | ?> 92 |
93 | 94 | 95 |
96 | 97 |
98 | 99 | 100 |
101 | bulk_actions( $which ); ?> 102 |
103 | extra_tablenav( $which ); 105 | $this->pagination( $which ); 106 | ?> 107 | 108 |
109 |
110 | '; 122 | $this->single_row_columns( $item ); 123 | echo ''; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /class.application-passwords.php: -------------------------------------------------------------------------------- 1 | ID ) { 61 | /* 62 | * For our authentication to work, we need to remove the cached lack 63 | * of a current user, so the next time it checks, we can detect that 64 | * this is a rest api request and allow our override to happen. This 65 | * is because the constant is defined later than the first get current 66 | * user call may run. 67 | */ 68 | $current_user = null; 69 | } 70 | return $class; 71 | } 72 | 73 | /** 74 | * Handle declaration of REST API endpoints. 75 | * 76 | * @since 0.1-dev 77 | * 78 | * @access public 79 | * @static 80 | */ 81 | public static function rest_api_init() { 82 | // List existing application passwords. 83 | register_rest_route( '2fa/v1', '/application-passwords/(?P[\d]+)', array( 84 | 'methods' => WP_REST_Server::READABLE, 85 | 'callback' => __CLASS__ . '::rest_list_application_passwords', 86 | 'permission_callback' => __CLASS__ . '::rest_edit_user_callback', 87 | ) ); 88 | 89 | // Add new application passwords. 90 | register_rest_route( '2fa/v1', '/application-passwords/(?P[\d]+)/add', array( 91 | 'methods' => WP_REST_Server::CREATABLE, 92 | 'callback' => __CLASS__ . '::rest_add_application_password', 93 | 'permission_callback' => __CLASS__ . '::rest_edit_user_callback', 94 | 'args' => array( 95 | 'name' => array( 96 | 'required' => true, 97 | ), 98 | ), 99 | ) ); 100 | 101 | // Delete an application password. 102 | register_rest_route( '2fa/v1', '/application-passwords/(?P[\d]+)/(?P[\da-fA-F]{12})', array( 103 | 'methods' => WP_REST_Server::DELETABLE, 104 | 'callback' => __CLASS__ . '::rest_delete_application_password', 105 | 'permission_callback' => __CLASS__ . '::rest_edit_user_callback', 106 | ) ); 107 | 108 | // Delete all application passwords for a given user. 109 | register_rest_route( '2fa/v1', '/application-passwords/(?P[\d]+)', array( 110 | 'methods' => WP_REST_Server::DELETABLE, 111 | 'callback' => __CLASS__ . '::rest_delete_all_application_passwords', 112 | 'permission_callback' => __CLASS__ . '::rest_edit_user_callback', 113 | ) ); 114 | 115 | // Some hosts that run PHP in FastCGI mode won't be given the Authentication header. 116 | register_rest_route( '2fa/v1', '/test-basic-authorization-header/', array( 117 | 'methods' => WP_REST_Server::READABLE . ', ' . WP_REST_Server::CREATABLE, 118 | 'callback' => __CLASS__ . '::rest_test_basic_authorization_header', 119 | 'permission_callback' => '__return_true', 120 | ) ); 121 | } 122 | 123 | /** 124 | * REST API endpoint to list existing application passwords for a user. 125 | * 126 | * @since 0.1-dev 127 | * 128 | * @access public 129 | * @static 130 | * 131 | * @param $data 132 | * 133 | * @return array 134 | */ 135 | public static function rest_list_application_passwords( $data ) { 136 | $application_passwords = self::get_user_application_passwords( $data['user_id'] ); 137 | $with_slugs = array(); 138 | 139 | if ( $application_passwords ) { 140 | foreach ( $application_passwords as $item ) { 141 | $item['slug'] = self::password_unique_slug( $item ); 142 | unset( $item['raw'] ); 143 | unset( $item['password'] ); 144 | 145 | $item['created'] = date( get_option( 'date_format', 'r' ), $item['created'] ); 146 | 147 | if ( empty( $item['last_used'] ) ) { 148 | $item['last_used'] = '—'; 149 | } else { 150 | $item['last_used'] = date( get_option( 'date_format', 'r' ), $item['last_used'] ); 151 | } 152 | 153 | if ( empty( $item['last_ip'] ) ) { 154 | $item['last_ip'] = '—'; 155 | } 156 | 157 | $with_slugs[ $item['slug'] ] = $item; 158 | } 159 | } 160 | 161 | return $with_slugs; 162 | } 163 | 164 | /** 165 | * REST API endpoint to add a new application password for a user. 166 | * 167 | * @since 0.1-dev 168 | * 169 | * @access public 170 | * @static 171 | * 172 | * @param $data 173 | * 174 | * @return array 175 | */ 176 | public static function rest_add_application_password( $data ) { 177 | list( $new_password, $new_item ) = self::create_new_application_password( $data['user_id'], $data['name'] ); 178 | 179 | // Some tidying before we return it. 180 | $new_item['slug'] = self::password_unique_slug( $new_item ); 181 | $new_item['created'] = date( get_option( 'date_format', 'r' ), $new_item['created'] ); 182 | $new_item['last_used'] = '—'; 183 | $new_item['last_ip'] = '—'; 184 | unset( $new_item['password'] ); 185 | 186 | return array( 187 | 'row' => $new_item, 188 | 'password' => self::chunk_password( $new_password ) 189 | ); 190 | } 191 | 192 | /** 193 | * REST API endpoint to delete a given application password. 194 | * 195 | * @since 0.1-dev 196 | * 197 | * @access public 198 | * @static 199 | * 200 | * @param $data 201 | * 202 | * @return bool 203 | */ 204 | public static function rest_delete_application_password( $data ) { 205 | return self::delete_application_password( $data['user_id'], $data['slug'] ); 206 | } 207 | 208 | /** 209 | * REST API endpoint to delete all of a user's application passwords. 210 | * 211 | * @since 0.1-dev 212 | * 213 | * @access public 214 | * @static 215 | * 216 | * @param $data 217 | * 218 | * @return int The number of deleted passwords 219 | */ 220 | public static function rest_delete_all_application_passwords( $data ) { 221 | return self::delete_all_application_passwords( $data['user_id'] ); 222 | } 223 | 224 | /** 225 | * Whether or not the current user can edit the specified user. 226 | * 227 | * @since 0.1-dev 228 | * 229 | * @access public 230 | * @static 231 | * 232 | * @param $data 233 | * 234 | * @return bool 235 | */ 236 | public static function rest_edit_user_callback( $data ) { 237 | return current_user_can( 'edit_user', $data['user_id'] ); 238 | } 239 | 240 | /** 241 | * Loosely Based on https://github.com/WP-API/Basic-Auth/blob/master/basic-auth.php 242 | * 243 | * @since 0.1-dev 244 | * 245 | * @access public 246 | * @static 247 | * 248 | * @param $input_user 249 | * 250 | * @return WP_User|bool 251 | */ 252 | public static function rest_api_auth_handler( $input_user ){ 253 | // Don't authenticate twice. 254 | if ( ! empty( $input_user ) ) { 255 | return $input_user; 256 | } 257 | 258 | // Check that we're trying to authenticate 259 | if ( ! isset( $_SERVER['PHP_AUTH_USER'] ) ) { 260 | return $input_user; 261 | } 262 | 263 | $user = self::authenticate( $input_user, $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ); 264 | 265 | if ( $user instanceof WP_User ) { 266 | return $user->ID; 267 | } 268 | 269 | // If it wasn't a user what got returned, just pass on what we had received originally. 270 | return $input_user; 271 | } 272 | 273 | /** 274 | * Test whether PHP can see Basic Authorization headers passed to the web server. 275 | * 276 | * @return WP_Error|array 277 | */ 278 | public static function rest_test_basic_authorization_header() { 279 | $response = array(); 280 | 281 | if ( isset( $_SERVER['PHP_AUTH_USER'] ) ) { 282 | $response['PHP_AUTH_USER'] = $_SERVER['PHP_AUTH_USER']; 283 | } 284 | 285 | if ( isset( $_SERVER['PHP_AUTH_PW'] ) ) { 286 | $response['PHP_AUTH_PW'] = $_SERVER['PHP_AUTH_PW']; 287 | } 288 | 289 | if ( empty( $response ) ) { 290 | return new WP_Error( 'no-credentials', __( 'No HTTP Basic Authorization credentials were found submitted with this request.' ), array( 'status' => 404 ) ); 291 | } 292 | 293 | return $response; 294 | } 295 | 296 | /** 297 | * Some servers running in CGI or FastCGI mode don't pass the Authorization 298 | * header on to WordPress. If it's been rewritten to the `REMOTE_USER` header, 299 | * fill in the proper $_SERVER variables instead. 300 | */ 301 | public static function fallback_populate_username_password() { 302 | // If we don't have anything to pull from, return early. 303 | if ( ! isset( $_SERVER['REMOTE_USER'] ) && ! isset( $_SERVER['REDIRECT_REMOTE_USER'] ) ) { 304 | return; 305 | } 306 | 307 | // If either PHP_AUTH key is already set, do nothing. 308 | if ( isset( $_SERVER['PHP_AUTH_USER'] ) || isset( $_SERVER['PHP_AUTH_PW'] ) ) { 309 | return; 310 | } 311 | 312 | // From our prior conditional, one of these must be set. 313 | $header = isset( $_SERVER['REMOTE_USER'] ) ? $_SERVER['REMOTE_USER'] : $_SERVER['REDIRECT_REMOTE_USER']; 314 | 315 | // Test to make sure the pattern matches expected. 316 | if ( ! preg_match( '%^Basic [a-z\d/+]*={0,2}$%i', $header ) ) { 317 | return; 318 | } 319 | 320 | // Removing `Basic ` the token would start six characters in. 321 | $token = substr( $header, 6 ); 322 | $userpass = base64_decode( $token ); 323 | list( $user, $pass ) = explode( ':', $userpass ); 324 | 325 | // Now shove them in the proper keys where we're expecting later on. 326 | $_SERVER['PHP_AUTH_USER'] = $user; 327 | $_SERVER['PHP_AUTH_PW'] = $pass; 328 | 329 | return array( $user, $pass ); 330 | } 331 | 332 | /** 333 | * Check if the current request is an API request 334 | * for which we should check the HTTP Auth headers. 335 | * 336 | * @return boolean 337 | */ 338 | public static function is_api_request() { 339 | // Process the authentication only after the APIs have been initialized. 340 | return ( ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST ) || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ); 341 | } 342 | 343 | /** 344 | * Filter the user to authenticate. 345 | * 346 | * @since 0.1-dev 347 | * 348 | * @access public 349 | * @static 350 | * 351 | * @param WP_User $input_user User to authenticate. 352 | * @param string $username User login. 353 | * @param string $password User password. 354 | * 355 | * @return mixed 356 | */ 357 | public static function authenticate( $input_user, $username, $password ) { 358 | if ( ! apply_filters( 'application_password_is_api_request', self::is_api_request() ) ) { 359 | return $input_user; 360 | } 361 | 362 | $user = get_user_by( 'login', $username ); 363 | 364 | if ( ! $user && is_email( $username ) ) { 365 | $user = get_user_by( 'email', $username ); 366 | } 367 | 368 | // If the login name is invalid, short circuit. 369 | if ( ! $user ) { 370 | return $input_user; 371 | } 372 | 373 | /* 374 | * Strip out anything non-alphanumeric. This is so passwords can be used with 375 | * or without spaces to indicate the groupings for readability. 376 | * 377 | * Generated application passwords are exclusively alphanumeric. 378 | */ 379 | $password = preg_replace( '/[^a-z\d]/i', '', $password ); 380 | 381 | $hashed_passwords = get_user_meta( $user->ID, self::USERMETA_KEY_APPLICATION_PASSWORDS, true ); 382 | 383 | // If there aren't any, there's nothing to return. Avoid the foreach. 384 | if ( empty( $hashed_passwords ) ) { 385 | return $input_user; 386 | } 387 | 388 | foreach ( $hashed_passwords as $key => $item ) { 389 | if ( wp_check_password( $password, $item['password'], $user->ID ) ) { 390 | $item['last_used'] = time(); 391 | $item['last_ip'] = $_SERVER['REMOTE_ADDR']; 392 | $hashed_passwords[ $key ] = $item; 393 | update_user_meta( $user->ID, self::USERMETA_KEY_APPLICATION_PASSWORDS, $hashed_passwords ); 394 | 395 | do_action( 'application_password_did_authenticate', $user, $item ); 396 | 397 | return $user; 398 | } 399 | } 400 | 401 | // By default, return what we've been passed. 402 | return $input_user; 403 | } 404 | 405 | /** 406 | * Registers the hidden admin page to handle auth. 407 | */ 408 | public static function admin_menu() { 409 | add_submenu_page( null, __( 'Approve Application' ), null, 'exist', 'auth_app', array( __CLASS__, 'auth_app_page' ) ); 410 | } 411 | 412 | /** 413 | * Page for authorizing applications. 414 | */ 415 | public static function auth_app_page() { 416 | $app_name = ! empty( $_GET['app_name'] ) ? $_GET['app_name'] : ''; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification 417 | $success_url = ! empty( $_GET['success_url'] ) ? $_GET['success_url'] : null; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification 418 | $reject_url = ! empty( $_GET['reject_url'] ) ? $_GET['reject_url'] : $success_url; // phpcs:ignore WordPress.Security.NonceVerification.NoNonceVerification 419 | $user = wp_get_current_user(); 420 | 421 | wp_enqueue_script( 'auth-app', plugin_dir_url( __FILE__ ) . 'auth-app.js', array(), APPLICATION_PASSWORDS_VERSION, true ); 422 | wp_localize_script( 423 | 'auth-app', 424 | 'authApp', 425 | array( 426 | 'root' => esc_url_raw( rest_url() ), 427 | 'namespace' => '2fa/v1', 428 | 'nonce' => wp_create_nonce( 'wp_rest' ), 429 | 'user_id' => $user->ID, 430 | 'user_login' => $user->user_login, 431 | 'success' => $success_url, 432 | 'reject' => $reject_url ? $reject_url : admin_url(), 433 | 'strings' => array( 434 | // translators: application, password. 435 | 'new_pass' => esc_html_x( 'Your new password for %1$s is: %2$s', 'application, password' ), 436 | ), 437 | ) 438 | ); 439 | ?> 440 |
441 |

442 | 443 |
444 |

445 | 446 |

447 | ' . esc_html( $app_name ) . '' ); 450 | ?> 451 |

452 | 453 |

454 | 455 |
456 | 457 | 458 | 459 | 460 | 461 | 462 | 463 | 464 |

465 |
466 | ' . esc_html( 472 | add_query_arg( 473 | array( 474 | 'username' => $user->user_login, 475 | 'password' => '[------]', 476 | ), 477 | $success_url 478 | ) 479 | ) . '' 480 | ); 481 | } else { 482 | esc_html_e( 'You will be given a password to manually enter into the application in question.' ); 483 | } 484 | ?> 485 | 486 |

487 | 488 |

489 |
490 | ' . esc_html( 496 | add_query_arg( 497 | array( 498 | 'success' => 'false', 499 | ), 500 | $reject_url 501 | ) 502 | ) . '' 503 | ); 504 | } else { 505 | esc_html_e( 'You will be returned to the WordPress Dashboard, and we will never speak of this again.' ); 506 | } 507 | ?> 508 | 509 |

510 | 511 |
512 |
513 |
514 | ' . esc_html__( 'Your New Application Password:' ) . '

' . esc_html( self::chunk_password( $new_password ) ) . '

' ); 537 | } 538 | $redirect = add_query_arg( 539 | array( 540 | 'username' => wp_get_current_user()->user_login, 541 | 'password' => $new_password, 542 | ), 543 | $success_url 544 | ); 545 | } 546 | 547 | wp_redirect( $redirect ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect 548 | exit; 549 | 550 | } 551 | 552 | /** 553 | * Display the application password section in a users profile. 554 | * 555 | * This executes during the `show_user_security_settings` action. 556 | * 557 | * @since 0.1-dev 558 | * 559 | * @access public 560 | * @static 561 | * 562 | * @param WP_User $user WP_User object of the logged-in user. 563 | */ 564 | public static function show_user_profile( $user ) { 565 | wp_enqueue_style( 'application-passwords-css', plugin_dir_url( __FILE__ ) . 'application-passwords.css', array(), APPLICATION_PASSWORDS_VERSION ); 566 | wp_enqueue_script( 'application-passwords-js', plugin_dir_url( __FILE__ ) . 'application-passwords.js', array(), APPLICATION_PASSWORDS_VERSION, true ); 567 | wp_localize_script( 568 | 'application-passwords-js', 569 | 'appPass', 570 | array( 571 | 'root' => esc_url_raw( rest_url() ), 572 | 'namespace' => '2fa/v1', 573 | 'nonce' => wp_create_nonce( 'wp_rest' ), 574 | 'user_id' => $user->ID, 575 | 'text' => array( 576 | 'no_credentials' => __( 'Due to a potential server misconfiguration, it seems that HTTP Basic Authorization may not work for the REST API on this site: `Authorization` headers are not being sent to WordPress by the web server. You can learn more about this problem, and a possible solution, on our GitHub Wiki.' ), 577 | 'revoke_password' => esc_attr__( 'Are you sure you want to revoke this password? This action cannot be undone.' ), 578 | 'revoke_all_passwords' => esc_attr__( 'Are you sure you want to revoke all passwords? This action cannot be undone.' ), 579 | ), 580 | ) 581 | ); 582 | 583 | ?> 584 |
585 |

586 |

587 |
588 | 589 | 590 |
591 | 592 |
593 | items = array_reverse( self::get_user_application_passwords( $user->ID ) ); 597 | $application_passwords_list_table->prepare_items(); 598 | $application_passwords_list_table->display(); 599 | ?> 600 |
601 |
602 | 603 | 623 | 624 | 644 | 645 | 648 | $name, 669 | 'password' => $hashed_password, 670 | 'created' => time(), 671 | 'last_used' => null, 672 | 'last_ip' => null, 673 | ); 674 | 675 | $passwords = self::get_user_application_passwords( $user_id ); 676 | if ( ! $passwords ) { 677 | $passwords = array(); 678 | } 679 | 680 | $passwords[] = $new_item; 681 | self::set_user_application_passwords( $user_id, $passwords ); 682 | 683 | return array( $new_password, $new_item ); 684 | } 685 | 686 | /** 687 | * Delete a specified application password. 688 | * 689 | * @since 0.1-dev 690 | * 691 | * @access public 692 | * @static 693 | * 694 | * @see Application_Passwords::password_unique_slug() 695 | * 696 | * @param int $user_id User ID. 697 | * @param string $slug The generated slug of the password in question. 698 | * @return bool Whether the password was successfully found and deleted. 699 | */ 700 | public static function delete_application_password( $user_id, $slug ) { 701 | $passwords = self::get_user_application_passwords( $user_id ); 702 | 703 | foreach ( $passwords as $key => $item ) { 704 | if ( self::password_unique_slug( $item ) === $slug ) { 705 | unset( $passwords[ $key ] ); 706 | self::set_user_application_passwords( $user_id, $passwords ); 707 | return true; 708 | } 709 | } 710 | 711 | // Specified Application Password not found! 712 | return false; 713 | } 714 | 715 | /** 716 | * Deletes all application passwords for the given user. 717 | * 718 | * @since 0.1-dev 719 | * 720 | * @access public 721 | * @static 722 | * 723 | * @param int $user_id User ID. 724 | * @return int The number of passwords that were deleted. 725 | */ 726 | public static function delete_all_application_passwords( $user_id ) { 727 | $passwords = self::get_user_application_passwords( $user_id ); 728 | 729 | if ( is_array( $passwords ) ) { 730 | self::set_user_application_passwords( $user_id, array() ); 731 | return sizeof( $passwords ); 732 | } 733 | 734 | return 0; 735 | } 736 | 737 | /** 738 | * Generate a unique repeatable slug from the hashed password, name, and when it was created. 739 | * 740 | * @since 0.1-dev 741 | * 742 | * @access public 743 | * @static 744 | * 745 | * @param array $item The current item. 746 | * @return string 747 | */ 748 | public static function password_unique_slug( $item ) { 749 | $concat = $item['name'] . '|' . $item['password'] . '|' . $item['created']; 750 | $hash = md5( $concat ); 751 | return substr( $hash, 0, 12 ); 752 | } 753 | 754 | /** 755 | * Sanitize and then split a password into smaller chunks. 756 | * 757 | * @since 0.1-dev 758 | * 759 | * @access public 760 | * @static 761 | * 762 | * @param string $raw_password Users raw password. 763 | * @return string 764 | */ 765 | public static function chunk_password( $raw_password ) { 766 | $raw_password = preg_replace( '/[^a-z\d]/i', '', $raw_password ); 767 | return trim( chunk_split( $raw_password, 4, ' ' ) ); 768 | } 769 | 770 | /** 771 | * Get a users application passwords. 772 | * 773 | * @since 0.1-dev 774 | * 775 | * @access public 776 | * @static 777 | * 778 | * @param int $user_id User ID. 779 | * @return array 780 | */ 781 | public static function get_user_application_passwords( $user_id ) { 782 | $passwords = get_user_meta( $user_id, self::USERMETA_KEY_APPLICATION_PASSWORDS, true ); 783 | if ( ! is_array( $passwords ) ) { 784 | return array(); 785 | } 786 | return $passwords; 787 | } 788 | 789 | /** 790 | * Set a users application passwords. 791 | * 792 | * @since 0.1-dev 793 | * 794 | * @access public 795 | * @static 796 | * 797 | * @param int $user_id User ID. 798 | * @param array $passwords Application passwords. 799 | * 800 | * @return bool 801 | */ 802 | public static function set_user_application_passwords( $user_id, $passwords ) { 803 | return update_user_meta( $user_id, self::USERMETA_KEY_APPLICATION_PASSWORDS, $passwords ); 804 | } 805 | } 806 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "georgestephanis/application-passwords", 3 | "description": "Provide Application Passwords for WordPress core", 4 | "homepage": "https://github.com/WordPress/application-passwords", 5 | "keywords": [ 6 | "wordpress", 7 | "application Ppasswords", 8 | "api" 9 | ], 10 | "type": "wordpress-plugin", 11 | "license": "GPL-2.0-or-later", 12 | "authors": [ 13 | { 14 | "name": "George Stephanis", 15 | "email": "georgestephanis@automattic.com", 16 | "homepage": "https://github.com/georgestephanis" 17 | } 18 | ], 19 | "support": { 20 | "issues": "https://github.com/WordPress/application-passwords/issues" 21 | }, 22 | "config": { 23 | "platform": { 24 | "php": "5.6" 25 | } 26 | }, 27 | "require": { 28 | "composer/installers": "~1.0" 29 | }, 30 | "require-dev": { 31 | "xwp/wp-dev-lib": "^1.2", 32 | "wp-coding-standards/wpcs": "^2.1", 33 | "phpunit/phpunit": "^4.8", 34 | "wpsh/local": "^0.2.3" 35 | }, 36 | "scripts": { 37 | "lint-staged": [ 38 | "DEV_LIB_SKIP=phpunit DIFF_BASE=master SYNC_README_MD=0 ./vendor/xwp/wp-dev-lib/scripts/pre-commit" 39 | ], 40 | "lint-head": [ 41 | "DEV_LIB_SKIP=phpunit DIFF_BASE=master DIFF_HEAD=HEAD SYNC_README_MD=0 ./vendor/xwp/wp-dev-lib/scripts/pre-commit" 42 | ], 43 | "lint": [ 44 | "phpcs ." 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "d33bf18b29d80d32d40525d0f6e5c2e5", 8 | "packages": [ 9 | { 10 | "name": "composer/installers", 11 | "version": "v1.7.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/composer/installers.git", 15 | "reference": "141b272484481432cda342727a427dc1e206bfa0" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/composer/installers/zipball/141b272484481432cda342727a427dc1e206bfa0", 20 | "reference": "141b272484481432cda342727a427dc1e206bfa0", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "composer-plugin-api": "^1.0" 25 | }, 26 | "replace": { 27 | "roundcube/plugin-installer": "*", 28 | "shama/baton": "*" 29 | }, 30 | "require-dev": { 31 | "composer/composer": "1.0.*@dev", 32 | "phpunit/phpunit": "^4.8.36" 33 | }, 34 | "type": "composer-plugin", 35 | "extra": { 36 | "class": "Composer\\Installers\\Plugin", 37 | "branch-alias": { 38 | "dev-master": "1.0-dev" 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "Composer\\Installers\\": "src/Composer/Installers" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Kyle Robinson Young", 53 | "email": "kyle@dontkry.com", 54 | "homepage": "https://github.com/shama" 55 | } 56 | ], 57 | "description": "A multi-framework Composer library installer", 58 | "homepage": "https://composer.github.io/installers/", 59 | "keywords": [ 60 | "Craft", 61 | "Dolibarr", 62 | "Eliasis", 63 | "Hurad", 64 | "ImageCMS", 65 | "Kanboard", 66 | "Lan Management System", 67 | "MODX Evo", 68 | "Mautic", 69 | "Maya", 70 | "OXID", 71 | "Plentymarkets", 72 | "Porto", 73 | "RadPHP", 74 | "SMF", 75 | "Thelia", 76 | "Whmcs", 77 | "WolfCMS", 78 | "agl", 79 | "aimeos", 80 | "annotatecms", 81 | "attogram", 82 | "bitrix", 83 | "cakephp", 84 | "chef", 85 | "cockpit", 86 | "codeigniter", 87 | "concrete5", 88 | "croogo", 89 | "dokuwiki", 90 | "drupal", 91 | "eZ Platform", 92 | "elgg", 93 | "expressionengine", 94 | "fuelphp", 95 | "grav", 96 | "installer", 97 | "itop", 98 | "joomla", 99 | "known", 100 | "kohana", 101 | "laravel", 102 | "lavalite", 103 | "lithium", 104 | "magento", 105 | "majima", 106 | "mako", 107 | "mediawiki", 108 | "modulework", 109 | "modx", 110 | "moodle", 111 | "osclass", 112 | "phpbb", 113 | "piwik", 114 | "ppi", 115 | "puppet", 116 | "pxcms", 117 | "reindex", 118 | "roundcube", 119 | "shopware", 120 | "silverstripe", 121 | "sydes", 122 | "symfony", 123 | "typo3", 124 | "wordpress", 125 | "yawik", 126 | "zend", 127 | "zikula" 128 | ], 129 | "time": "2019-08-12T15:00:31+00:00" 130 | } 131 | ], 132 | "packages-dev": [ 133 | { 134 | "name": "doctrine/instantiator", 135 | "version": "1.0.5", 136 | "source": { 137 | "type": "git", 138 | "url": "https://github.com/doctrine/instantiator.git", 139 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 140 | }, 141 | "dist": { 142 | "type": "zip", 143 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 144 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 145 | "shasum": "" 146 | }, 147 | "require": { 148 | "php": ">=5.3,<8.0-DEV" 149 | }, 150 | "require-dev": { 151 | "athletic/athletic": "~0.1.8", 152 | "ext-pdo": "*", 153 | "ext-phar": "*", 154 | "phpunit/phpunit": "~4.0", 155 | "squizlabs/php_codesniffer": "~2.0" 156 | }, 157 | "type": "library", 158 | "extra": { 159 | "branch-alias": { 160 | "dev-master": "1.0.x-dev" 161 | } 162 | }, 163 | "autoload": { 164 | "psr-4": { 165 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 166 | } 167 | }, 168 | "notification-url": "https://packagist.org/downloads/", 169 | "license": [ 170 | "MIT" 171 | ], 172 | "authors": [ 173 | { 174 | "name": "Marco Pivetta", 175 | "email": "ocramius@gmail.com", 176 | "homepage": "http://ocramius.github.com/" 177 | } 178 | ], 179 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 180 | "homepage": "https://github.com/doctrine/instantiator", 181 | "keywords": [ 182 | "constructor", 183 | "instantiate" 184 | ], 185 | "time": "2015-06-14T21:17:01+00:00" 186 | }, 187 | { 188 | "name": "phpdocumentor/reflection-common", 189 | "version": "1.0.1", 190 | "source": { 191 | "type": "git", 192 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 193 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6" 194 | }, 195 | "dist": { 196 | "type": "zip", 197 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 198 | "reference": "21bdeb5f65d7ebf9f43b1b25d404f87deab5bfb6", 199 | "shasum": "" 200 | }, 201 | "require": { 202 | "php": ">=5.5" 203 | }, 204 | "require-dev": { 205 | "phpunit/phpunit": "^4.6" 206 | }, 207 | "type": "library", 208 | "extra": { 209 | "branch-alias": { 210 | "dev-master": "1.0.x-dev" 211 | } 212 | }, 213 | "autoload": { 214 | "psr-4": { 215 | "phpDocumentor\\Reflection\\": [ 216 | "src" 217 | ] 218 | } 219 | }, 220 | "notification-url": "https://packagist.org/downloads/", 221 | "license": [ 222 | "MIT" 223 | ], 224 | "authors": [ 225 | { 226 | "name": "Jaap van Otterdijk", 227 | "email": "opensource@ijaap.nl" 228 | } 229 | ], 230 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 231 | "homepage": "http://www.phpdoc.org", 232 | "keywords": [ 233 | "FQSEN", 234 | "phpDocumentor", 235 | "phpdoc", 236 | "reflection", 237 | "static analysis" 238 | ], 239 | "time": "2017-09-11T18:02:19+00:00" 240 | }, 241 | { 242 | "name": "phpdocumentor/reflection-docblock", 243 | "version": "3.3.2", 244 | "source": { 245 | "type": "git", 246 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 247 | "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2" 248 | }, 249 | "dist": { 250 | "type": "zip", 251 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/bf329f6c1aadea3299f08ee804682b7c45b326a2", 252 | "reference": "bf329f6c1aadea3299f08ee804682b7c45b326a2", 253 | "shasum": "" 254 | }, 255 | "require": { 256 | "php": "^5.6 || ^7.0", 257 | "phpdocumentor/reflection-common": "^1.0.0", 258 | "phpdocumentor/type-resolver": "^0.4.0", 259 | "webmozart/assert": "^1.0" 260 | }, 261 | "require-dev": { 262 | "mockery/mockery": "^0.9.4", 263 | "phpunit/phpunit": "^4.4" 264 | }, 265 | "type": "library", 266 | "autoload": { 267 | "psr-4": { 268 | "phpDocumentor\\Reflection\\": [ 269 | "src/" 270 | ] 271 | } 272 | }, 273 | "notification-url": "https://packagist.org/downloads/", 274 | "license": [ 275 | "MIT" 276 | ], 277 | "authors": [ 278 | { 279 | "name": "Mike van Riel", 280 | "email": "me@mikevanriel.com" 281 | } 282 | ], 283 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 284 | "time": "2017-11-10T14:09:06+00:00" 285 | }, 286 | { 287 | "name": "phpdocumentor/type-resolver", 288 | "version": "0.4.0", 289 | "source": { 290 | "type": "git", 291 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 292 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7" 293 | }, 294 | "dist": { 295 | "type": "zip", 296 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7", 297 | "reference": "9c977708995954784726e25d0cd1dddf4e65b0f7", 298 | "shasum": "" 299 | }, 300 | "require": { 301 | "php": "^5.5 || ^7.0", 302 | "phpdocumentor/reflection-common": "^1.0" 303 | }, 304 | "require-dev": { 305 | "mockery/mockery": "^0.9.4", 306 | "phpunit/phpunit": "^5.2||^4.8.24" 307 | }, 308 | "type": "library", 309 | "extra": { 310 | "branch-alias": { 311 | "dev-master": "1.0.x-dev" 312 | } 313 | }, 314 | "autoload": { 315 | "psr-4": { 316 | "phpDocumentor\\Reflection\\": [ 317 | "src/" 318 | ] 319 | } 320 | }, 321 | "notification-url": "https://packagist.org/downloads/", 322 | "license": [ 323 | "MIT" 324 | ], 325 | "authors": [ 326 | { 327 | "name": "Mike van Riel", 328 | "email": "me@mikevanriel.com" 329 | } 330 | ], 331 | "time": "2017-07-14T14:27:02+00:00" 332 | }, 333 | { 334 | "name": "phpspec/prophecy", 335 | "version": "1.10.1", 336 | "source": { 337 | "type": "git", 338 | "url": "https://github.com/phpspec/prophecy.git", 339 | "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc" 340 | }, 341 | "dist": { 342 | "type": "zip", 343 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 344 | "reference": "cbe1df668b3fe136bcc909126a0f529a78d4cbbc", 345 | "shasum": "" 346 | }, 347 | "require": { 348 | "doctrine/instantiator": "^1.0.2", 349 | "php": "^5.3|^7.0", 350 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2|^4.0|^5.0", 351 | "sebastian/comparator": "^1.2.3|^2.0|^3.0", 352 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 353 | }, 354 | "require-dev": { 355 | "phpspec/phpspec": "^2.5 || ^3.2", 356 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.5 || ^7.1" 357 | }, 358 | "type": "library", 359 | "extra": { 360 | "branch-alias": { 361 | "dev-master": "1.10.x-dev" 362 | } 363 | }, 364 | "autoload": { 365 | "psr-4": { 366 | "Prophecy\\": "src/Prophecy" 367 | } 368 | }, 369 | "notification-url": "https://packagist.org/downloads/", 370 | "license": [ 371 | "MIT" 372 | ], 373 | "authors": [ 374 | { 375 | "name": "Konstantin Kudryashov", 376 | "email": "ever.zet@gmail.com", 377 | "homepage": "http://everzet.com" 378 | }, 379 | { 380 | "name": "Marcello Duarte", 381 | "email": "marcello.duarte@gmail.com" 382 | } 383 | ], 384 | "description": "Highly opinionated mocking framework for PHP 5.3+", 385 | "homepage": "https://github.com/phpspec/prophecy", 386 | "keywords": [ 387 | "Double", 388 | "Dummy", 389 | "fake", 390 | "mock", 391 | "spy", 392 | "stub" 393 | ], 394 | "time": "2019-12-22T21:05:45+00:00" 395 | }, 396 | { 397 | "name": "phpunit/php-code-coverage", 398 | "version": "2.2.4", 399 | "source": { 400 | "type": "git", 401 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 402 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979" 403 | }, 404 | "dist": { 405 | "type": "zip", 406 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/eabf68b476ac7d0f73793aada060f1c1a9bf8979", 407 | "reference": "eabf68b476ac7d0f73793aada060f1c1a9bf8979", 408 | "shasum": "" 409 | }, 410 | "require": { 411 | "php": ">=5.3.3", 412 | "phpunit/php-file-iterator": "~1.3", 413 | "phpunit/php-text-template": "~1.2", 414 | "phpunit/php-token-stream": "~1.3", 415 | "sebastian/environment": "^1.3.2", 416 | "sebastian/version": "~1.0" 417 | }, 418 | "require-dev": { 419 | "ext-xdebug": ">=2.1.4", 420 | "phpunit/phpunit": "~4" 421 | }, 422 | "suggest": { 423 | "ext-dom": "*", 424 | "ext-xdebug": ">=2.2.1", 425 | "ext-xmlwriter": "*" 426 | }, 427 | "type": "library", 428 | "extra": { 429 | "branch-alias": { 430 | "dev-master": "2.2.x-dev" 431 | } 432 | }, 433 | "autoload": { 434 | "classmap": [ 435 | "src/" 436 | ] 437 | }, 438 | "notification-url": "https://packagist.org/downloads/", 439 | "license": [ 440 | "BSD-3-Clause" 441 | ], 442 | "authors": [ 443 | { 444 | "name": "Sebastian Bergmann", 445 | "email": "sb@sebastian-bergmann.de", 446 | "role": "lead" 447 | } 448 | ], 449 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 450 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 451 | "keywords": [ 452 | "coverage", 453 | "testing", 454 | "xunit" 455 | ], 456 | "time": "2015-10-06T15:47:00+00:00" 457 | }, 458 | { 459 | "name": "phpunit/php-file-iterator", 460 | "version": "1.4.5", 461 | "source": { 462 | "type": "git", 463 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 464 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4" 465 | }, 466 | "dist": { 467 | "type": "zip", 468 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/730b01bc3e867237eaac355e06a36b85dd93a8b4", 469 | "reference": "730b01bc3e867237eaac355e06a36b85dd93a8b4", 470 | "shasum": "" 471 | }, 472 | "require": { 473 | "php": ">=5.3.3" 474 | }, 475 | "type": "library", 476 | "extra": { 477 | "branch-alias": { 478 | "dev-master": "1.4.x-dev" 479 | } 480 | }, 481 | "autoload": { 482 | "classmap": [ 483 | "src/" 484 | ] 485 | }, 486 | "notification-url": "https://packagist.org/downloads/", 487 | "license": [ 488 | "BSD-3-Clause" 489 | ], 490 | "authors": [ 491 | { 492 | "name": "Sebastian Bergmann", 493 | "email": "sb@sebastian-bergmann.de", 494 | "role": "lead" 495 | } 496 | ], 497 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 498 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 499 | "keywords": [ 500 | "filesystem", 501 | "iterator" 502 | ], 503 | "time": "2017-11-27T13:52:08+00:00" 504 | }, 505 | { 506 | "name": "phpunit/php-text-template", 507 | "version": "1.2.1", 508 | "source": { 509 | "type": "git", 510 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 511 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 512 | }, 513 | "dist": { 514 | "type": "zip", 515 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 516 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 517 | "shasum": "" 518 | }, 519 | "require": { 520 | "php": ">=5.3.3" 521 | }, 522 | "type": "library", 523 | "autoload": { 524 | "classmap": [ 525 | "src/" 526 | ] 527 | }, 528 | "notification-url": "https://packagist.org/downloads/", 529 | "license": [ 530 | "BSD-3-Clause" 531 | ], 532 | "authors": [ 533 | { 534 | "name": "Sebastian Bergmann", 535 | "email": "sebastian@phpunit.de", 536 | "role": "lead" 537 | } 538 | ], 539 | "description": "Simple template engine.", 540 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 541 | "keywords": [ 542 | "template" 543 | ], 544 | "time": "2015-06-21T13:50:34+00:00" 545 | }, 546 | { 547 | "name": "phpunit/php-timer", 548 | "version": "1.0.9", 549 | "source": { 550 | "type": "git", 551 | "url": "https://github.com/sebastianbergmann/php-timer.git", 552 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 553 | }, 554 | "dist": { 555 | "type": "zip", 556 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 557 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 558 | "shasum": "" 559 | }, 560 | "require": { 561 | "php": "^5.3.3 || ^7.0" 562 | }, 563 | "require-dev": { 564 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 565 | }, 566 | "type": "library", 567 | "extra": { 568 | "branch-alias": { 569 | "dev-master": "1.0-dev" 570 | } 571 | }, 572 | "autoload": { 573 | "classmap": [ 574 | "src/" 575 | ] 576 | }, 577 | "notification-url": "https://packagist.org/downloads/", 578 | "license": [ 579 | "BSD-3-Clause" 580 | ], 581 | "authors": [ 582 | { 583 | "name": "Sebastian Bergmann", 584 | "email": "sb@sebastian-bergmann.de", 585 | "role": "lead" 586 | } 587 | ], 588 | "description": "Utility class for timing", 589 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 590 | "keywords": [ 591 | "timer" 592 | ], 593 | "time": "2017-02-26T11:10:40+00:00" 594 | }, 595 | { 596 | "name": "phpunit/php-token-stream", 597 | "version": "1.4.12", 598 | "source": { 599 | "type": "git", 600 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 601 | "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16" 602 | }, 603 | "dist": { 604 | "type": "zip", 605 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/1ce90ba27c42e4e44e6d8458241466380b51fa16", 606 | "reference": "1ce90ba27c42e4e44e6d8458241466380b51fa16", 607 | "shasum": "" 608 | }, 609 | "require": { 610 | "ext-tokenizer": "*", 611 | "php": ">=5.3.3" 612 | }, 613 | "require-dev": { 614 | "phpunit/phpunit": "~4.2" 615 | }, 616 | "type": "library", 617 | "extra": { 618 | "branch-alias": { 619 | "dev-master": "1.4-dev" 620 | } 621 | }, 622 | "autoload": { 623 | "classmap": [ 624 | "src/" 625 | ] 626 | }, 627 | "notification-url": "https://packagist.org/downloads/", 628 | "license": [ 629 | "BSD-3-Clause" 630 | ], 631 | "authors": [ 632 | { 633 | "name": "Sebastian Bergmann", 634 | "email": "sebastian@phpunit.de" 635 | } 636 | ], 637 | "description": "Wrapper around PHP's tokenizer extension.", 638 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 639 | "keywords": [ 640 | "tokenizer" 641 | ], 642 | "time": "2017-12-04T08:55:13+00:00" 643 | }, 644 | { 645 | "name": "phpunit/phpunit", 646 | "version": "4.8.36", 647 | "source": { 648 | "type": "git", 649 | "url": "https://github.com/sebastianbergmann/phpunit.git", 650 | "reference": "46023de9a91eec7dfb06cc56cb4e260017298517" 651 | }, 652 | "dist": { 653 | "type": "zip", 654 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/46023de9a91eec7dfb06cc56cb4e260017298517", 655 | "reference": "46023de9a91eec7dfb06cc56cb4e260017298517", 656 | "shasum": "" 657 | }, 658 | "require": { 659 | "ext-dom": "*", 660 | "ext-json": "*", 661 | "ext-pcre": "*", 662 | "ext-reflection": "*", 663 | "ext-spl": "*", 664 | "php": ">=5.3.3", 665 | "phpspec/prophecy": "^1.3.1", 666 | "phpunit/php-code-coverage": "~2.1", 667 | "phpunit/php-file-iterator": "~1.4", 668 | "phpunit/php-text-template": "~1.2", 669 | "phpunit/php-timer": "^1.0.6", 670 | "phpunit/phpunit-mock-objects": "~2.3", 671 | "sebastian/comparator": "~1.2.2", 672 | "sebastian/diff": "~1.2", 673 | "sebastian/environment": "~1.3", 674 | "sebastian/exporter": "~1.2", 675 | "sebastian/global-state": "~1.0", 676 | "sebastian/version": "~1.0", 677 | "symfony/yaml": "~2.1|~3.0" 678 | }, 679 | "suggest": { 680 | "phpunit/php-invoker": "~1.1" 681 | }, 682 | "bin": [ 683 | "phpunit" 684 | ], 685 | "type": "library", 686 | "extra": { 687 | "branch-alias": { 688 | "dev-master": "4.8.x-dev" 689 | } 690 | }, 691 | "autoload": { 692 | "classmap": [ 693 | "src/" 694 | ] 695 | }, 696 | "notification-url": "https://packagist.org/downloads/", 697 | "license": [ 698 | "BSD-3-Clause" 699 | ], 700 | "authors": [ 701 | { 702 | "name": "Sebastian Bergmann", 703 | "email": "sebastian@phpunit.de", 704 | "role": "lead" 705 | } 706 | ], 707 | "description": "The PHP Unit Testing framework.", 708 | "homepage": "https://phpunit.de/", 709 | "keywords": [ 710 | "phpunit", 711 | "testing", 712 | "xunit" 713 | ], 714 | "time": "2017-06-21T08:07:12+00:00" 715 | }, 716 | { 717 | "name": "phpunit/phpunit-mock-objects", 718 | "version": "2.3.8", 719 | "source": { 720 | "type": "git", 721 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 722 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983" 723 | }, 724 | "dist": { 725 | "type": "zip", 726 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/ac8e7a3db35738d56ee9a76e78a4e03d97628983", 727 | "reference": "ac8e7a3db35738d56ee9a76e78a4e03d97628983", 728 | "shasum": "" 729 | }, 730 | "require": { 731 | "doctrine/instantiator": "^1.0.2", 732 | "php": ">=5.3.3", 733 | "phpunit/php-text-template": "~1.2", 734 | "sebastian/exporter": "~1.2" 735 | }, 736 | "require-dev": { 737 | "phpunit/phpunit": "~4.4" 738 | }, 739 | "suggest": { 740 | "ext-soap": "*" 741 | }, 742 | "type": "library", 743 | "extra": { 744 | "branch-alias": { 745 | "dev-master": "2.3.x-dev" 746 | } 747 | }, 748 | "autoload": { 749 | "classmap": [ 750 | "src/" 751 | ] 752 | }, 753 | "notification-url": "https://packagist.org/downloads/", 754 | "license": [ 755 | "BSD-3-Clause" 756 | ], 757 | "authors": [ 758 | { 759 | "name": "Sebastian Bergmann", 760 | "email": "sb@sebastian-bergmann.de", 761 | "role": "lead" 762 | } 763 | ], 764 | "description": "Mock Object library for PHPUnit", 765 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 766 | "keywords": [ 767 | "mock", 768 | "xunit" 769 | ], 770 | "abandoned": true, 771 | "time": "2015-10-02T06:51:40+00:00" 772 | }, 773 | { 774 | "name": "sebastian/comparator", 775 | "version": "1.2.4", 776 | "source": { 777 | "type": "git", 778 | "url": "https://github.com/sebastianbergmann/comparator.git", 779 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" 780 | }, 781 | "dist": { 782 | "type": "zip", 783 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 784 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 785 | "shasum": "" 786 | }, 787 | "require": { 788 | "php": ">=5.3.3", 789 | "sebastian/diff": "~1.2", 790 | "sebastian/exporter": "~1.2 || ~2.0" 791 | }, 792 | "require-dev": { 793 | "phpunit/phpunit": "~4.4" 794 | }, 795 | "type": "library", 796 | "extra": { 797 | "branch-alias": { 798 | "dev-master": "1.2.x-dev" 799 | } 800 | }, 801 | "autoload": { 802 | "classmap": [ 803 | "src/" 804 | ] 805 | }, 806 | "notification-url": "https://packagist.org/downloads/", 807 | "license": [ 808 | "BSD-3-Clause" 809 | ], 810 | "authors": [ 811 | { 812 | "name": "Jeff Welch", 813 | "email": "whatthejeff@gmail.com" 814 | }, 815 | { 816 | "name": "Volker Dusch", 817 | "email": "github@wallbash.com" 818 | }, 819 | { 820 | "name": "Bernhard Schussek", 821 | "email": "bschussek@2bepublished.at" 822 | }, 823 | { 824 | "name": "Sebastian Bergmann", 825 | "email": "sebastian@phpunit.de" 826 | } 827 | ], 828 | "description": "Provides the functionality to compare PHP values for equality", 829 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 830 | "keywords": [ 831 | "comparator", 832 | "compare", 833 | "equality" 834 | ], 835 | "time": "2017-01-29T09:50:25+00:00" 836 | }, 837 | { 838 | "name": "sebastian/diff", 839 | "version": "1.4.3", 840 | "source": { 841 | "type": "git", 842 | "url": "https://github.com/sebastianbergmann/diff.git", 843 | "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" 844 | }, 845 | "dist": { 846 | "type": "zip", 847 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", 848 | "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", 849 | "shasum": "" 850 | }, 851 | "require": { 852 | "php": "^5.3.3 || ^7.0" 853 | }, 854 | "require-dev": { 855 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 856 | }, 857 | "type": "library", 858 | "extra": { 859 | "branch-alias": { 860 | "dev-master": "1.4-dev" 861 | } 862 | }, 863 | "autoload": { 864 | "classmap": [ 865 | "src/" 866 | ] 867 | }, 868 | "notification-url": "https://packagist.org/downloads/", 869 | "license": [ 870 | "BSD-3-Clause" 871 | ], 872 | "authors": [ 873 | { 874 | "name": "Kore Nordmann", 875 | "email": "mail@kore-nordmann.de" 876 | }, 877 | { 878 | "name": "Sebastian Bergmann", 879 | "email": "sebastian@phpunit.de" 880 | } 881 | ], 882 | "description": "Diff implementation", 883 | "homepage": "https://github.com/sebastianbergmann/diff", 884 | "keywords": [ 885 | "diff" 886 | ], 887 | "time": "2017-05-22T07:24:03+00:00" 888 | }, 889 | { 890 | "name": "sebastian/environment", 891 | "version": "1.3.8", 892 | "source": { 893 | "type": "git", 894 | "url": "https://github.com/sebastianbergmann/environment.git", 895 | "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea" 896 | }, 897 | "dist": { 898 | "type": "zip", 899 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/be2c607e43ce4c89ecd60e75c6a85c126e754aea", 900 | "reference": "be2c607e43ce4c89ecd60e75c6a85c126e754aea", 901 | "shasum": "" 902 | }, 903 | "require": { 904 | "php": "^5.3.3 || ^7.0" 905 | }, 906 | "require-dev": { 907 | "phpunit/phpunit": "^4.8 || ^5.0" 908 | }, 909 | "type": "library", 910 | "extra": { 911 | "branch-alias": { 912 | "dev-master": "1.3.x-dev" 913 | } 914 | }, 915 | "autoload": { 916 | "classmap": [ 917 | "src/" 918 | ] 919 | }, 920 | "notification-url": "https://packagist.org/downloads/", 921 | "license": [ 922 | "BSD-3-Clause" 923 | ], 924 | "authors": [ 925 | { 926 | "name": "Sebastian Bergmann", 927 | "email": "sebastian@phpunit.de" 928 | } 929 | ], 930 | "description": "Provides functionality to handle HHVM/PHP environments", 931 | "homepage": "http://www.github.com/sebastianbergmann/environment", 932 | "keywords": [ 933 | "Xdebug", 934 | "environment", 935 | "hhvm" 936 | ], 937 | "time": "2016-08-18T05:49:44+00:00" 938 | }, 939 | { 940 | "name": "sebastian/exporter", 941 | "version": "1.2.2", 942 | "source": { 943 | "type": "git", 944 | "url": "https://github.com/sebastianbergmann/exporter.git", 945 | "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4" 946 | }, 947 | "dist": { 948 | "type": "zip", 949 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/42c4c2eec485ee3e159ec9884f95b431287edde4", 950 | "reference": "42c4c2eec485ee3e159ec9884f95b431287edde4", 951 | "shasum": "" 952 | }, 953 | "require": { 954 | "php": ">=5.3.3", 955 | "sebastian/recursion-context": "~1.0" 956 | }, 957 | "require-dev": { 958 | "ext-mbstring": "*", 959 | "phpunit/phpunit": "~4.4" 960 | }, 961 | "type": "library", 962 | "extra": { 963 | "branch-alias": { 964 | "dev-master": "1.3.x-dev" 965 | } 966 | }, 967 | "autoload": { 968 | "classmap": [ 969 | "src/" 970 | ] 971 | }, 972 | "notification-url": "https://packagist.org/downloads/", 973 | "license": [ 974 | "BSD-3-Clause" 975 | ], 976 | "authors": [ 977 | { 978 | "name": "Jeff Welch", 979 | "email": "whatthejeff@gmail.com" 980 | }, 981 | { 982 | "name": "Volker Dusch", 983 | "email": "github@wallbash.com" 984 | }, 985 | { 986 | "name": "Bernhard Schussek", 987 | "email": "bschussek@2bepublished.at" 988 | }, 989 | { 990 | "name": "Sebastian Bergmann", 991 | "email": "sebastian@phpunit.de" 992 | }, 993 | { 994 | "name": "Adam Harvey", 995 | "email": "aharvey@php.net" 996 | } 997 | ], 998 | "description": "Provides the functionality to export PHP variables for visualization", 999 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1000 | "keywords": [ 1001 | "export", 1002 | "exporter" 1003 | ], 1004 | "time": "2016-06-17T09:04:28+00:00" 1005 | }, 1006 | { 1007 | "name": "sebastian/global-state", 1008 | "version": "1.1.1", 1009 | "source": { 1010 | "type": "git", 1011 | "url": "https://github.com/sebastianbergmann/global-state.git", 1012 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 1013 | }, 1014 | "dist": { 1015 | "type": "zip", 1016 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 1017 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 1018 | "shasum": "" 1019 | }, 1020 | "require": { 1021 | "php": ">=5.3.3" 1022 | }, 1023 | "require-dev": { 1024 | "phpunit/phpunit": "~4.2" 1025 | }, 1026 | "suggest": { 1027 | "ext-uopz": "*" 1028 | }, 1029 | "type": "library", 1030 | "extra": { 1031 | "branch-alias": { 1032 | "dev-master": "1.0-dev" 1033 | } 1034 | }, 1035 | "autoload": { 1036 | "classmap": [ 1037 | "src/" 1038 | ] 1039 | }, 1040 | "notification-url": "https://packagist.org/downloads/", 1041 | "license": [ 1042 | "BSD-3-Clause" 1043 | ], 1044 | "authors": [ 1045 | { 1046 | "name": "Sebastian Bergmann", 1047 | "email": "sebastian@phpunit.de" 1048 | } 1049 | ], 1050 | "description": "Snapshotting of global state", 1051 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1052 | "keywords": [ 1053 | "global state" 1054 | ], 1055 | "time": "2015-10-12T03:26:01+00:00" 1056 | }, 1057 | { 1058 | "name": "sebastian/recursion-context", 1059 | "version": "1.0.5", 1060 | "source": { 1061 | "type": "git", 1062 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1063 | "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7" 1064 | }, 1065 | "dist": { 1066 | "type": "zip", 1067 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/b19cc3298482a335a95f3016d2f8a6950f0fbcd7", 1068 | "reference": "b19cc3298482a335a95f3016d2f8a6950f0fbcd7", 1069 | "shasum": "" 1070 | }, 1071 | "require": { 1072 | "php": ">=5.3.3" 1073 | }, 1074 | "require-dev": { 1075 | "phpunit/phpunit": "~4.4" 1076 | }, 1077 | "type": "library", 1078 | "extra": { 1079 | "branch-alias": { 1080 | "dev-master": "1.0.x-dev" 1081 | } 1082 | }, 1083 | "autoload": { 1084 | "classmap": [ 1085 | "src/" 1086 | ] 1087 | }, 1088 | "notification-url": "https://packagist.org/downloads/", 1089 | "license": [ 1090 | "BSD-3-Clause" 1091 | ], 1092 | "authors": [ 1093 | { 1094 | "name": "Jeff Welch", 1095 | "email": "whatthejeff@gmail.com" 1096 | }, 1097 | { 1098 | "name": "Sebastian Bergmann", 1099 | "email": "sebastian@phpunit.de" 1100 | }, 1101 | { 1102 | "name": "Adam Harvey", 1103 | "email": "aharvey@php.net" 1104 | } 1105 | ], 1106 | "description": "Provides functionality to recursively process PHP variables", 1107 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1108 | "time": "2016-10-03T07:41:43+00:00" 1109 | }, 1110 | { 1111 | "name": "sebastian/version", 1112 | "version": "1.0.6", 1113 | "source": { 1114 | "type": "git", 1115 | "url": "https://github.com/sebastianbergmann/version.git", 1116 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6" 1117 | }, 1118 | "dist": { 1119 | "type": "zip", 1120 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1121 | "reference": "58b3a85e7999757d6ad81c787a1fbf5ff6c628c6", 1122 | "shasum": "" 1123 | }, 1124 | "type": "library", 1125 | "autoload": { 1126 | "classmap": [ 1127 | "src/" 1128 | ] 1129 | }, 1130 | "notification-url": "https://packagist.org/downloads/", 1131 | "license": [ 1132 | "BSD-3-Clause" 1133 | ], 1134 | "authors": [ 1135 | { 1136 | "name": "Sebastian Bergmann", 1137 | "email": "sebastian@phpunit.de", 1138 | "role": "lead" 1139 | } 1140 | ], 1141 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1142 | "homepage": "https://github.com/sebastianbergmann/version", 1143 | "time": "2015-06-21T13:59:46+00:00" 1144 | }, 1145 | { 1146 | "name": "squizlabs/php_codesniffer", 1147 | "version": "3.5.3", 1148 | "source": { 1149 | "type": "git", 1150 | "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", 1151 | "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb" 1152 | }, 1153 | "dist": { 1154 | "type": "zip", 1155 | "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 1156 | "reference": "557a1fc7ac702c66b0bbfe16ab3d55839ef724cb", 1157 | "shasum": "" 1158 | }, 1159 | "require": { 1160 | "ext-simplexml": "*", 1161 | "ext-tokenizer": "*", 1162 | "ext-xmlwriter": "*", 1163 | "php": ">=5.4.0" 1164 | }, 1165 | "require-dev": { 1166 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1167 | }, 1168 | "bin": [ 1169 | "bin/phpcs", 1170 | "bin/phpcbf" 1171 | ], 1172 | "type": "library", 1173 | "extra": { 1174 | "branch-alias": { 1175 | "dev-master": "3.x-dev" 1176 | } 1177 | }, 1178 | "notification-url": "https://packagist.org/downloads/", 1179 | "license": [ 1180 | "BSD-3-Clause" 1181 | ], 1182 | "authors": [ 1183 | { 1184 | "name": "Greg Sherwood", 1185 | "role": "lead" 1186 | } 1187 | ], 1188 | "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", 1189 | "homepage": "https://github.com/squizlabs/PHP_CodeSniffer", 1190 | "keywords": [ 1191 | "phpcs", 1192 | "standards" 1193 | ], 1194 | "time": "2019-12-04T04:46:47+00:00" 1195 | }, 1196 | { 1197 | "name": "symfony/polyfill-ctype", 1198 | "version": "v1.13.1", 1199 | "source": { 1200 | "type": "git", 1201 | "url": "https://github.com/symfony/polyfill-ctype.git", 1202 | "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3" 1203 | }, 1204 | "dist": { 1205 | "type": "zip", 1206 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 1207 | "reference": "f8f0b461be3385e56d6de3dbb5a0df24c0c275e3", 1208 | "shasum": "" 1209 | }, 1210 | "require": { 1211 | "php": ">=5.3.3" 1212 | }, 1213 | "suggest": { 1214 | "ext-ctype": "For best performance" 1215 | }, 1216 | "type": "library", 1217 | "extra": { 1218 | "branch-alias": { 1219 | "dev-master": "1.13-dev" 1220 | } 1221 | }, 1222 | "autoload": { 1223 | "psr-4": { 1224 | "Symfony\\Polyfill\\Ctype\\": "" 1225 | }, 1226 | "files": [ 1227 | "bootstrap.php" 1228 | ] 1229 | }, 1230 | "notification-url": "https://packagist.org/downloads/", 1231 | "license": [ 1232 | "MIT" 1233 | ], 1234 | "authors": [ 1235 | { 1236 | "name": "Gert de Pagter", 1237 | "email": "BackEndTea@gmail.com" 1238 | }, 1239 | { 1240 | "name": "Symfony Community", 1241 | "homepage": "https://symfony.com/contributors" 1242 | } 1243 | ], 1244 | "description": "Symfony polyfill for ctype functions", 1245 | "homepage": "https://symfony.com", 1246 | "keywords": [ 1247 | "compatibility", 1248 | "ctype", 1249 | "polyfill", 1250 | "portable" 1251 | ], 1252 | "time": "2019-11-27T13:56:44+00:00" 1253 | }, 1254 | { 1255 | "name": "symfony/yaml", 1256 | "version": "v3.4.36", 1257 | "source": { 1258 | "type": "git", 1259 | "url": "https://github.com/symfony/yaml.git", 1260 | "reference": "dab657db15207879217fc81df4f875947bf68804" 1261 | }, 1262 | "dist": { 1263 | "type": "zip", 1264 | "url": "https://api.github.com/repos/symfony/yaml/zipball/dab657db15207879217fc81df4f875947bf68804", 1265 | "reference": "dab657db15207879217fc81df4f875947bf68804", 1266 | "shasum": "" 1267 | }, 1268 | "require": { 1269 | "php": "^5.5.9|>=7.0.8", 1270 | "symfony/polyfill-ctype": "~1.8" 1271 | }, 1272 | "conflict": { 1273 | "symfony/console": "<3.4" 1274 | }, 1275 | "require-dev": { 1276 | "symfony/console": "~3.4|~4.0" 1277 | }, 1278 | "suggest": { 1279 | "symfony/console": "For validating YAML files using the lint command" 1280 | }, 1281 | "type": "library", 1282 | "extra": { 1283 | "branch-alias": { 1284 | "dev-master": "3.4-dev" 1285 | } 1286 | }, 1287 | "autoload": { 1288 | "psr-4": { 1289 | "Symfony\\Component\\Yaml\\": "" 1290 | }, 1291 | "exclude-from-classmap": [ 1292 | "/Tests/" 1293 | ] 1294 | }, 1295 | "notification-url": "https://packagist.org/downloads/", 1296 | "license": [ 1297 | "MIT" 1298 | ], 1299 | "authors": [ 1300 | { 1301 | "name": "Fabien Potencier", 1302 | "email": "fabien@symfony.com" 1303 | }, 1304 | { 1305 | "name": "Symfony Community", 1306 | "homepage": "https://symfony.com/contributors" 1307 | } 1308 | ], 1309 | "description": "Symfony Yaml Component", 1310 | "homepage": "https://symfony.com", 1311 | "time": "2019-10-24T15:33:53+00:00" 1312 | }, 1313 | { 1314 | "name": "webmozart/assert", 1315 | "version": "1.6.0", 1316 | "source": { 1317 | "type": "git", 1318 | "url": "https://github.com/webmozart/assert.git", 1319 | "reference": "573381c0a64f155a0d9a23f4b0c797194805b925" 1320 | }, 1321 | "dist": { 1322 | "type": "zip", 1323 | "url": "https://api.github.com/repos/webmozart/assert/zipball/573381c0a64f155a0d9a23f4b0c797194805b925", 1324 | "reference": "573381c0a64f155a0d9a23f4b0c797194805b925", 1325 | "shasum": "" 1326 | }, 1327 | "require": { 1328 | "php": "^5.3.3 || ^7.0", 1329 | "symfony/polyfill-ctype": "^1.8" 1330 | }, 1331 | "conflict": { 1332 | "vimeo/psalm": "<3.6.0" 1333 | }, 1334 | "require-dev": { 1335 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 1336 | }, 1337 | "type": "library", 1338 | "autoload": { 1339 | "psr-4": { 1340 | "Webmozart\\Assert\\": "src/" 1341 | } 1342 | }, 1343 | "notification-url": "https://packagist.org/downloads/", 1344 | "license": [ 1345 | "MIT" 1346 | ], 1347 | "authors": [ 1348 | { 1349 | "name": "Bernhard Schussek", 1350 | "email": "bschussek@gmail.com" 1351 | } 1352 | ], 1353 | "description": "Assertions to validate method input/output with nice error messages.", 1354 | "keywords": [ 1355 | "assert", 1356 | "check", 1357 | "validate" 1358 | ], 1359 | "time": "2019-11-24T13:36:37+00:00" 1360 | }, 1361 | { 1362 | "name": "wp-coding-standards/wpcs", 1363 | "version": "2.2.0", 1364 | "source": { 1365 | "type": "git", 1366 | "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", 1367 | "reference": "f90e8692ce97b693633db7ab20bfa78d930f536a" 1368 | }, 1369 | "dist": { 1370 | "type": "zip", 1371 | "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/f90e8692ce97b693633db7ab20bfa78d930f536a", 1372 | "reference": "f90e8692ce97b693633db7ab20bfa78d930f536a", 1373 | "shasum": "" 1374 | }, 1375 | "require": { 1376 | "php": ">=5.4", 1377 | "squizlabs/php_codesniffer": "^3.3.1" 1378 | }, 1379 | "require-dev": { 1380 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0", 1381 | "phpcompatibility/php-compatibility": "^9.0", 1382 | "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" 1383 | }, 1384 | "suggest": { 1385 | "dealerdirect/phpcodesniffer-composer-installer": "^0.5.0 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." 1386 | }, 1387 | "type": "phpcodesniffer-standard", 1388 | "notification-url": "https://packagist.org/downloads/", 1389 | "license": [ 1390 | "MIT" 1391 | ], 1392 | "authors": [ 1393 | { 1394 | "name": "Contributors", 1395 | "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" 1396 | } 1397 | ], 1398 | "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", 1399 | "keywords": [ 1400 | "phpcs", 1401 | "standards", 1402 | "wordpress" 1403 | ], 1404 | "time": "2019-11-11T12:34:03+00:00" 1405 | }, 1406 | { 1407 | "name": "wpsh/local", 1408 | "version": "0.2.3", 1409 | "source": { 1410 | "type": "git", 1411 | "url": "https://github.com/wpsh/wpsh-local.git", 1412 | "reference": "17338432ed06386273a1ee4db8c8467aa8feab17" 1413 | }, 1414 | "dist": { 1415 | "type": "zip", 1416 | "url": "https://api.github.com/repos/wpsh/wpsh-local/zipball/17338432ed06386273a1ee4db8c8467aa8feab17", 1417 | "reference": "17338432ed06386273a1ee4db8c8467aa8feab17", 1418 | "shasum": "" 1419 | }, 1420 | "type": "library", 1421 | "notification-url": "https://packagist.org/downloads/", 1422 | "license": [ 1423 | "MIT" 1424 | ], 1425 | "description": "A Docker Compose development environment for any project.", 1426 | "time": "2019-06-12T10:54:32+00:00" 1427 | }, 1428 | { 1429 | "name": "xwp/wp-dev-lib", 1430 | "version": "1.3.2", 1431 | "source": { 1432 | "type": "git", 1433 | "url": "https://github.com/xwp/wp-dev-lib.git", 1434 | "reference": "31c72369fc43a4c956c43732f669dd3c7e179cef" 1435 | }, 1436 | "dist": { 1437 | "type": "zip", 1438 | "url": "https://api.github.com/repos/xwp/wp-dev-lib/zipball/31c72369fc43a4c956c43732f669dd3c7e179cef", 1439 | "reference": "31c72369fc43a4c956c43732f669dd3c7e179cef", 1440 | "shasum": "" 1441 | }, 1442 | "type": "library", 1443 | "notification-url": "https://packagist.org/downloads/", 1444 | "license": [ 1445 | "MIT" 1446 | ], 1447 | "authors": [ 1448 | { 1449 | "name": "Weston Ruter", 1450 | "email": "weston@xwp.co", 1451 | "homepage": "https://weston.ruter.net" 1452 | }, 1453 | { 1454 | "name": "XWP", 1455 | "email": "engage@xwp.co", 1456 | "homepage": "https://xwp.co" 1457 | } 1458 | ], 1459 | "description": "Common code used during development of WordPress plugins and themes", 1460 | "homepage": "https://github.com/xwp/wp-dev-lib", 1461 | "keywords": [ 1462 | "wordpress" 1463 | ], 1464 | "time": "2020-01-08T06:05:16+00:00" 1465 | } 1466 | ], 1467 | "aliases": [], 1468 | "minimum-stability": "stable", 1469 | "stability-flags": [], 1470 | "prefer-stable": false, 1471 | "prefer-lowest": false, 1472 | "platform": [], 1473 | "platform-dev": [], 1474 | "platform-overrides": { 1475 | "php": "5.6" 1476 | } 1477 | } 1478 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | 5 | mysql: 6 | image: mysql:5 7 | volumes: 8 | - db_data:/var/lib/mysql 9 | restart: always 10 | environment: 11 | MYSQL_DATABASE: wordpress 12 | MYSQL_USER: wordpress 13 | MYSQL_PASSWORD: password 14 | MYSQL_ROOT_PASSWORD: password 15 | 16 | wordpress: 17 | image: wordpress:php7.3-apache 18 | depends_on: 19 | - mysql 20 | ports: 21 | - "80:80" 22 | - "443:443" 23 | volumes: 24 | - .:/var/www/html/wp-content/plugins/application-passwords 25 | - ./dist:/var/www/html/wp-content/plugins/application-passwords-dist 26 | restart: always 27 | environment: 28 | WORDPRESS_DEBUG: 1 29 | WORDPRESS_DB_USER: wordpress 30 | WORDPRESS_DB_PASSWORD: password 31 | 32 | volumes: 33 | db_data: {} 34 | wp_data: {} 35 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = function( grunt ) { 4 | 'use strict'; 5 | 6 | require( 'load-grunt-tasks' )( grunt ); 7 | 8 | grunt.initConfig( { 9 | dist_dir: 'dist', 10 | 11 | clean: { 12 | dist: [ '<%= dist_dir %>' ], 13 | }, 14 | 15 | copy: { 16 | dist: { 17 | files: [ 18 | // Ensure that all required files are included in the distribution bundle. 19 | { 20 | src: [ 21 | '*.php', 22 | 'application-passwords.js', 23 | 'application-passwords.css', 24 | 'auth-app.js', 25 | 'readme.md', 26 | 'readme.txt', 27 | 'composer.json', 28 | 'composer.lock', 29 | ], 30 | dest: '<%= dist_dir %>', 31 | expand: true, 32 | }, 33 | ], 34 | }, 35 | }, 36 | 37 | wp_deploy: { 38 | options: { 39 | plugin_slug: 'application-passwords', 40 | build_dir: '<%= dist_dir %>', 41 | assets_dir: 'assets', 42 | }, 43 | trunk: { 44 | options: { 45 | deploy_tag: false, 46 | deploy_trunk: true, 47 | }, 48 | }, 49 | }, 50 | } ); 51 | 52 | grunt.registerTask( 53 | 'build', [ 54 | 'clean', 55 | 'copy', 56 | ] 57 | ); 58 | 59 | grunt.registerTask( 60 | 'deploy', [ 61 | 'build', 62 | 'wp_deploy:trunk', 63 | ] 64 | ); 65 | }; 66 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@georgestephanis/application-passwords", 3 | "version": "1.0.0", 4 | "description": "Provide Application Passwords for WordPress core", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/WordPress/application-passwords.git" 8 | }, 9 | "author": "George Stephanis (https://stephanis.info)", 10 | "license": "GPL-2.0-or-later", 11 | "devDependencies": { 12 | "@wordpress/eslint-plugin": "^2.4.0", 13 | "eslint": "^6.8.0", 14 | "grunt": "^1.3.0", 15 | "grunt-cli": "^1.3.2", 16 | "grunt-contrib-clean": "^2.0.0", 17 | "grunt-contrib-copy": "^1.0.0", 18 | "grunt-wp-deploy": "^2.1.2", 19 | "husky": "^3.1.0", 20 | "load-grunt-tasks": "^5.1.0" 21 | }, 22 | "scripts": { 23 | "postinstall": "composer install", 24 | "lint-js": "eslint .", 25 | "lint-staged": "npm run lint-js && composer lint-staged", 26 | "lint": "npm run lint-js && composer lint-head", 27 | "build": "grunt build", 28 | "deploy": "grunt deploy" 29 | }, 30 | "husky": { 31 | "hooks": { 32 | "pre-commit": "npm run lint-staged" 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /phpcs.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | Generally-applicable sniffs for WordPress plugins 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | */node_modules/* 15 | */vendor/* 16 | 17 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | tests 15 | 16 | 17 | 18 | 19 | . 20 | 21 | tests 22 | vendor 23 | node_modules 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Application Passwords for WordPress 2 | 3 | **⚠️ IMPORTANT: This plugin has been merged into WordPress core version 5.6 and doesn't have to be installed separately. [View the integration guide →](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/)** 4 | 5 | If you wish to open a ticket relating to this feature, please do so in the [Application Passwords component](https://make.wordpress.org/core/components/rest-api/application-passwords/) on the WordPress Core issue tracker. 6 | 7 | --- 8 | 9 | Creates unique passwords for applications to authenticate users without revealing their main passwords. 10 | 11 | 12 | ## Install 13 | 14 | This plugin has been merged into WordPress core version 5.6 and doesn't have to be installed separately. [View the integration guide →](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/) 15 | 16 | ## Credits 17 | 18 | Created by [George Stephanis](https://github.com/georgestephanis). View [all contributors](https://github.com/WordPress/application-passwords/graphs/contributors). 19 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | === Application Passwords === 2 | Contributors: georgestephanis, valendesigns, kraftbj, kasparsd, passoniate 3 | Tags: application-passwords, rest api, xml-rpc, security, authentication 4 | Requires at least: 4.4 5 | Tested up to: 5.5 6 | Stable tag: trunk 7 | License: GPLv2 or later 8 | License URI: http://www.gnu.org/licenses/gpl-2.0.html 9 | 10 | Creates unique passwords for applications to authenticate users without revealing their main passwords. 11 | 12 | 13 | == Description == 14 | 15 | **⚠️ IMPORTANT: This plugin has been merged into WordPress core version 5.6 and doesn't have to be installed separately. [View the integration guide →](https://make.wordpress.org/core/2020/11/05/application-passwords-integration-guide/)** 16 | 17 | --- 18 | 19 | Use Application Passwords to authenticate users without providing their passwords directly. Instead, a unique password is generated for each application without revealing the user's main password. Application passwords can be revoked for each application individually. 20 | 21 | **Important:** Application Passwords can be used only for authenticating API requests such as [REST API](https://developer.wordpress.org/rest-api/) and [XML-RPC](https://codex.wordpress.org/XML-RPC_WordPress_API), and they won't work for regular site logins. 22 | 23 | 24 | = Contribute = 25 | 26 | - Translate the plugin [into your language](https://translate.wordpress.org/projects/wp-plugins/application-passwords/). 27 | - Report issues, suggest features and contribute code [on GitHub](https://github.com/WordPress/application-passwords). 28 | 29 | 30 | = Creating Application Password Manually = 31 | 32 | 1. Go the User Profile page of the user that you want to generate a new application password for. To do so, click *Users* on the left side of the WordPress admin, then click on the user that you want to manage. 33 | 2. Scroll down until you see the Application Passwords section. This is typically at the bottom of the page. 34 | 3. Within the input field, type in a name for your new application password, then click *Add New*. 35 | **Note:** The application password name is only used to describe your password for easy management later. It will not affect your password in any way. Be descriptive, as it will lead to easier management if you ever need to change it later. 36 | 4. Once the *Add New* button is clicked, your new application password will appear. Be sure to keep this somewhere safe, as it will not be displayed to you again. If you lose this password, it cannot be obtained again. 37 | 38 | 39 | = Two Factor Support = 40 | 41 | Application Passwords can be used together with the [Two Factor plugin](https://wordpress.org/plugins/two-factor/) as long as you bypass the API acccess restrictions added by the Two Factor plugin. Those protections disable API requests with password authentication _for users with Two Factor enabled_. 42 | 43 | Use the `two_factor_user_api_login_enable` filter to allow API requests authenticated using an application passwords: 44 | 45 | add_filter( 'two_factor_user_api_login_enable', function( $enable ) { 46 | // Allow API login when using an application password even with 2fa enabled. 47 | if ( did_action( 'application_password_did_authenticate' ) ) { 48 | return true; 49 | } 50 | 51 | return $enable; 52 | } ); 53 | 54 | This is not required if the user associated with the application password doesn't have any of the Two Factor methods enabled. 55 | 56 | 57 | = Requesting Password for Application = 58 | 59 | To request a password for your application, redirect users to: 60 | 61 | https://example.com/wp-admin/admin.php?page=auth_app 62 | 63 | and use the following `GET` request parameters to specify: 64 | 65 | - `app_name` (required) - The human readable identifier for your app. This will be the name of the generated application password, so structure it like ... "WordPress Mobile App on iPhone 12" for uniqueness between multiple versions. If omitted, the user will be required to provide an application name. 66 | - `success_url` (recommended) - The URL that you'd like the user to be sent to if they approve the connection. Two GET variables will be appended when they are passed back -- `user_login` and `password` -- these credentials can then be used for API calls. If the `success_url` variable is omitted, a password will be generated and displayed to the user, to manually enter into your application. 67 | - `reject_url` (optional) - If included, the user will get sent there if they reject the connection. If omitted, the user will be sent to the `success_url`, with `?success=false` appended to the end. If the `success_url` is omitted, the user will be sent to their dashboard. 68 | 69 | 70 | = Testing an Application Password = 71 | 72 | We use [curl](https://curl.haxx.se) to send HTTP requests to the API endpoints in the examples below. 73 | 74 | #### WordPress REST API 75 | 76 | Make a REST API call to update a post. Because you are performing a `POST` request, you will need to authorize the request using your newly created base64 encoded access token. If authorized correctly, you will see the post title update to "New Title." 77 | 78 | curl --user "USERNAME:APPLICATION_PASSWORD" -X POST -d "title=New Title" https://LOCALHOST/wp-json/wp/v2/posts/POST_ID 79 | 80 | When running this command, be sure to replace `USERNAME` and `APPLICATION_PASSWORD` with your credentials (curl takes care of base64 encoding and setting the `Authorization` header), `LOCALHOST` with the hostname of your WordPress installation, and `POST_ID` with the ID of the post that you want to edit. 81 | 82 | #### XML-RPC 83 | 84 | Unlike the WordPress REST API, XML-RPC does not require your username and password to be base64 encoded. Send an XML-RPC request to list all users: 85 | 86 | curl -H 'Content-Type: text/xml' -d 'wp.getUsers1USERNAMEPASSWORD' https://LOCALHOST/xmlrpc.php 87 | 88 | In the above example, replace `USERNAME` with your username, `PASSWORD` with your new application password, and `LOCALHOST` with the hostname of your WordPress installation. This should output a response containing all users on your site. 89 | 90 | 91 | = Plugin History = 92 | 93 | This is a feature plugin that is a spinoff of the main [Two-Factor Authentication plugin](https://github.com/WordPress/two-factor/). 94 | 95 | 96 | == Changelog == 97 | 98 | See the [release notes on GitHub](https://github.com/WordPress/application-passwords/releases). 99 | 100 | 101 | == Installation == 102 | 103 | Search for "Application Passwords" under "Plugins" → "Add New" in your WordPress dashboard to install the plugin. 104 | 105 | Or install it manually: 106 | 107 | 1. Download the [plugin zip file](https://downloads.wordpress.org/plugin/application-passwords.zip). 108 | 2. Go to *Plugins* → *Add New* in your WordPress admin. 109 | 3. Click on the *Upload Plugin* button. 110 | 4. Select the file you downloaded. 111 | 5. Click *Install Plugin*. 112 | 6. Activate. 113 | 114 | = Using Composer = 115 | 116 | Add this plugin as a [Composer](https://getcomposer.org) dependency [from Packagist](https://packagist.org/packages/georgestephanis/application-passwords): 117 | 118 | composer require georgestephanis/application-passwords 119 | 120 | 121 | == Screenshots == 122 | 123 | 1. New application passwords has been created. 124 | 2. After at least one Application Password for you account exists, you'll see a table displaying them, allowing you to view usage and revoke them as desired. 125 | -------------------------------------------------------------------------------- /tests/logs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /tests/test-class.application-passwords.php: -------------------------------------------------------------------------------- 1 | assertEquals( 10, has_action( 'authenticate', array( 'Application_Passwords', 'authenticate' ) ) ); 16 | } 17 | 18 | /** 19 | * Regular front-end requests are not REST requests. 20 | * 21 | * @covers Application_Passwords::is_api_request() 22 | */ 23 | public function test_rest_api_request() { 24 | $this->assertFalse( Application_Passwords::is_api_request() ); 25 | } 26 | 27 | /** 28 | * HTTP Auth headers are used to determine the current user. 29 | * 30 | * @covers Application_Passwords::rest_api_auth_handler() 31 | */ 32 | public function test_can_login_user_through_http_auth_headers() { 33 | $user_id = $this->factory->user->create( 34 | array( 35 | 'user_login' => 'http_auth_login', 36 | 'user_pass' => 'http_auth_pass', // Shouldn't be allowed for API login. 37 | ) 38 | ); 39 | 40 | // Create a new app-only password. 41 | $user_app_password = Application_Passwords::create_new_application_password( $user_id, 'phpunit' ); 42 | 43 | // Fake a REST API request. 44 | add_filter( 'application_password_is_api_request', '__return_true' ); 45 | 46 | // Fake an HTTP Auth request with the regular account password first. 47 | $_SERVER['PHP_AUTH_USER'] = 'http_auth_login'; 48 | $_SERVER['PHP_AUTH_PW'] = 'http_auth_pass'; 49 | 50 | $this->assertEquals( 51 | 0, 52 | Application_Passwords::rest_api_auth_handler( null ), 53 | 'Regular user account password should not be allowed for API authenticaiton' 54 | ); 55 | 56 | // Not try with an App password instead. 57 | $_SERVER['PHP_AUTH_PW'] = $user_app_password[0]; 58 | 59 | $this->assertEquals( 60 | $user_id, 61 | Application_Passwords::rest_api_auth_handler( null ), 62 | 'Application passwords should be allowed for API authentication' 63 | ); 64 | 65 | remove_filter( 'application_password_is_api_request', '__return_true' ); 66 | 67 | // Cleanup all the global state. 68 | unset( $_SERVER['PHP_AUTH_USER'], $_SERVER['PHP_AUTH_PW'] ); 69 | } 70 | } 71 | --------------------------------------------------------------------------------