PluginProbe
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar / 3.2.11
NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar v3.2.11
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 / Core / Rest / Integration.php

Integration.php in NotificationX – FOMO, Live Sales Notification, WooCommerce Sales Popup, GDPR, Social Proof, Announcement Banner & Floating Notification Bar 3.2.11, at includes/Core/Rest/Integration.php

370 lines 14.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace NotificationX\Core\Rest;
4
5 use NotificationX\Core\PostType;
6 use NotificationX\Core\REST;
7 use NotificationX\Extensions\ExtensionFactory;
8 use NotificationX\Extensions\GlobalFields;
9 use NotificationX\GetInstance;
10 use NotificationX\NotificationX;
11 use WP_REST_Controller;
12 use WP_REST_Response;
13 use WP_REST_Server;
14 use WP_Error;
15
16 /**
17 * @method static Integration get_instance($args = null)
18 */
19 class Integration {
20 /**
21 * Instance of NotificationX
22 *
23 * @var NotificationX
24 */
25 use GetInstance;
26
27 public $namespace;
28 public $rest_base;
29
30 /**
31 * Grace window during which the legacy md5(home_url()) key is still accepted
32 * after upgrade, so existing Zapier zaps keep firing while customers rotate.
33 */
34 const LEGACY_KEY_GRACE_DAYS = 14;
35
36 const OPT_API_KEY = 'nx_integration_api_key';
37 const OPT_GRACE_STARTED_AT = 'nx_integration_api_key_grace_started_at';
38 const OPT_LEGACY_KEY_LAST_USED = 'nx_integration_legacy_key_last_used_at';
39 const OPT_SEEDED_VERSION = 'nx_integration_key_seeded_version';
40
41 /**
42 * Constructor.
43 *
44 * @since 4.7.0
45 *
46 * @param string $post_type Post type.
47 */
48 public function __construct() {
49 $this->namespace = 'notificationx/v1';
50 $this->rest_base = 'notification';
51 add_action('rest_api_init', [$this, 'register_routes']);
52 add_action('admin_notices', [$this, 'legacy_api_key_notice']);
53 // Runs once per install/upgrade to seed the API key and, only when
54 // needed, open the legacy-key grace window. Gated by a version marker
55 // so it never retriggers on the same version.
56 add_action('plugins_loaded', [__CLASS__, 'maybe_seed_from_upgrade'], 20);
57 }
58
59 /**
60 * Registers the routes for the objects of the controller.
61 *
62 * @since 4.7.0
63 *
64 * @see register_rest_route()
65 */
66 public function register_routes() {
67 // Settings Integration
68 register_rest_route( $this->namespace, '/api-connect', array(
69 'methods' => WP_REST_Server::EDITABLE,
70 'callback' => array( $this, 'api_connect' ),
71 'permission_callback' => array($this, 'settings_permission'),
72 ));
73
74 // calls from integration provider.
75 register_rest_route(
76 $this->namespace,
77 '/' . $this->rest_base . '/(?P<id>[\d]+)',
78 array(
79 array(
80 'methods' => WP_REST_Server::READABLE,
81 'callback' => array($this, 'get_response'),
82 'permission_callback' => '__return_true',
83 'args' => array(
84 'id' => array(
85 'required' => true,
86 'description' => __('Unique identifier for the object.', 'notificationx'),
87 'type' => 'integer',
88 ),
89 'api_key' => array(
90 'required' => true,
91 'description' => __('Unique identifier for the site.', 'notificationx'),
92 'type' => 'string',
93 ),
94 ),
95 ),
96 array(
97 'methods' => WP_REST_Server::CREATABLE,
98 'callback' => array($this, 'save_response'),
99 'permission_callback' => '__return_true',
100 'args' => array(
101 'id' => array(
102 'required' => true,
103 'description' => __('Unique identifier for the object.', 'notificationx'),
104 'type' => 'integer',
105 ),
106 'api_key' => array(
107 'required' => true,
108 'description' => __('Unique identifier for the site.', 'notificationx'),
109 'type' => 'string',
110 ),
111 ),
112 ),
113 )
114 );
115 // OLD Fallback for Zapier
116 register_rest_route(
117 "notificationx",
118 '/' . $this->rest_base . '/(?P<id>[\d]+)',
119 array(
120 array(
121 'methods' => WP_REST_Server::READABLE,
122 'callback' => array($this, 'get_response'),
123 'permission_callback' => '__return_true',
124 'args' => array(
125 'id' => array(
126 'required' => true,
127 'description' => __('Unique identifier for the object.', 'notificationx'),
128 'type' => 'integer',
129 ),
130 'api_key' => array(
131 'required' => true,
132 'description' => __('Unique identifier for the site.', 'notificationx'),
133 'type' => 'string',
134 ),
135 ),
136 ),
137 array(
138 'methods' => WP_REST_Server::CREATABLE,
139 'callback' => array($this, 'save_response'),
140 'permission_callback' => '__return_true',
141 'args' => array(
142 'id' => array(
143 'required' => true,
144 'description' => __('Unique identifier for the object.', 'notificationx'),
145 'type' => 'integer',
146 ),
147 'api_key' => array(
148 'required' => true,
149 'description' => __('Unique identifier for the site.', 'notificationx'),
150 'type' => 'string',
151 ),
152 ),
153 ),
154 )
155 );
156 }
157
158 /**
159 * Returns the site's integration API key, generating and persisting one if it doesn't exist yet.
160 * Key generation has no side effects on the legacy-key grace window — that is opened only by
161 * {@see self::maybe_seed_from_upgrade()} at plugin bootstrap, so an unauthenticated attacker
162 * probing the endpoint can never lazy-open a grace window.
163 */
164 public static function get_api_key() {
165 $key = get_option( self::OPT_API_KEY );
166 if ( empty( $key ) ) {
167 $key = wp_generate_password( 32, false );
168 update_option( self::OPT_API_KEY, $key, false );
169 }
170 return $key;
171 }
172
173 /**
174 * Runs once per plugin version at bootstrap. Ensures the API key exists and opens the
175 * legacy-key grace window ONLY when the site actually had legacy integrations configured
176 * before the upgrade — never on a fresh install and never on customer sites that never
177 * used Zapier/IFTTT. Gated by a version marker so it does not retrigger.
178 */
179 public static function maybe_seed_from_upgrade() {
180 $seeded = get_option( self::OPT_SEEDED_VERSION );
181 if ( $seeded === NOTIFICATIONX_VERSION ) {
182 return;
183 }
184 update_option( self::OPT_SEEDED_VERSION, NOTIFICATIONX_VERSION, false );
185
186 // Ensure the new key exists (idempotent — no grace side effect).
187 self::get_api_key();
188
189 // Open the grace window only for sites that had legacy integrations.
190 if ( ! get_option( self::OPT_GRACE_STARTED_AT ) && self::has_legacy_integrations() ) {
191 update_option( self::OPT_GRACE_STARTED_AT, time(), false );
192 }
193 }
194
195 /**
196 * Detects whether the site has (or had) legacy Zapier/IFTTT integrations that would have
197 * been configured with the legacy md5(home_url()) key. Used to decide whether the upgrade
198 * seeder should open a grace window.
199 */
200 protected static function has_legacy_integrations(): bool {
201 global $wpdb;
202 $table = $wpdb->prefix . 'nx_posts';
203 $count = $wpdb->get_var(
204 "SELECT COUNT(*) FROM {$table}
205 WHERE source LIKE 'zapier%%'
206 OR source LIKE 'ifttt%%'"
207 );
208 return (int) $count > 0;
209 }
210
211 /**
212 * Unix timestamp at which the legacy md5(home_url()) key stops being accepted.
213 */
214 public static function legacy_key_grace_ends_at(): int {
215 $started = (int) get_option( self::OPT_GRACE_STARTED_AT );
216 if ( ! $started ) {
217 return 0;
218 }
219 return $started + ( self::LEGACY_KEY_GRACE_DAYS * DAY_IN_SECONDS );
220 }
221
222 /**
223 * Validates an incoming API key.
224 * The new random key is always accepted. The legacy md5(home_url()) key is accepted only
225 * during the post-upgrade grace window; we record every accepted legacy use so the admin
226 * notice can prompt the customer to rotate. After the grace window closes, the legacy key
227 * is rejected. Wrong keys never write to the DB — so a probing attacker can never open a
228 * grace window or flip the admin notice on.
229 */
230 public static function is_valid_api_key( string $api_key ): bool {
231 $stored = (string) get_option( self::OPT_API_KEY );
232 if ( $stored !== '' && hash_equals( $stored, $api_key ) ) {
233 return true;
234 }
235 $is_legacy = hash_equals( md5( home_url( '', 'http' ) ), $api_key )
236 || hash_equals( md5( home_url( '', 'https' ) ), $api_key );
237 if ( ! $is_legacy ) {
238 return false;
239 }
240 $grace_ends_at = self::legacy_key_grace_ends_at();
241 if ( $grace_ends_at === 0 || time() >= $grace_ends_at ) {
242 return false;
243 }
244 update_option( self::OPT_LEGACY_KEY_LAST_USED, time(), false );
245 return true;
246 }
247
248 /**
249 * Persistent dashboard notice — surfaced whenever the legacy key has been used recently
250 * so the customer can rotate before (or after) the grace window closes.
251 */
252 public function legacy_api_key_notice() {
253 if ( ! current_user_can( 'edit_notificationx_settings' ) ) {
254 return;
255 }
256 $last_used = (int) get_option( self::OPT_LEGACY_KEY_LAST_USED );
257 if ( ! $last_used ) {
258 return;
259 }
260 $grace_ends_at = self::legacy_key_grace_ends_at();
261 $settings_url = admin_url( 'admin.php?page=nx-settings' );
262 if ( $grace_ends_at > 0 && time() < $grace_ends_at ) {
263 $deadline = wp_date( get_option( 'date_format' ), $grace_ends_at );
264 $message = sprintf(
265 /* translators: %s: rotation deadline date. */
266 __( 'A Zapier (or other webhook) integration is still calling NotificationX with the legacy API key. Rotate it to the new key before %s — after that, requests using the old key will be rejected.', 'notificationx' ),
267 '<strong>' . esc_html( $deadline ) . '</strong>'
268 );
269 $class = 'notice notice-warning';
270 } else {
271 $message = __( 'A Zapier (or other webhook) integration is still calling NotificationX with the legacy API key. Those calls are now being rejected — update the integration with the new key to restore it.', 'notificationx' );
272 $class = 'notice notice-error';
273 }
274 printf(
275 '<div class="%1$s"><p>%2$s <a href="%3$s">%4$s</a></p></div>',
276 esc_attr( $class ),
277 wp_kses_post( $message ),
278 esc_url( $settings_url ),
279 esc_html__( 'Get the new key', 'notificationx' )
280 );
281 }
282
283 public function get_response( \WP_REST_Request $request ){
284 $id = $request['id'];
285 $api_key = $request['api_key'];
286 $error = [];
287
288 if( self::is_valid_api_key( (string) $api_key ) ) {
289 $notificationx = PostType::get_instance()->get_post( $id );
290 if( $notificationx ) {
291 return wp_send_json( true );
292 }
293 $error['message'] = sprintf( __( 'There is no notification created with this id: %s', 'notificationx' ), $id );
294 return wp_send_json_error( $error, 401 );
295 } else {
296 $error['message'] = __( 'Error: API Key Invalid!', 'notificationx' );
297 return wp_send_json_error( $error, 401 );
298 }
299 }
300
301 /**
302 * Undocumented function
303 *
304 * @param \WP_REST_Request $request
305 * @return void
306 */
307 public function save_response( \WP_REST_Request $request ){
308 $response_data = array(
309 'data' => '',
310 'error' => false
311 );
312
313 if ( ! isset( $request['api_key'] ) ) {
314 $response_data['error'] = __('Error: You should provide an API key.', 'notificationx');
315 } else {
316 if ( ! self::is_valid_api_key( (string) $request['api_key'] ) ) {
317 $response_data['error'] = __('Error: Invalid API key.', 'notificationx');
318 }
319 }
320
321 if ( ! $response_data['error'] ) {
322 $response_data['data'] = $request->get_params();
323 if ( isset( $response_data['data']['api_key'] ) ) {
324 unset( $response_data['data']['api_key'] );
325 }
326 array_walk_recursive( $response_data['data'], function( &$val ) {
327 $val = sanitize_text_field( (string) $val );
328 } );
329 if (isset($response_data['data']['id'])){
330 $post = PostType::get_instance()->get_post($response_data['data']['id']);
331 if($post['source']){
332 do_action( "nx_api_response_success_{$post['source']}", $response_data['data'] );
333 }
334 }
335 do_action( 'nx_api_response_success', $response_data['data'] );
336 }
337
338 return apply_filters( 'nx_api_response', $response_data );
339 }
340
341 /**
342 * Undocumented function
343 *
344 * @param \WP_REST_Request $request
345 * @return
346 */
347 public function api_connect( \WP_REST_Request $request ){
348 $params = $request->get_params();
349 $source = !empty($params['source']) ? $params['source'] : '';
350 /**
351 * @var Extension
352 */
353 $ext = ExtensionFactory::get_instance()->get($source);
354 if($ext && method_exists($ext, 'connect')){
355 return $ext->connect($params);
356 }
357 else{
358 $result = apply_filters("nx_api_connect_$source", null, $params);
359 if($result){
360 return $result;
361 }
362 }
363 return REST::get_instance()->error();
364 }
365
366 public function settings_permission( $request ) {
367 return current_user_can('edit_notificationx_settings');
368 }
369 }
370