| @@ -9,20 +9,8 @@ | ||
| 9 | 9 | use function get_current_user_id; |
| 10 | 10 | |
| 11 | 11 | class Options extends Base { |
| 12 | 12 | /** |
| 13 | - * Current User Id | |
| 14 | - * @var integer | |
| 15 | - */ | |
| 16 | - private $current_user; | |
| 17 | - | |
| 18 | - /** | |
| 19 | - * User has any api? | |
| 20 | - * @var boolean | |
| 21 | - */ | |
| 22 | - private $has_api; | |
| 23 | - | |
| 24 | - /** | |
| 25 | 13 | * Pin every derived read/write to the acting user, bypassing the |
| 26 | 14 | * global-login fallback in user_id(). |
| 27 | 15 | * |
| 28 | 16 | * @var bool |
| @@ -29,33 +17,38 @@ | ||
| 29 | 17 | */ |
| 30 | 18 | private $force_current_user = false; |
| 31 | 19 | |
| 32 | 20 | /** |
| 33 | - * Automatically invoked and set up the properties. | |
| 34 | - */ | |
| 35 | - public function __construct(){ | |
| 36 | - $this->current_user = get_current_user_id(); | |
| 37 | - $this->has_api = ! empty( $this->get( 'api_key', '', $this->current_user ) ); | |
| 38 | - } | |
| 39 | - | |
| 40 | - /** | |
| 41 | 21 | * Get the current user ID. |
| 42 | 22 | * |
| 43 | - * `get_current_user_id()` can still be 0 when this singleton is built — a | |
| 44 | - * wp-cli run before `wp_set_current_user()`, or a request whose auth has not | |
| 45 | - * been resolved yet. Caching that 0 for the rest of the request made every | |
| 46 | - * derived write bail in `can_write()`, so a connect could answer "Site | |
| 47 | - * connected successfully" and persist nothing. Re-read while it is still | |
| 48 | - * unknown; a resolved id is never overwritten. | |
| 23 | + * Resolved live on every call rather than cached at construction time: | |
| 24 | + * `Options` is a request-lifetime singleton (`Base::get_instance()`), and | |
| 25 | + * something in Templately's own bootstrap (e.g. `Admin`/`Settings`) | |
| 26 | + * constructs it during `plugins_loaded` — before WordPress resolves the | |
| 27 | + * REST Application-Password current user (which only happens later, when | |
| 28 | + * the REST server actually dispatches the route). Caching ` | |
| 29 | + * get_current_user_id()` at construction froze it at `0` for the rest of | |
| 30 | + * the request on any headless (non-cookie) REST/MCP call, so every later | |
| 31 | + * `Options` read looked up user `0`'s meta instead of the real acting | |
| 32 | + * user's — reporting "session expired" even for a genuinely connected | |
| 33 | + * account. `get_current_user_id()` is itself cheap (it just reads WP | |
| 34 | + * core's own already-resolved `$current_user` global), so there is no | |
| 35 | + * reason to cache it a second time here. | |
| 49 | 36 | * |
| 50 | 37 | * @return int |
| 51 | 38 | */ |
| 52 | 39 | public function current_user_id(): int { |
| 53 | - if ( $this->current_user <= 0 ) { | |
| 54 | - $this->current_user = get_current_user_id(); | |
| 55 | - } | |
| 40 | + return get_current_user_id(); | |
| 41 | + } | |
| 56 | 42 | |
| 57 | - return $this->current_user; | |
| 43 | + /** | |
| 44 | + * Whether the current user has an API key set — computed live for the | |
| 45 | + * same reason as {@see current_user_id()}, not cached. | |
| 46 | + * | |
| 47 | + * @return boolean | |
| 48 | + */ | |
| 49 | + private function has_api(): bool { | |
| 50 | + return ! empty( $this->get( 'api_key', '', $this->current_user_id() ) ); | |
| 58 | 51 | } |
| 59 | 52 | |
| 60 | 53 | /** |
| 61 | 54 | * Can a user link another templately account in a setup?. |
| @@ -61,13 +54,13 @@ | ||
| 61 | 54 | * Can a user link another templately account in a setup?. |
| 62 | 55 | * @return boolean |
| 63 | 56 | */ |
| 64 | 57 | public function link_account(): bool { |
| 65 | - return $this->who_am_i() === 'link' && ! $this->has_api; | |
| 58 | + return $this->who_am_i() === 'link' && ! $this->has_api(); | |
| 66 | 59 | } |
| 67 | 60 | |
| 68 | 61 | public function unlink_account(): bool { |
| 69 | - return ( $this->who_am_i() === 'link' || $this->who_am_i() === 'local' ) && $this->has_api; | |
| 62 | + return ( $this->who_am_i() === 'link' || $this->who_am_i() === 'local' ) && $this->has_api(); | |
| 70 | 63 | } |
| 71 | 64 | |
| 72 | 65 | /** |
| 73 | 66 | * Get determined who am I. |
| @@ -74,14 +67,15 @@ | ||
| 74 | 67 | * @return string |
| 75 | 68 | */ |
| 76 | 69 | public function who_am_i(): string { |
| 77 | 70 | $_who_am_i = 'local'; |
| 71 | + $current_user = $this->current_user_id(); | |
| 78 | 72 | |
| 79 | - if( $this->is_global() > 0 && $this->is_global() === $this->current_user_id() ) { | |
| 73 | + if( $this->is_global() > 0 && $this->is_global() === $current_user ) { | |
| 80 | 74 | $_who_am_i = 'global'; |
| 81 | 75 | } |
| 82 | 76 | |
| 83 | - if( $this->is_global() > 0 && $this->is_global() !== $this->current_user_id() ) { | |
| 77 | + if( $this->is_global() > 0 && $this->is_global() !== $current_user ) { | |
| 84 | 78 | $_who_am_i = 'link'; |
| 85 | 79 | } |
| 86 | 80 | |
| 87 | 81 | if( $this->is_global() == 0 ) { |
| @@ -114,15 +108,23 @@ | ||
| 114 | 108 | |
| 115 | 109 | $_who_am_i = $this->who_am_i(); |
| 116 | 110 | |
| 117 | 111 | if( ! empty( $_SERVER['REQUEST_URI'] ) ) { |
| 118 | - $parse_uri = explode( '/', substr( $_SERVER['REQUEST_URI'], 0, strpos( $_SERVER['REQUEST_URI'], '?' ) ) ); | |
| 119 | - if( $_who_am_i === 'link' && array_pop( $parse_uri ) === 'login' ) { | |
| 112 | + // FR-003: in 'link' mode the login-endpoint override targets the | |
| 113 | + // current user so they can store their own credentials. Detect it | |
| 114 | + // with a substring match on '/login' so REST paths such as | |
| 115 | + // `/wp-json/templately/v1/login` are covered — including requests | |
| 116 | + // with NO query string, which the former last-path-segment check | |
| 117 | + // (substr up to '?', which collapses to '' when there is no '?') | |
| 118 | + // never matched. `strpos(...) !== false` keeps the PHP 7.4 floor | |
| 119 | + // (`str_contains()` is PHP 8.0+). | |
| 120 | + $uri = sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ); | |
| 121 | + if( $_who_am_i === 'link' && strpos( $uri, '/login' ) !== false ) { | |
| 120 | 122 | return $this->current_user_id(); |
| 121 | 123 | } |
| 122 | 124 | } |
| 123 | 125 | |
| 124 | - if( $_who_am_i === 'link' && $this->has_api ) { | |
| 126 | + if( $_who_am_i === 'link' && $this->has_api() ) { | |
| 125 | 127 | return $this->current_user_id(); |
| 126 | 128 | } |
| 127 | 129 | |
| 128 | 130 | return $_who_am_i === 'local' ? $this->current_user_id() : $this->is_global(); |
| @@ -170,15 +172,9 @@ | ||
| 170 | 172 | */ |
| 171 | 173 | public function set( $key, $value, $user_id = null ): bool { |
| 172 | 174 | $key = '_templately_' . $key; |
| 173 | 175 | |
| 174 | - $updated = $this->update_user_meta( $user_id, $key, $value ); | |
| 175 | - | |
| 176 | - if( $key === '_templately_api_key' && $updated ) { | |
| 177 | - $this->has_api = true; | |
| 178 | - } | |
| 179 | - | |
| 180 | - return $updated; | |
| 176 | + return $this->update_user_meta( $user_id, $key, $value ); | |
| 181 | 177 | } |
| 182 | 178 | |
| 183 | 179 | /** |
| 184 | 180 | * Get optional user meta or option data |
| @@ -189,9 +185,12 @@ | ||
| 189 | 185 | */ |
| 190 | 186 | public function get( $key, $default = false, $user_id = null ){ |
| 191 | 187 | $key = '_templately_' . $key; |
| 192 | 188 | $_user_meta = $this->get_user_meta( $user_id, $key, true ); |
| 193 | - return ! empty( $_user_meta ) ? $_user_meta : $default; | |
| 189 | + // '' (get_user_meta) and false (get_user_option) are the MISSING sentinels — | |
| 190 | + // but 0, '0' and [] are legitimate stored values and must not collapse to | |
| 191 | + // the default (the same falsy-swallow bug fixed in API::get_param()). | |
| 192 | + return ( '' === $_user_meta || false === $_user_meta ) ? $default : $_user_meta; | |
| 194 | 193 | } |
| 195 | 194 | |
| 196 | 195 | /** |
| 197 | 196 | * Remove options data or user meta |
| @@ -200,13 +199,9 @@ | ||
| 200 | 199 | * @return Options |
| 201 | 200 | */ |
| 202 | 201 | public function remove( string $key ): Options { |
| 203 | 202 | $key = '_templately_' . $key; |
| 204 | - $updated = $this->delete_user_meta( $key ); | |
| 205 | - | |
| 206 | - if( $key === '_templately_api_key' && $updated ) { | |
| 207 | - $this->has_api = false; | |
| 208 | - } | |
| 203 | + $this->delete_user_meta( $key ); | |
| 209 | 204 | |
| 210 | 205 | return $this; |
| 211 | 206 | } |
| 212 | 207 | |