PluginProbe
Yoast SEO – Advanced SEO with real-time guidance and built-in AI / 27.7
Yoast SEO – Advanced SEO with real-time guidance and built-in AI v27.7
28.5 28.4 28.3 28.2 28.1 28.0 27.9 27.8 27.7 27.6 27.5 trunk 18.0 18.1 18.2 18.3 18.4 18.4.1 18.5 18.5.1 18.6 18.7 18.8 18.9 19.0 All 129 releases
wordpress-seo / src / myyoast-client / infrastructure / token / user-token-storage.php

user-token-storage.php in Yoast SEO – Advanced SEO with real-time guidance and built-in AI 27.7, at src/myyoast-client/infrastructure/token/user-token-storage.php

169 lines 5.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 // phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
3
4 namespace Yoast\WP\SEO\MyYoast_Client\Infrastructure\Token;
5
6 use Exception;
7 use Yoast\WP\SEO\Helpers\User_Helper;
8 use Yoast\WP\SEO\MyYoast_Client\Application\Exceptions\Token_Storage_Exception;
9 use Yoast\WP\SEO\MyYoast_Client\Application\Ports\User_Token_Storage_Interface;
10 use Yoast\WP\SEO\MyYoast_Client\Domain\Token_Set;
11 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption;
12 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\Crypto\Encryption_Exception;
13 use Yoast\WP\SEO\MyYoast_Client\Infrastructure\OIDC\Issuer_Config;
14 use YoastSEO_Vendor\Psr\Log\LoggerAwareInterface;
15 use YoastSEO_Vendor\Psr\Log\LoggerAwareTrait;
16 use YoastSEO_Vendor\Psr\Log\NullLogger;
17
18 /**
19 * Stores and retrieves encrypted user-level tokens in wp_usermeta.
20 *
21 * Used for authorization code flow tokens (user-specific).
22 */
23 class User_Token_Storage implements User_Token_Storage_Interface, LoggerAwareInterface {
24 use LoggerAwareTrait;
25
26 private const META_KEY_PREFIX = '_wpseo_myyoast_user_tokens_';
27 private const ENCRYPTION_CONTEXT = 'yoast-myyoast-user-tokens';
28
29 /**
30 * The user helper.
31 *
32 * @var User_Helper
33 */
34 private $user_helper;
35
36 /**
37 * The encryption service.
38 *
39 * @var Encryption
40 */
41 private $encryption;
42
43 /**
44 * The issuer configuration.
45 *
46 * @var Issuer_Config
47 */
48 private $issuer_config;
49
50 /**
51 * User_Token_Storage constructor.
52 *
53 * @param User_Helper $user_helper The user helper.
54 * @param Encryption $encryption The encryption service.
55 * @param Issuer_Config $issuer_config The issuer configuration.
56 */
57 public function __construct( User_Helper $user_helper, Encryption $encryption, Issuer_Config $issuer_config ) {
58 $this->user_helper = $user_helper;
59 $this->encryption = $encryption;
60 $this->issuer_config = $issuer_config;
61 $this->logger = new NullLogger();
62 }
63
64 /**
65 * Returns the issuer-scoped user meta key.
66 *
67 * @return string The meta key.
68 */
69 private function get_meta_key(): string {
70 return self::META_KEY_PREFIX . $this->issuer_config->get_issuer_key();
71 }
72
73 /**
74 * Stores a token set for a user (encrypted).
75 *
76 * @param int $user_id The user ID.
77 * @param Token_Set $token_set The token set to store.
78 *
79 * @return void
80 *
81 * @throws Token_Storage_Exception If encryption fails.
82 */
83 public function store( int $user_id, Token_Set $token_set ): void {
84 try {
85 // phpcs:ignore Yoast.Yoast.JsonEncodeAlternative.Found -- Encoding for encrypted storage, not user-facing output.
86 $json = \wp_json_encode( $token_set->to_array() );
87 if ( $json === false ) {
88 throw new Token_Storage_Exception( 'Failed to JSON-encode token set for storage.' );
89 }
90
91 $encrypted = $this->encryption->encrypt( $json, self::ENCRYPTION_CONTEXT );
92 }
93 catch ( Encryption_Exception $e ) {
94 // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Internal exception message.
95 throw new Token_Storage_Exception( 'Failed to encrypt token set for storage: ' . $e->getMessage(), 0, $e );
96 }
97
98 $this->user_helper->update_meta( $user_id, $this->get_meta_key(), $encrypted );
99 }
100
101 /**
102 * Retrieves the stored token set for a user.
103 *
104 * @param int $user_id The user ID.
105 *
106 * @return Token_Set|null The token set, or null if not stored or decryption fails.
107 */
108 public function get( int $user_id ): ?Token_Set {
109 $stored = $this->user_helper->get_meta( $user_id, $this->get_meta_key(), true );
110 if ( ! \is_string( $stored ) || $stored === '' ) {
111 return null;
112 }
113
114 try {
115 $decrypted = $this->encryption->decrypt( $stored, self::ENCRYPTION_CONTEXT );
116 $data = \json_decode( $decrypted, true, 512, \JSON_THROW_ON_ERROR );
117
118 if ( ! \is_array( $data ) || empty( $data['access_token'] ) ) {
119 return null;
120 }
121
122 return Token_Set::from_array( $data );
123 }
124 catch ( Exception $e ) {
125 $this->logger->error(
126 'Failed to decrypt stored user token for user {user_id}: {error}',
127 [
128 'user_id' => $user_id,
129 'error' => $e->getMessage(),
130 ],
131 );
132 return null;
133 }
134 }
135
136 /**
137 * Deletes the stored token set for a user.
138 *
139 * @param int $user_id The user ID.
140 *
141 * @return void
142 */
143 public function delete( int $user_id ): void {
144 $this->user_helper->delete_meta( $user_id, $this->get_meta_key() );
145 }
146
147 /**
148 * Deletes all stored user token sets across all issuers.
149 * This is used for cleanup on uninstall, as we cannot know which users had tokens stored.
150 * Uses a LIKE match on the meta key prefix to ensure tokens from all issuers are removed.
151 *
152 * @return void
153 */
154 public function delete_all(): void {
155 global $wpdb;
156
157 if ( isset( $wpdb ) ) {
158 // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk cleanup on uninstall.
159 $wpdb->query(
160 $wpdb->prepare(
161 // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key -- Bulk cleanup on uninstall.
162 "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
163 $wpdb->esc_like( self::META_KEY_PREFIX ) . '%',
164 ),
165 );
166 }
167 }
168 }
169