| [ Index ] |
PHP Cross Reference of WordPress Trunk (Updated Daily) |
[Summary view] [Print] [Text view]
1 <?php 2 /** 3 * Secrets API 4 * 5 * @package WordPress 6 * @subpackage Secrets 7 * @since 7.2.0 8 */ 9 10 /** 11 * The store or key backend is misbehaving. 12 * 13 * The secret exists and was decrypted, but the backend it came from failed in a way 14 * none of the more specific codes below describe. 15 * 16 * @since 7.2.0 17 */ 18 define( 'WP_SECRETS_ERROR_DECRYPTION_FAILED', 'secret_decryption_failed' ); 19 20 /** 21 * The key needed to encrypt or decrypt a secret could not be obtained. 22 * 23 * The keyring is unreachable, misconfigured, or refusing to answer. 24 * 25 * @since 7.2.0 26 */ 27 define( 'WP_SECRETS_ERROR_KEY_UNAVAILABLE', 'secret_key_unavailable' ); 28 29 /** 30 * The storage back end could not be reached at all. 31 * 32 * @since 7.2.0 33 */ 34 define( 'WP_SECRETS_ERROR_STORE_UNAVAILABLE', 'secret_store_unavailable' ); 35 36 /** 37 * A secret name failed wp_secrets_validate_name(). 38 * 39 * @since 7.2.0 40 */ 41 define( 'WP_SECRETS_ERROR_INVALID_NAME', 'secret_invalid_name' ); 42 43 /** 44 * A secret value was not a string. 45 * 46 * @since 7.2.0 47 */ 48 define( 'WP_SECRETS_ERROR_INVALID_VALUE', 'secret_invalid_value' ); 49 50 /** 51 * A caller passed an argument this API cannot act on. 52 * 53 * An unrecognized version constant, or an invalid scope, slot, or site id. 54 * 55 * Distinct from the codes above in cause rather than in severity: those describe 56 * a runtime condition a correct caller can still hit (a name that failed 57 * validation, an unavailable key), while this one only ever means the calling 58 * code is wrong. Every site that returns it also calls _doing_it_wrong(), so the 59 * mistake is visible in development rather than only in a return value the caller 60 * may not be checking. 61 * 62 * @since 7.2.0 63 */ 64 define( 'WP_SECRETS_ERROR_INVALID_ARGUMENT', 'secret_invalid_argument' ); 65 66 /** 67 * The cipher this API depends on is unavailable. 68 * 69 * Neither libsodium nor its sodium_compat fallback is present in this environment. 70 * 71 * @since 7.2.0 72 */ 73 define( 'WP_SECRETS_ERROR_CRYPTO_UNAVAILABLE', 'secret_crypto_unavailable' ); 74 75 /** 76 * A stored record could not be parsed as a secret record at all. 77 * 78 * Distinct from decrypting incorrectly, which is WP_SECRETS_ERROR_DECRYPTION_FAILED. 79 * 80 * @since 7.2.0 81 */ 82 define( 'WP_SECRETS_ERROR_RECORD_MALFORMED', 'secret_record_malformed' ); 83 84 /** 85 * A stored record parses, but its format version is not one this API understands. 86 * 87 * Distinct from WP_SECRETS_ERROR_RECORD_MALFORMED so that an operator, or Site 88 * Health, can tell a corrupt record apart from one written by a newer version of 89 * this plugin than is currently active. See 90 * docs/decisions/0006-record-format-v2-not-read-compatible.md. The upgrade path for a future v2 is not 91 * designed, so an unrecognized version is rejected outright rather than guessed at. 92 * 93 * @since 7.2.0 94 */ 95 define( 'WP_SECRETS_ERROR_RECORD_UNSUPPORTED_VERSION', 'secret_record_unsupported_version' ); 96 97 /** 98 * The active provider does not accept writes. 99 * 100 * Its credentials are managed by host tooling, a control panel, or a key policy 101 * outside WordPress. Distinct from WP_SECRETS_ERROR_STORE_UNAVAILABLE, which means 102 * the write could have succeeded and did not. This one means it was never possible. 103 * 104 * @since 7.2.0 105 */ 106 define( 'WP_SECRETS_ERROR_PROVIDER_READ_ONLY', 'secret_provider_read_only' ); 107 108 /** 109 * The current record format version. 110 * 111 * Bumped only if the record shape in WP_Secrets_Cipher's slot arrays ever changes 112 * incompatibly. 113 * 114 * @since 7.2.0 115 */ 116 define( 'WP_SECRETS_RECORD_VERSION', 1 ); 117 118 /** 119 * The longest a secret name may be. 120 * 121 * The wp_options.option_name column allows 191 characters. The longest prefix this 122 * API stores under is '_wp_network_secret_' (19 characters), so 191 - 19 = 172 123 * is the longest name any store built on core's options tables can accept 124 * without truncation, and every store is held to the same limit for 125 * consistency regardless of what a specific backend could technically fit. 126 * 127 * @since 7.2.0 128 */ 129 define( 'WP_SECRETS_MAX_NAME_LENGTH', 172 ); 130 131 /** 132 * Capability required to manage site-scope secrets at the operator boundary. 133 * 134 * Checked by WP-CLI and by a future admin screen, never by the function-level API 135 * itself. See wp_set_secret() for why. 136 * 137 * @since 7.2.0 138 */ 139 define( 'WP_SECRETS_CAP_MANAGE', 'manage_secrets' ); 140 141 /** 142 * Capability required at the operator boundary to manage network-scope secrets. 143 * 144 * @since 7.2.0 145 */ 146 define( 'WP_SECRETS_CAP_MANAGE_NETWORK', 'manage_network_secrets' ); 147 148 /** 149 * Best-effort clearing of a plaintext value from memory. 150 * 151 * This is hygiene, not a guarantee. PHP strings are reference-counted and often 152 * shared by copy-on-write; sodium_memzero() can only scrub the bytes of a string it 153 * exclusively owns; if another reference to the same value exists elsewhere, 154 * PHP forces a private copy before zeroing rather than corrupting the shared buffer, 155 * and that other reference is left completely untouched. Call this on every plaintext 156 * local as soon as it is no longer needed, and do not keep incidental copies around. 157 * 158 * Under sodium_compat, core's documented fallback when the libsodium extension is 159 * unavailable, this scrubs nothing. A userland polyfill cannot reach a PHP string's 160 * underlying memory at all. Overwriting the local binding is the only thing available 161 * in that case, and it removes the live reference from this scope without touching 162 * the memory the string used to occupy. 163 * 164 * @since 7.2.0 165 * 166 * @param string $value The value to clear, by reference. 167 */ 168 function wp_secrets_memzero( &$value ): void { 169 if ( ! is_string( $value ) ) { 170 return; 171 } 172 173 if ( function_exists( 'sodium_memzero' ) ) { 174 try { 175 /* 176 * sodium_memzero() is called through a true reference alias, not 177 * directly on $value, so that a static analyzer's stub for 178 * sodium_memzero() (whose own by-ref parameter is documented as 179 * becoming null) is evaluated against a local variable rather than 180 * against this function's own by-ref parameter. The alias is not a 181 * copy: `=&` shares the identical underlying storage, so this still 182 * zeroes the real buffer the caller holds. 183 */ 184 $alias =& $value; 185 sodium_memzero( $alias ); 186 } catch ( \Throwable $e ) { 187 // A value this function doesn't exclusively own (a shared, 188 // copy-on-write buffer) can legitimately refuse. The unconditional 189 // reassignment below still runs regardless of what happened here. 190 unset( $e ); 191 } 192 } 193 194 // Reached on every path, including sodium_memzero() success (it leaves the 195 // variable null, not an empty string) so callers see one consistent contract. 196 $value = ''; 197 } 198 199 /** 200 * Validates a secret's name. 201 * 202 * Names take the form 'plugin-slug/secret-name': lowercase alphanumerics, 203 * hyphens, and underscores in each segment, exactly one '/' separating them, 204 * and no segment starting or ending with a hyphen or underscore. 205 * 206 * Namespacing is a matter of organization, not security. It groups secrets by owner 207 * so that listings and admin screens can be sensible. It confers no isolation: any 208 * plugin that can run PHP can read any secret, namespaced or not. 209 * 210 * An unnamespaced name ('secret-name', no '/') is accepted, but reports through 211 * _doing_it_wrong(). It exists for one reason: code written against the earlier 212 * prototype used a flat keyspace, and refusing those names outright would mean 213 * every such call site has to be rewritten before it can be ported at all. 214 * Accepting them keeps that migration incremental. Nothing else should use one. 215 * Two plugins that both choose 'api-key' collide silently, which is a real problem 216 * even though it is not a security one. 217 * 218 * @since 7.2.0 219 * 220 * @param string $name Candidate secret name. 221 * @return true|WP_Error True if $name is usable, including the unnamespaced form. 222 * Otherwise WP_Error with code WP_SECRETS_ERROR_INVALID_NAME. 223 */ 224 function wp_secrets_validate_name( string $name ) { 225 if ( '' === $name ) { 226 return new WP_Error( 227 WP_SECRETS_ERROR_INVALID_NAME, 228 __( 'Secret names must not be empty.' ) 229 ); 230 } 231 232 if ( strlen( $name ) > WP_SECRETS_MAX_NAME_LENGTH ) { 233 return new WP_Error( 234 WP_SECRETS_ERROR_INVALID_NAME, 235 sprintf( 236 /* translators: %d: Maximum allowed length, in characters. */ 237 __( 'Secret names must be %d characters or fewer.' ), 238 WP_SECRETS_MAX_NAME_LENGTH 239 ) 240 ); 241 } 242 243 $slashes = substr_count( $name, '/' ); 244 245 if ( $slashes > 1 ) { 246 return new WP_Error( 247 WP_SECRETS_ERROR_INVALID_NAME, 248 __( 'Secret names may contain at most one "/", separating a namespace from a key.' ) 249 ); 250 } 251 252 $segment_pattern = '/^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/'; 253 254 if ( 0 === $slashes ) { 255 if ( ! preg_match( $segment_pattern, $name ) ) { 256 return new WP_Error( 257 WP_SECRETS_ERROR_INVALID_NAME, 258 __( 'Secret names may contain only lowercase letters, numbers, hyphens, and underscores, and must not start or end with a hyphen or underscore.' ) 259 ); 260 } 261 262 /* 263 * Reported here rather than at each public entry point: this is the one 264 * place that decides what a name is, so a caller cannot reach the store 265 * with an unnamespaced name without passing through it. 266 */ 267 _doing_it_wrong( 268 __FUNCTION__, 269 sprintf( 270 /* translators: %s: The unnamespaced secret name. */ 271 __( 'The secret name "%s" has no namespace. Namespaced names ("plugin-slug/secret-name") group secrets by owner so that listings and admin screens can be organized; unnamespaced names are supported only so that code written against the Displace prototype can be ported incrementally.' ), 272 $name 273 ), 274 '7.2.0' 275 ); 276 277 return true; 278 } 279 280 $segments = explode( '/', $name ); 281 $namespace = $segments[0]; 282 $key = $segments[1]; 283 284 if ( '' === $namespace || '' === $key ) { 285 return new WP_Error( 286 WP_SECRETS_ERROR_INVALID_NAME, 287 __( 'Both segments of a secret name must be non-empty.' ) 288 ); 289 } 290 291 if ( ! preg_match( $segment_pattern, $namespace ) || ! preg_match( $segment_pattern, $key ) ) { 292 return new WP_Error( 293 WP_SECRETS_ERROR_INVALID_NAME, 294 __( 'Secret name segments may contain only lowercase letters, numbers, hyphens, and underscores, and must not start or end with a hyphen or underscore.' ) 295 ); 296 } 297 298 return true; 299 } 300 301 /** 302 * Returns the active secret store. 303 * 304 * No filter is applied here, or anywhere on the retrieval path. See 305 * docs/spec/extension-points.md. A secrets.php drop-in overrides the store by setting 306 * $GLOBALS['wp_secrets_store'] to an instance before this is first called, and 307 * that global is checked here directly, not through a hook. 308 * 309 * If the global was set but is not a valid WP_Secrets_Store, or if the drop-in 310 * itself failed to load cleanly, this returns WP_Secrets_Broken_Store rather than 311 * silently using the default: the drop-in's presence signals the operator wants 312 * storage other than local options, and falling back to local options anyway would 313 * be the exact silent downgrade this API refuses to make. 314 * 315 * @since 7.2.0 316 * 317 * @return WP_Secrets_Store 318 */ 319 function _wp_secrets_get_store(): WP_Secrets_Store { 320 static $store = null; 321 322 if ( $store instanceof WP_Secrets_Store ) { 323 return $store; 324 } 325 326 if ( ! empty( $GLOBALS['wp_secrets_dropin_broken'] ) ) { 327 $store = new WP_Secrets_Broken_Store(); 328 329 return $store; 330 } 331 332 if ( isset( $GLOBALS['wp_secrets_store'] ) && $GLOBALS['wp_secrets_store'] instanceof WP_Secrets_Store ) { 333 $store = $GLOBALS['wp_secrets_store']; 334 335 return $store; 336 } 337 338 $store = new WP_Secrets_Option_Store(); 339 340 return $store; 341 } 342 343 /** 344 * Returns the active key manager. 345 * 346 * A secrets.php drop-in overrides the keyring by setting 347 * $GLOBALS['wp_secrets_keyring'] to an instance before this is first called. See 348 * _wp_secrets_get_store() for why an invalid override or a failed drop-in load 349 * fails closed (WP_Secrets_Broken_Keyring) rather than falling back to the default. 350 * 351 * @since 7.2.0 352 * 353 * @return WP_Secrets_Key_Manager 354 */ 355 function _wp_secrets_get_key_manager(): WP_Secrets_Key_Manager { 356 static $key_manager = null; 357 358 if ( $key_manager instanceof WP_Secrets_Key_Manager ) { 359 return $key_manager; 360 } 361 362 if ( ! empty( $GLOBALS['wp_secrets_dropin_broken'] ) ) { 363 $key_manager = new WP_Secrets_Key_Manager( new WP_Secrets_Broken_Keyring() ); 364 365 return $key_manager; 366 } 367 368 $keyring = null; 369 370 if ( isset( $GLOBALS['wp_secrets_keyring'] ) && $GLOBALS['wp_secrets_keyring'] instanceof WP_Secrets_Keyring ) { 371 $keyring = $GLOBALS['wp_secrets_keyring']; 372 } 373 374 $key_manager = new WP_Secrets_Key_Manager( $keyring ); 375 376 return $key_manager; 377 } 378 379 /** 380 * Whether a secrets.php drop-in is present and was loaded. 381 * 382 * True whether or not the drop-in successfully provided a store or keyring 383 * override. It reports presence rather than health; Site Health reports separately 384 * on whether a loaded drop-in is actually working. 385 * 386 * @since 7.2.0 387 * 388 * @return bool 389 */ 390 function wp_using_secrets_dropin(): bool { 391 return ! empty( $GLOBALS['wp_secrets_dropin_loaded'] ); 392 } 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 /** 413 * Encrypts and stores a secret. 414 * 415 * Encryption is unconditional: there is no plaintext mode and no constant to 416 * disable it. No capability check is applied here, because this must be callable from 417 * cron, REST, and front-end requests where no user is logged in. Enforce 418 * capabilities at the operator boundary (CLI, an admin screen) instead. 419 * 420 * @since 7.2.0 421 * 422 * @param string $name The secret's namespaced name ('plugin-slug/secret-name'). 423 * @param string $value The plaintext value to store. 424 * @return true|WP_Error 425 */ 426 function wp_set_secret( string $name, $value ) { 427 return _wp_secrets_set( $name, $value, false ); 428 } 429 430 /** 431 * Imports an existing option's value as a secret. 432 * 433 * For an explicit migration, "on their own explicit upgrade schedule," 434 * in the proposal's words, because "core cannot reliably tell which options are 435 * credentials, and guessing would break sites." The source option is left 436 * untouched: this reads it, it does not move or delete it. 437 * 438 * The imported secret is flagged needs_rotation, unconditionally. A credential that 439 * sat in a plain option has already been through however many backups and 440 * replication paths that option went through; re-encrypting it here does not undo 441 * that, and the flag exists so an operator (or a future admin screen) knows to 442 * actually rotate the value rather than considering the migration finished. 443 * 444 * @since 7.2.0 445 * 446 * @param string $option The existing option's name. 447 * @param string $name The secret's namespaced name to store it under. 448 * @return true|WP_Error 449 */ 450 function wp_import_option_as_secret( string $option, string $name ) { 451 if ( '' === $option ) { 452 return new WP_Error( 453 WP_SECRETS_ERROR_INVALID_VALUE, 454 __( 'The option name must not be empty.' ) 455 ); 456 } 457 458 $value = get_option( $option, null ); 459 460 if ( null === $value ) { 461 return new WP_Error( 462 WP_SECRETS_ERROR_INVALID_VALUE, 463 __( 'The option does not exist.' ) 464 ); 465 } 466 467 if ( ! is_string( $value ) ) { 468 return new WP_Error( 469 WP_SECRETS_ERROR_INVALID_VALUE, 470 __( 'The option value is not a string and cannot be imported as a secret.' ) 471 ); 472 } 473 474 return _wp_secrets_set( $name, $value, false, true, 'imported' ); 475 } 476 477 478 479 /** 480 * Retrieves a secret. 481 * 482 * Three states, never collapsed: a WP_Secret if it exists and decrypts, null if it 483 * does not exist, WP_Error if it exists but could not be retrieved. No capability 484 * check is applied here. See wp_set_secret(). 485 * 486 * @since 7.2.0 487 * 488 * @param string $name The secret's namespaced name. 489 * @param string $version A WP_Secret_Version constant. Default WP_Secret_Version::CURRENT. 490 * @return WP_Secret|null|WP_Error 491 * 492 * @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $version 493 */ 494 function wp_get_secret( string $name, string $version = WP_Secret_Version::CURRENT ) { 495 return _wp_secrets_get( $name, $version, false ); 496 } 497 498 /** 499 * Deletes a secret. 500 * 501 * @since 7.2.0 502 * 503 * @param string $name The secret's namespaced name. 504 * @return true|WP_Error 505 */ 506 function wp_delete_secret( string $name ) { 507 return _wp_secrets_delete( $name, false ); 508 } 509 510 /** 511 * Clears a secret's previous version, retiring it for good. 512 * 513 * An explicit operator action, with no timers and no cron. Calling this on a secret 514 * that has never been rotated, or that does not exist, is a successful no-op: 515 * the previous slot is already absent either way. 516 * 517 * Beyond the API surface the proposal names. 518 * 519 * @since 7.2.0 520 * 521 * @param string $name The secret's namespaced name. 522 * @return true|WP_Error 523 */ 524 function wp_retire_secret_version( string $name ) { 525 return _wp_secrets_retire( $name, false ); 526 } 527 528 /** 529 * Lists secrets by name and metadata. Never a value. 530 * 531 * Beyond the API surface the proposal names, and justified by its statement that 532 * the hooks and accessors a future admin screen needs are in scope now, even though 533 * the screen itself is not. 534 * 535 * @since 7.2.0 536 * 537 * @param string $namespace Only secrets whose name starts with "{$namespace}/" are 538 * returned. Default '' returns every secret. 539 * @return array|WP_Error Array of associative arrays, each with keys 'name', 540 * 'fingerprint', 'created', 'has_previous', and 541 * 'needs_rotation'. 542 * 543 * @phpstan-return list<array{name: string, fingerprint: string, created: int, has_previous: bool, needs_rotation: bool}>|WP_Error 544 */ 545 function wp_list_secrets( string $namespace = '' ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- $namespace is the parameter name the proposal specifies. 546 return _wp_secrets_list( $namespace, false ); 547 } 548 549 /** 550 * Encrypts and stores a network-scope secret. 551 * 552 * Site secrets and network secrets are separate functions with separate 553 * capabilities and separate storage prefixes, with no implicit fallback from one 554 * scope to the other. The proposal describes network scope but does not name these 555 * functions; these are this implementation's names for them. 556 * 557 * @since 7.2.0 558 * 559 * @param string $name The secret's namespaced name. 560 * @param string $value The plaintext value to store. 561 * @return true|WP_Error 562 */ 563 function wp_set_network_secret( string $name, $value ) { 564 return _wp_secrets_set( $name, $value, true ); 565 } 566 567 /** 568 * Retrieves a network-scope secret. 569 * 570 * @since 7.2.0 571 * 572 * @param string $name The secret's namespaced name. 573 * @param string $version A WP_Secret_Version constant. Default WP_Secret_Version::CURRENT. 574 * @return WP_Secret|null|WP_Error 575 * 576 * @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $version 577 */ 578 function wp_get_network_secret( string $name, string $version = WP_Secret_Version::CURRENT ) { 579 return _wp_secrets_get( $name, $version, true ); 580 } 581 582 /** 583 * Deletes a network-scope secret. 584 * 585 * @since 7.2.0 586 * 587 * @param string $name The secret's namespaced name. 588 * @return true|WP_Error 589 */ 590 function wp_delete_network_secret( string $name ) { 591 return _wp_secrets_delete( $name, true ); 592 } 593 594 /** 595 * Clears a network-scope secret's previous version. 596 * 597 * @since 7.2.0 598 * 599 * @param string $name The secret's namespaced name. 600 * @return true|WP_Error 601 */ 602 function wp_retire_network_secret_version( string $name ) { 603 return _wp_secrets_retire( $name, true ); 604 } 605 606 /** 607 * Lists network-scope secrets by name and metadata. Never a value. 608 * 609 * @since 7.2.0 610 * 611 * @param string $namespace Only secrets whose name starts with "{$namespace}/" are 612 * returned. Default '' returns every network-scope secret. 613 * @return array|WP_Error 614 * 615 * @phpstan-return list<array{name: string, fingerprint: string, created: int, has_previous: bool, needs_rotation: bool}>|WP_Error 616 */ 617 function wp_list_network_secrets( string $namespace = '' ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.namespaceFound -- matches wp_list_secrets()'s own signature. 618 return _wp_secrets_list( $namespace, true ); 619 } 620 621 /** 622 * Returns the active provider. 623 * 624 * The provider is whatever is responsible for holding, protecting, and answering for 625 * this site's secrets. 626 * 627 * A secrets.php drop-in overrides it by setting $GLOBALS['wp_secrets_provider'] to 628 * an instance before this is first called. Absent that, the provider WordPress 629 * ships is assembled from the active store and keyring, which a drop-in can also 630 * replace individually, so wanting host-managed key custody with default storage needs 631 * no provider at all. 632 * 633 * No filter is applied here, or anywhere on the retrieval path. Substitution is by 634 * replacement, never by interception: a filter that can observe which provider 635 * answers is one step from a filter that can answer instead. 636 * 637 * Cached for the request. A failed drop-in fails closed, returning WP_Error from 638 * every operation, rather than silently reverting to the default, because a 639 * misconfigured credential backend must never look like a working one that happens 640 * to be empty. 641 * 642 * @since 7.2.0 643 * 644 * @return WP_Secrets_Provider 645 */ 646 function _wp_secrets_get_provider(): WP_Secrets_Provider { 647 static $provider = null; 648 649 if ( $provider instanceof WP_Secrets_Provider ) { 650 return $provider; 651 } 652 653 if ( ! empty( $GLOBALS['wp_secrets_dropin_broken'] ) ) { 654 $provider = new WP_Secrets_Broken_Provider(); 655 656 return $provider; 657 } 658 659 if ( isset( $GLOBALS['wp_secrets_provider'] ) ) { 660 /* 661 * Set, but not a provider. Failing closed here rather than falling through to 662 * the default is the whole point: a platform drop-in that names its class 663 * wrongly would otherwise have every secret served from wp_options, where none 664 * of them exist, and a site whose credentials are held elsewhere would report 665 * them absent rather than unreachable. 666 */ 667 if ( ! ( $GLOBALS['wp_secrets_provider'] instanceof WP_Secrets_Provider ) ) { 668 $provider = new WP_Secrets_Broken_Provider(); 669 670 return $provider; 671 } 672 673 $provider = $GLOBALS['wp_secrets_provider']; 674 675 return $provider; 676 } 677 678 $provider = new WP_Secrets_Libsodium_Provider( 679 _wp_secrets_get_store(), 680 _wp_secrets_get_key_manager() 681 ); 682 683 return $provider; 684 } 685 686 /** 687 * Whether the active provider accepts writes. 688 * 689 * Exists so a settings screen can disable its own save control before an operator 690 * types a credential into a field that will only reject it. A provider whose 691 * credentials are managed by host tooling or a control panel reports false. 692 * 693 * @since 7.2.0 694 * 695 * @return bool 696 */ 697 function wp_secrets_provider_is_writable(): bool { 698 return _wp_secrets_get_provider()->is_writable(); 699 } 700 701 /** 702 * A human-readable description of what is protecting this site's secrets. 703 * 704 * For Site Health and a future admin screen. Never key material, never a value. 705 * 706 * @since 7.2.0 707 * 708 * @return string 709 */ 710 function wp_secrets_provider_label(): string { 711 return _wp_secrets_get_provider()->get_label(); 712 } 713 714 /** 715 * Shared implementation behind wp_set_secret() and wp_set_network_secret(). 716 * 717 * @since 7.2.0 718 * 719 * @param string $name The secret's name. 720 * @param string $value The plaintext value. 721 * @param bool $network Whether this is a network-scope secret. 722 * @param bool $needs_rotation Flag the stored secret as needing rotation. 723 * @param string|null $action_override Overrides the $action reported to 724 * wp_secret_changed. 725 * @return true|WP_Error 726 */ 727 function _wp_secrets_set( string $name, $value, bool $network, bool $needs_rotation = false, ?string $action_override = null ) { 728 return _wp_secrets_get_provider()->set( $name, $value, $network, $needs_rotation, $action_override ); 729 } 730 731 /** 732 * Shared implementation behind wp_get_secret() and wp_get_network_secret(). 733 * 734 * @since 7.2.0 735 * 736 * @param string $name The secret's name. 737 * @param string $version A WP_Secret_Version constant. 738 * @param bool $network Whether this is a network-scope secret. 739 * @return WP_Secret|null|WP_Error 740 * 741 * @phpstan-param WP_Secret_Version::CURRENT|WP_Secret_Version::PREVIOUS $version 742 */ 743 function _wp_secrets_get( string $name, string $version, bool $network ) { 744 /* 745 * Checked here rather than inside the provider: this is a contract between 746 * the API and its caller, and asking every provider to re-validate a core 747 * constant would be duplicated work that a third-party provider could 748 * silently skip. 749 */ 750 if ( ! in_array( $version, array( WP_Secret_Version::CURRENT, WP_Secret_Version::PREVIOUS ), true ) ) { 751 _doing_it_wrong( 752 __FUNCTION__, 753 __( 'The version must be WP_Secret_Version::CURRENT or WP_Secret_Version::PREVIOUS.' ), 754 '7.2.0' 755 ); 756 757 return new WP_Error( 758 WP_SECRETS_ERROR_INVALID_ARGUMENT, 759 __( 'The version must be WP_Secret_Version::CURRENT or WP_Secret_Version::PREVIOUS.' ) 760 ); 761 } 762 763 return _wp_secrets_get_provider()->get( $name, $version, $network ); 764 } 765 766 /** 767 * Shared implementation behind wp_delete_secret() and wp_delete_network_secret(). 768 * 769 * @since 7.2.0 770 * 771 * @param string $name The secret's name. 772 * @param bool $network Whether this is a network-scope secret. 773 * @return true|WP_Error 774 */ 775 function _wp_secrets_delete( string $name, bool $network ) { 776 return _wp_secrets_get_provider()->delete( $name, $network ); 777 } 778 779 /** 780 * Shared implementation behind wp_retire_secret_version() and its network twin. 781 * 782 * @since 7.2.0 783 * 784 * @param string $name The secret's name. 785 * @param bool $network Whether this is a network-scope secret. 786 * @return true|WP_Error 787 */ 788 function _wp_secrets_retire( string $name, bool $network ) { 789 return _wp_secrets_get_provider()->retire_previous( $name, $network ); 790 } 791 792 /** 793 * Shared implementation behind wp_list_secrets() and wp_list_network_secrets(). 794 * 795 * @since 7.2.0 796 * 797 * @param string $name_prefix Restrict to names beginning with this prefix. 798 * @param bool $network Whether to list network-scope secrets. 799 * @return array|WP_Error 800 * 801 * @phpstan-return list<array{name: string, fingerprint: string, created: int, has_previous: bool, needs_rotation: bool}>|WP_Error 802 */ 803 function _wp_secrets_list( string $name_prefix, bool $network ) { 804 return _wp_secrets_get_provider()->list_secrets( $name_prefix, $network ); 805 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
| Generated : Wed Oct 14 08:20:33 2026 | Cross-referenced by PHPXref |