PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / trunk
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar vtrunk
3.3.1 3.3.0 3.2.14 3.2.13 3.2.12 3.2.11 3.2.10 3.2.9 3.2.8 3.2.7 trunk 0.2.5.5 0.2.5.6 0.2.5.7 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.2.0 1.2.1 All 156 releases
notificationx / includes / MCP / Pairing.php

Pairing.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar trunk, at includes/MCP/Pairing.php

178 lines 4.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Static pairing-token connection for the NotificationX MCP server.
4 *
5 * This is the simplest way to connect a client that accepts a bearer token
6 * (ChatGPT, Cursor, custom clients): the admin enables MCP, a long random
7 * token is minted, and the client sends it as `Authorization: Bearer <token>`.
8 * The token is bound to the admin who created it — every ability runs as that
9 * user, and if they lose `manage_options` the connection stops working.
10 *
11 * OAuth 2.1 ({@see OAuth}) is the alternative for one-click clients like Claude.
12 *
13 * @package NotificationX\MCP
14 */
15
16 namespace NotificationX\MCP;
17
18 use NotificationX\GetInstance;
19
20 if ( ! defined( 'ABSPATH' ) ) {
21 exit;
22 }
23
24 /**
25 * @method static Pairing get_instance( $args = null )
26 */
27 class Pairing {
28
29 use GetInstance;
30
31 const OPTION = 'notificationx_mcp_pairing';
32
33 /**
34 * The stored pairing state.
35 *
36 * @return array
37 */
38 public function state() {
39 $state = get_option( self::OPTION, array() );
40 return is_array( $state ) ? $state : array();
41 }
42
43 /**
44 * Whether a pairing token currently exists.
45 *
46 * @return bool
47 */
48 public function is_connected() {
49 $state = $this->state();
50 return ! empty( $state['connected'] ) && ! empty( $state['site_token'] );
51 }
52
53 /**
54 * The current pairing token (empty string if not connected).
55 *
56 * @return string
57 */
58 public function site_token() {
59 $state = $this->state();
60 return isset( $state['site_token'] ) ? (string) $state['site_token'] : '';
61 }
62
63 /**
64 * The scopes granted to the pairing token.
65 *
66 * @return string[]
67 */
68 public function scopes() {
69 $state = $this->state();
70 return isset( $state['scopes'] ) && is_array( $state['scopes'] ) ? $state['scopes'] : array( 'read', 'write' );
71 }
72
73 /**
74 * Whether the pairing token is read-only.
75 *
76 * @return bool
77 */
78 public function is_read_only() {
79 $scopes = $this->scopes();
80 return ! in_array( 'write', $scopes, true );
81 }
82
83 /**
84 * Create the pairing token if it does not already exist. Idempotent.
85 *
86 * @param array $scopes Scopes to grant (defaults to read + write).
87 * @return array The pairing state.
88 */
89 public function connect( $scopes = array( 'read', 'write' ) ) {
90 $state = $this->state();
91 if ( empty( $state['site_token'] ) ) {
92 $state = array(
93 'site_token' => $this->generate_token(),
94 'connected' => true,
95 'connected_at' => time(),
96 'scopes' => $scopes,
97 'user_id' => get_current_user_id(),
98 'last_used' => 0,
99 );
100 update_option( self::OPTION, $state, false );
101 }
102 return $state;
103 }
104
105 /**
106 * Replace the pairing token with a fresh one (invalidates the old token).
107 *
108 * @return array The new pairing state.
109 */
110 public function rotate() {
111 $state = $this->state();
112 $state['site_token'] = $this->generate_token();
113 $state['connected'] = true;
114 $state['connected_at'] = time();
115 $state['user_id'] = get_current_user_id();
116 if ( empty( $state['scopes'] ) ) {
117 $state['scopes'] = array( 'read', 'write' );
118 }
119 update_option( self::OPTION, $state, false );
120 return $state;
121 }
122
123 /**
124 * Remove the pairing token entirely.
125 *
126 * @return void
127 */
128 public function disconnect() {
129 delete_option( self::OPTION );
130 }
131
132 /**
133 * Note the last time the token was used (for the admin UI).
134 *
135 * @return void
136 */
137 public function touch_last_used() {
138 $state = $this->state();
139 if ( ! empty( $state ) ) {
140 $state['last_used'] = time();
141 update_option( self::OPTION, $state, false );
142 }
143 }
144
145 /**
146 * Constant-time comparison of a presented token against the stored one.
147 *
148 * @param string $token Presented bearer token.
149 * @return bool
150 */
151 public function verify( $token ) {
152 $stored = $this->site_token();
153 if ( '' === $stored || '' === (string) $token ) {
154 return false;
155 }
156 return hash_equals( $stored, (string) $token );
157 }
158
159 /**
160 * The WordPress user id the token is bound to.
161 *
162 * @return int
163 */
164 public function user_id() {
165 $state = $this->state();
166 return isset( $state['user_id'] ) ? (int) $state['user_id'] : 0;
167 }
168
169 /**
170 * Generate a 64-char random token.
171 *
172 * @return string
173 */
174 protected function generate_token() {
175 return bin2hex( random_bytes( 32 ) );
176 }
177 }
178