Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
2.27% |
2 / 88 |
|
20.00% |
1 / 5 |
CRAP | |
0.00% |
0 / 1 |
DB_Data_Store | |
2.27% |
2 / 88 |
|
20.00% |
1 / 5 |
170.74 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
create_db | |
0.00% |
0 / 12 |
|
0.00% |
0 / 1 |
6 | |||
save | |
0.00% |
0 / 37 |
|
0.00% |
0 / 1 |
12 | |||
get_value_for_code | |
0.00% |
0 / 29 |
|
0.00% |
0 / 1 |
30 | |||
delete_expired_codes | |
0.00% |
0 / 8 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | /** |
3 | * Uses a custom db table (as distinct from transients) to store the autologin codes. |
4 | * |
5 | * @link https://BrianHenry.ie |
6 | * @since 1.2.1 |
7 | * |
8 | * @package brianhenryie/bh-wp-autologin-urls |
9 | */ |
10 | |
11 | namespace BrianHenryIE\WP_Autologin_URLs\API\Data_Stores; |
12 | |
13 | use BrianHenryIE\WP_Autologin_URLs\API\Data_Store_Interface; |
14 | use DateInterval; |
15 | use DateTime; |
16 | use DateTimeImmutable; |
17 | use DateTimeInterface; |
18 | use DateTimeZone; |
19 | use Exception; |
20 | use Psr\Log\LoggerInterface; |
21 | use Psr\Log\LoggerAwareTrait; |
22 | |
23 | /** |
24 | * Creates a custom database table via standard $wpdb functions to store and retrieve the autologin codes. |
25 | * |
26 | * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery |
27 | */ |
28 | class DB_Data_Store implements Data_Store_Interface { |
29 | use LoggerAwareTrait; |
30 | |
31 | /** |
32 | * Constructor. |
33 | * |
34 | * @param LoggerInterface $logger A PSR logger. |
35 | */ |
36 | public function __construct( LoggerInterface $logger ) { |
37 | $this->setLogger( $logger ); |
38 | |
39 | add_action( 'plugins_loaded', array( $this, 'create_db' ), 1 ); |
40 | } |
41 | |
42 | /** |
43 | * The current plugin database version. |
44 | * |
45 | * @var int |
46 | */ |
47 | protected static $db_version = 1; |
48 | |
49 | /** |
50 | * Option name in wp_options for checking the database version on install/upgrades. |
51 | * |
52 | * @var string |
53 | */ |
54 | public static $db_version_option_name = 'bh_wp_autologin_urls_db_version'; |
55 | |
56 | /** |
57 | * Create or upgrade the database. |
58 | * |
59 | * Check the saved wp_option for the last time the database was created and update accordingly. |
60 | * |
61 | * @hooked plugins_loaded |
62 | */ |
63 | public function create_db(): void { |
64 | |
65 | $current_db_version = get_option( self::$db_version_option_name, 0 ); |
66 | |
67 | if ( ! ( self::$db_version > $current_db_version ) ) { |
68 | return; |
69 | } |
70 | |
71 | global $wpdb; |
72 | |
73 | $table_name = $wpdb->prefix . 'autologin_urls'; |
74 | $charset_collate = $wpdb->get_charset_collate(); |
75 | |
76 | $sql = "CREATE TABLE {$table_name} ( |
77 | expires_at datetime DEFAULT '0000-00-00 00:00:00' NOT NULL, |
78 | hash varchar(64) NOT NULL, |
79 | userhash varchar(64) NOT NULL, |
80 | PRIMARY KEY (hash), |
81 | KEY expires_at (expires_at) |
82 | ) {$charset_collate};"; |
83 | |
84 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; |
85 | $result = dbDelta( $sql ); |
86 | |
87 | update_option( self::$db_version_option_name, self::$db_version ); |
88 | |
89 | $this->logger->info( 'Updated database to version ' . self::$db_version ); |
90 | } |
91 | |
92 | /** |
93 | * Save an autologin code for a user so it can be checked in future, before the expires_in time. |
94 | * |
95 | * @param int $user_id The user id the code is being saved for. |
96 | * @param string $code The autologin code being used in the user's URL. |
97 | * @param int $expires_in Number of seconds the code is valid for. |
98 | * |
99 | * @throws Exception DateTime exception. |
100 | * @throws Exception For `$wpdb->last_error`. |
101 | */ |
102 | public function save( int $user_id, string $code, int $expires_in ): void { |
103 | |
104 | $key = hash( 'sha256', $code ); |
105 | |
106 | // Concatenate $user_id and $password so the database cannot be searched by username. |
107 | $value = hash( 'sha256', $user_id . $code ); |
108 | |
109 | global $wpdb; |
110 | |
111 | $datetime = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); |
112 | $datetime->add( new DateInterval( "PT{$expires_in}S" ) ); |
113 | $expires_at = $datetime->format( 'Y-m-d H:i:s' ); |
114 | |
115 | $result = $wpdb->insert( |
116 | $wpdb->prefix . 'autologin_urls', |
117 | array( |
118 | 'expires_at' => $expires_at, |
119 | 'hash' => $key, |
120 | 'userhash' => $value, |
121 | ) |
122 | ); |
123 | |
124 | if ( ! empty( $wpdb->last_error ) ) { |
125 | $this->logger->error( $wpdb->last_error ); |
126 | throw new Exception( $wpdb->last_error ); |
127 | } |
128 | |
129 | if ( false === $result ) { |
130 | $this->logger->error( |
131 | 'Error saving autologin code for wp_user:' . $user_id, |
132 | array( |
133 | 'user_id' => $user_id, |
134 | 'code' => $code, |
135 | 'expires_in' => $expires_in, |
136 | 'expires_at' => $expires_at, |
137 | 'hash' => $key, |
138 | 'userhash' => $value, |
139 | ) |
140 | ); |
141 | return; |
142 | } |
143 | |
144 | $this->logger->debug( |
145 | 'Saved autologin code for wp_user:' . $user_id, |
146 | array( |
147 | 'user_id' => $user_id, |
148 | 'expires' => $expires_at, |
149 | ) |
150 | ); |
151 | } |
152 | |
153 | /** |
154 | * Retrieve the value stored for the given autologin code, if it has not expired. |
155 | * |
156 | * @param string $code The autologin code in the user's URL. |
157 | * @param bool $delete Delete the code after fetching it. I.e. this is a single use code. |
158 | * |
159 | * @return string|null |
160 | * @throws Exception DateTime exception. |
161 | * @throws Exception For `$wpdb->last_error`. |
162 | */ |
163 | public function get_value_for_code( string $code, bool $delete = true ): ?string { |
164 | |
165 | global $wpdb; |
166 | |
167 | $key = hash( 'sha256', $code ); |
168 | |
169 | /** |
170 | * We've no interest in caching, rather, for security, we're deleting the entry as soon as it's found. |
171 | * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching |
172 | */ |
173 | $result = $wpdb->get_row( |
174 | $wpdb->prepare( 'SELECT expires_at, userhash FROM ' . $wpdb->prefix . 'autologin_urls WHERE hash = %s', $key ) |
175 | ); |
176 | |
177 | if ( ! empty( $wpdb->last_error ) ) { |
178 | $this->logger->error( $wpdb->last_error ); |
179 | throw new Exception( $wpdb->last_error ); |
180 | } |
181 | |
182 | if ( is_null( $result ) ) { |
183 | $this->logger->debug( 'Code not found.' ); |
184 | return null; |
185 | } |
186 | |
187 | // Delete the code so it can only be used once (whether valid or not). |
188 | if ( $delete ) { |
189 | $wpdb->delete( |
190 | $wpdb->prefix . 'autologin_urls', |
191 | array( 'hash' => $key ) |
192 | ); |
193 | } |
194 | |
195 | $expires_at = new DateTimeImmutable( $result->expires_at, new DateTimeZone( 'UTC' ) ); |
196 | |
197 | $now = new DateTimeImmutable( 'now', new DateTimeZone( 'UTC' ) ); |
198 | |
199 | if ( $now > $expires_at ) { |
200 | $this->logger->debug( |
201 | 'Valid code but already expired', |
202 | array( |
203 | 'code' => $code, |
204 | 'expires_at' => $expires_at, |
205 | 'hash' => $key, |
206 | ) |
207 | ); |
208 | return null; |
209 | } |
210 | |
211 | return $result->userhash; |
212 | } |
213 | |
214 | /** |
215 | * Delete codes that are no longer valid. |
216 | * |
217 | * @param DateTimeInterface $before The date from which to purge old codes. |
218 | * |
219 | * @return array{deleted_count:int|null} |
220 | * @throws Exception For `$wpdb->last_error`. |
221 | */ |
222 | public function delete_expired_codes( DateTimeInterface $before ): array { |
223 | |
224 | // get current datetime in mysql format. |
225 | $mysql_formatted_date = $before->format( 'Y-m-d H:i:s' ); |
226 | |
227 | global $wpdb; |
228 | $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'autologin_urls WHERE expires_at < %s', $mysql_formatted_date ) ); |
229 | |
230 | if ( ! empty( $wpdb->last_error ) ) { |
231 | $this->logger->error( $wpdb->last_error ); |
232 | throw new Exception( $wpdb->last_error ); |
233 | } |
234 | |
235 | // I think this is the number of entries deleted. |
236 | $this->logger->info( 'Delete expired codes wpdb result: ' . $result, array( 'result' => $result ) ); |
237 | |
238 | return array( 'deleted_count' => intval( $result ) ); |
239 | } |
240 | } |