PluginProbe
Elementor Website Builder – more than just a page builder / 3.5.0-dev9
Elementor Website Builder – more than just a page builder v3.5.0-dev9
4.3.0-beta2 4.3.0-beta1 4.2.4 4.2.3 4.2.2 4.2.1 4.2.0 4.1.5 4.2.0-beta2 4.2.0-dev2 4.2.0-beta1 4.1.4 4.1.3 4.1.2 4.1.1 4.1.0 4.1.0-beta3 4.1.0-dev3 4.0.9 4.1.0-beta2 4.1.0-dev2 4.0.8 4.1.0-beta1 4.1.0-dev1 4.0.7 All 451 releases
elementor / core / common / modules / connect / apps / base-app.php

base-app.php in Elementor Website Builder – more than just a page builder 3.5.0-dev9, at core/common/modules/connect/apps/base-app.php

778 lines 17.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Elementor\Core\Common\Modules\Connect\Apps;
3
4 use Elementor\Core\Utils\Http;
5 use Elementor\Core\Utils\Collection;
6 use Elementor\Core\Admin\Admin_Notices;
7 use Elementor\Core\Common\Modules\Connect\Admin;
8 use Elementor\Core\Utils\Str;
9 use Elementor\Plugin;
10 use Elementor\Tracker;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit; // Exit if accessed directly
14 }
15
16 abstract class Base_App {
17
18 const OPTION_NAME_PREFIX = 'elementor_connect_';
19
20 const SITE_URL = 'https://my.elementor.com/connect/v1';
21
22 const API_URL = 'https://my.elementor.com/api/connect/v1';
23
24 const HTTP_RETURN_TYPE_OBJECT = 'object';
25 const HTTP_RETURN_TYPE_ARRAY = 'array';
26
27 protected $data = [];
28
29 protected $auth_mode = '';
30
31 /**
32 * @var Http
33 */
34 protected $http;
35
36 /**
37 * @since 2.3.0
38 * @access protected
39 * @abstract
40 * TODO: make it public.
41 */
42 abstract protected function get_slug();
43
44 /**
45 * @since 2.8.0
46 * @access public
47 * TODO: make it abstract.
48 */
49 public function get_title() {
50 return $this->get_slug();
51 }
52
53 /**
54 * @since 2.3.0
55 * @access protected
56 * @abstract
57 */
58 abstract protected function update_settings();
59
60 /**
61 * @since 2.3.0
62 * @access public
63 * @static
64 */
65 public static function get_class_name() {
66 return get_called_class();
67 }
68
69 /**
70 * @access public
71 * @abstract
72 */
73 public function render_admin_widget() {
74 // PHPCS - the method get_title return a plain string.
75 echo '<h2>' . $this->get_title() . '</h2>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
76
77 if ( $this->is_connected() ) {
78 $remote_user = $this->get( 'user' );
79 $title = sprintf( esc_html__( 'Connected as %s', 'elementor' ), '<strong>' . esc_html( $remote_user->email ) . '</strong>' );
80 $label = esc_html__( 'Disconnect', 'elementor' );
81 $url = $this->get_admin_url( 'disconnect' );
82 $attr = '';
83
84 echo sprintf(
85 '%s <a %s href="%s">%s</a>',
86 // PHPCS - the variable $title is already escaped above.
87 $title, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
88 // PHPCS - the variable $attr is a plain string.
89 $attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
90 esc_attr( $url ),
91 esc_html( $label )
92 );
93 } else {
94 echo 'Not Connected';
95 }
96
97 echo '<hr>';
98
99 $this->print_app_info();
100
101 if ( current_user_can( 'manage_options' ) ) {
102 printf( '<div><a href="%s">%s</a></div>', esc_url( $this->get_admin_url( 'reset' ) ), esc_html__( 'Reset Data', 'elementor' ) );
103 }
104
105 echo '<hr>';
106 }
107
108
109 /**
110 * @since 2.3.0
111 * @access protected
112 */
113 protected function get_option_name() {
114 return static::OPTION_NAME_PREFIX . $this->get_slug();
115 }
116
117 /**
118 * @since 2.3.0
119 * @access public
120 */
121 public function admin_notice() {
122 $notices = $this->get( 'notices' );
123
124 if ( ! $notices ) {
125 return;
126 }
127
128 $this->print_notices( $notices );
129
130 $this->delete( 'notices' );
131 }
132
133
134 public function get_app_token_from_cli_token( $cli_token ) {
135 $response = $this->request( 'get_app_token_from_cli_token', [
136 'cli_token' => $cli_token,
137 ] );
138
139 if ( is_wp_error( $response ) ) {
140 // PHPCS - the variable $response does not contain a user input value.
141 wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
142 }
143
144 // Use state as usual.
145 $_REQUEST['state'] = $this->get( 'state' );
146 $_REQUEST['code'] = $response->code;
147 }
148 /**
149 * @since 2.3.0
150 * @access public
151 */
152 public function action_authorize() {
153 if ( $this->is_connected() ) {
154 $this->add_notice( esc_html__( 'Already connected.', 'elementor' ), 'info' );
155 $this->redirect_to_admin_page();
156 return;
157 }
158
159 $this->set_client_id();
160 $this->set_request_state();
161
162 $this->redirect_to_remote_authorize_url();
163 }
164
165 public function action_reset() {
166 delete_user_option( get_current_user_id(), 'elementor_connect_common_data' );
167
168 if ( current_user_can( 'manage_options' ) ) {
169 delete_option( 'elementor_connect_site_key' );
170 delete_option( 'elementor_remote_info_library' );
171 }
172
173 $this->redirect_to_admin_page();
174 }
175
176 /**
177 * @since 2.3.0
178 * @access public
179 */
180 public function action_get_token() {
181 if ( $this->is_connected() ) {
182 $this->redirect_to_admin_page();
183 }
184
185 if ( empty( $_REQUEST['state'] ) || $_REQUEST['state'] !== $this->get( 'state' ) ) {
186 $this->add_notice( 'Get Token: Invalid Request.', 'error' );
187 $this->redirect_to_admin_page();
188 }
189
190 $response = $this->request( 'get_token', [
191 'grant_type' => 'authorization_code',
192 'code' => $_REQUEST['code'],
193 'redirect_uri' => rawurlencode( $this->get_admin_url( 'get_token' ) ),
194 'client_id' => $this->get( 'client_id' ),
195 ] );
196
197 if ( is_wp_error( $response ) ) {
198 $notice = 'Cannot Get Token:' . $response->get_error_message();
199 $this->add_notice( $notice, 'error' );
200 $this->redirect_to_admin_page();
201 }
202
203 if ( ! empty( $response->data_share_opted_in ) && current_user_can( 'manage_options' ) ) {
204 Tracker::set_opt_in( true );
205 }
206
207 $this->delete( 'state' );
208 $this->set( (array) $response );
209
210 $this->after_connect();
211
212 // Add the notice *after* the method `after_connect`, so an app can redirect without the notice.
213 $this->add_notice( esc_html__( 'Connected Successfully.', 'elementor' ) );
214
215 $this->redirect_to_admin_page();
216 }
217
218 /**
219 * @since 2.3.0
220 * @access public
221 */
222 public function action_disconnect() {
223 if ( $this->is_connected() ) {
224 $this->disconnect();
225 $this->add_notice( esc_html__( 'Disconnected Successfully.', 'elementor' ) );
226 }
227
228 $this->redirect_to_admin_page();
229 }
230
231 /**
232 * @since 2.8.0
233 * @access public
234 */
235 public function action_reconnect() {
236 $this->disconnect();
237
238 $this->action_authorize();
239 }
240
241 /**
242 * @since 2.3.0
243 * @access public
244 */
245 public function get_admin_url( $action, $params = [] ) {
246 $params = [
247 'app' => $this->get_slug(),
248 'action' => $action,
249 'nonce' => wp_create_nonce( $this->get_slug() . $action ),
250 ] + $params;
251
252 $admin_url = Str::encode_idn_url( get_admin_url() );
253 $admin_url .= 'admin.php?page=' . Admin::PAGE_ID;
254
255 return add_query_arg( $params, $admin_url );
256 }
257
258 /**
259 * @since 2.3.0
260 * @access public
261 */
262 public function is_connected() {
263 return (bool) $this->get( 'access_token' );
264 }
265
266 /**
267 * @since 2.3.0
268 * @access protected
269 */
270 protected function init() {}
271
272 /**
273 * @since 2.3.0
274 * @access protected
275 */
276 protected function init_data() {}
277
278 /**
279 * @since 2.3.0
280 * @access protected
281 */
282 protected function after_connect() {}
283
284 /**
285 * @since 2.3.0
286 * @access public
287 */
288 public function get( $key, $default = null ) {
289 $this->init_data();
290
291 return isset( $this->data[ $key ] ) ? $this->data[ $key ] : $default;
292 }
293
294 /**
295 * @since 2.3.0
296 * @access protected
297 */
298 protected function set( $key, $value = null ) {
299 $this->init_data();
300
301 if ( is_array( $key ) ) {
302 $this->data = array_replace_recursive( $this->data, $key );
303 } else {
304 $this->data[ $key ] = $value;
305 }
306
307 $this->update_settings();
308 }
309
310 /**
311 * @since 2.3.0
312 * @access protected
313 */
314 protected function delete( $key = null ) {
315 $this->init_data();
316
317 if ( $key ) {
318 unset( $this->data[ $key ] );
319 } else {
320 $this->data = [];
321 }
322
323 $this->update_settings();
324 }
325
326 /**
327 * @since 2.3.0
328 * @access protected
329 */
330 protected function add( $key, $value, $default = '' ) {
331 $new_value = $this->get( $key, $default );
332
333 if ( is_array( $new_value ) ) {
334 $new_value[] = $value;
335 } elseif ( is_string( $new_value ) ) {
336 $new_value .= $value;
337 } elseif ( is_numeric( $new_value ) ) {
338 $new_value += $value;
339 }
340
341 $this->set( $key, $new_value );
342 }
343
344 /**
345 * @since 2.3.0
346 * @access protected
347 */
348 protected function add_notice( $content, $type = 'success' ) {
349 $this->add( 'notices', compact( 'content', 'type' ), [] );
350 }
351
352 /**
353 * @param $action
354 * @param array $request_body
355 * @param false $as_array
356 *
357 * @return mixed|\WP_Error
358 */
359 protected function request( $action, $request_body = [], $as_array = false ) {
360 $request_body = $this->get_connect_info() + $request_body;
361
362 return $this->http_request(
363 'POST',
364 $action,
365 [
366 'timeout' => 25,
367 'body' => $request_body,
368 'headers' => $this->is_connected() ?
369 [ 'X-Elementor-Signature' => $this->generate_signature( $request_body ) ] :
370 [],
371 ],
372 [
373 'return_type' => $as_array ? static::HTTP_RETURN_TYPE_ARRAY : static::HTTP_RETURN_TYPE_OBJECT,
374 ]
375 );
376 }
377
378 /**
379 * Get all the connect info
380 *
381 * @return array
382 */
383 protected function get_connect_info() {
384 $additional_info = apply_filters( 'elementor/connect/additional-connect-info', [], $this );
385
386 return array_merge(
387 [
388 'app' => $this->get_slug(),
389 'access_token' => $this->get( 'access_token' ),
390 'client_id' => $this->get( 'client_id' ),
391 'local_id' => get_current_user_id(),
392 'site_key' => $this->get_site_key(),
393 'home_url' => trailingslashit( home_url() ),
394 ],
395 $additional_info
396 );
397 }
398
399 /**
400 * @param $endpoint
401 *
402 * @return array
403 */
404 protected function generate_authentication_headers( $endpoint ) {
405 $connect_info = ( new Collection( $this->get_connect_info() ) )
406 ->map_with_keys( function ( $value, $key ) {
407 // For bc `get_connect_info` returns the connect info with underscore,
408 // headers with underscore are not valid, so all the keys with underscore will be replaced to hyphen.
409 return [ str_replace( '_', '-', $key ) => $value ];
410 } )
411 ->replace_recursive( [ 'endpoint' => $endpoint ] )
412 ->sort_keys();
413
414 return $connect_info
415 ->merge( [ 'X-Elementor-Signature' => $this->generate_signature( $connect_info->all() ) ] )
416 ->all();
417 }
418
419 /**
420 * Send an http request
421 *
422 * @param $method
423 * @param $endpoint
424 * @param array $args
425 * @param array $options
426 *
427 * @return mixed|\WP_Error
428 */
429 protected function http_request( $method, $endpoint, $args = [], $options = [] ) {
430 $options = wp_parse_args( $options, [
431 'return_type' => static::HTTP_RETURN_TYPE_OBJECT,
432 ] );
433
434 $args = array_replace_recursive( [
435 'headers' => $this->is_connected() ? $this->generate_authentication_headers( $endpoint ) : [],
436 'method' => $method,
437 'timeout' => 10,
438 ], $args );
439
440 $response = $this->http->request_with_fallback(
441 $this->get_generated_urls( $endpoint ),
442 $args
443 );
444
445 if ( is_wp_error( $response ) ) {
446 // PHPCS - the variable $response does not contain a user input value.
447 wp_die( $response, [ 'back_link' => true ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
448 }
449
450 $body = wp_remote_retrieve_body( $response );
451 $response_code = (int) wp_remote_retrieve_response_code( $response );
452
453 if ( ! $response_code ) {
454 return new \WP_Error( 500, 'No Response' );
455 }
456
457 // Server sent a success message without content.
458 if ( 'null' === $body ) {
459 $body = true;
460 }
461
462 $body = json_decode( $body, static::HTTP_RETURN_TYPE_ARRAY === $options['return_type'] );
463
464 if ( false === $body ) {
465 return new \WP_Error( 422, 'Wrong Server Response' );
466 }
467
468 if ( 200 !== $response_code ) {
469 // In case $as_array = true.
470 $body = (object) $body;
471
472 $message = isset( $body->message ) ? $body->message : wp_remote_retrieve_response_message( $response );
473 $code = (int) ( isset( $body->code ) ? $body->code : $response_code );
474
475 if ( 401 === $code ) {
476 $this->delete();
477
478 if ( 'xhr' !== $this->auth_mode ) {
479 $this->action_authorize();
480 }
481 }
482
483 return new \WP_Error( $code, $message );
484 }
485
486 return $body;
487 }
488
489 /**
490 * Create a signature for the http request
491 *
492 * @param array $payload
493 *
494 * @return false|string
495 */
496 private function generate_signature( $payload = [] ) {
497 return hash_hmac(
498 'sha256',
499 wp_json_encode( $payload, JSON_NUMERIC_CHECK ),
500 $this->get( 'access_token_secret' )
501 );
502 }
503
504 /**
505 * @since 2.3.0
506 * @access protected
507 */
508 protected function get_api_url() {
509 return static::API_URL . '/' . $this->get_slug();
510 }
511 /**
512 * @since 2.3.0
513 * @access protected
514 */
515 protected function get_remote_site_url() {
516 return static::SITE_URL . '/' . $this->get_slug();
517 }
518
519 /**
520 * @since 2.3.0
521 * @access protected
522 */
523 protected function get_remote_authorize_url() {
524 $redirect_uri = $this->get_auth_redirect_uri();
525
526 $url = add_query_arg( [
527 'action' => 'authorize',
528 'response_type' => 'code',
529 'client_id' => $this->get( 'client_id' ),
530 'auth_secret' => $this->get( 'auth_secret' ),
531 'state' => $this->get( 'state' ),
532 'redirect_uri' => rawurlencode( $redirect_uri ),
533 'may_share_data' => current_user_can( 'manage_options' ) && ! Tracker::is_allow_track(),
534 'reconnect_nonce' => wp_create_nonce( $this->get_slug() . 'reconnect' ),
535 ], $this->get_remote_site_url() );
536
537 return $url;
538 }
539
540 /**
541 * @since 2.3.0
542 * @access protected
543 */
544 protected function redirect_to_admin_page( $url = '' ) {
545 if ( ! $url ) {
546 $url = Admin::$url;
547 }
548
549 switch ( $this->auth_mode ) {
550 case 'popup':
551 $this->print_popup_close_script( $url );
552 break;
553
554 case 'cli':
555 $this->admin_notice();
556 die;
557
558 default:
559 wp_safe_redirect( $url );
560 die;
561 }
562 }
563
564 /**
565 * @since 2.3.0
566 * @access protected
567 */
568 protected function set_client_id() {
569 if ( $this->get( 'client_id' ) ) {
570 return;
571 }
572
573 $response = $this->request( 'get_client_id' );
574
575 if ( is_wp_error( $response ) ) {
576 // PHPCS - the variable $response does not contain a user input value.
577 wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
578 }
579
580 $this->set( 'client_id', $response->client_id );
581 $this->set( 'auth_secret', $response->auth_secret );
582 }
583
584 /**
585 * @since 2.3.0
586 * @access protected
587 */
588 protected function set_request_state() {
589 $this->set( 'state', wp_generate_password( 12, false ) );
590 }
591
592 protected function get_popup_success_event_data() {
593 return [];
594 }
595
596 /**
597 * @since 2.3.0
598 * @access protected
599 */
600 protected function print_popup_close_script( $url ) {
601 $data = $this->get_popup_success_event_data();
602
603 ?>
604 <script>
605 if ( opener && opener !== window ) {
606 opener.jQuery( 'body' ).trigger(
607 'elementor/connect/success/<?php echo esc_attr( $_REQUEST['callback_id'] ); ?>',
608 <?php echo wp_json_encode( $data ); ?>
609 );
610
611 window.close();
612 opener.focus();
613 } else {
614 location = '<?php echo esc_url( $url ); ?>';
615 }
616 </script>
617 <?php
618 die;
619 }
620
621 /**
622 * @since 2.3.0
623 * @access protected
624 */
625 protected function disconnect() {
626 if ( $this->is_connected() ) {
627 // Try update the server, but not needed to handle errors.
628 $this->request( 'disconnect' );
629 }
630
631 $this->delete();
632 }
633
634 /**
635 * @since 2.3.0
636 * @access protected
637 */
638 public function get_site_key() {
639 $site_key = get_option( 'elementor_connect_site_key' );
640
641 if ( ! $site_key ) {
642 $site_key = md5( uniqid( wp_generate_password() ) );
643 update_option( 'elementor_connect_site_key', $site_key );
644 }
645
646 return $site_key;
647 }
648
649 protected function redirect_to_remote_authorize_url() {
650 switch ( $this->auth_mode ) {
651 case 'cli':
652 $this->get_app_token_from_cli_token( $_REQUEST['token'] );
653 return;
654 default:
655 wp_redirect( $this->get_remote_authorize_url() );
656 die;
657 }
658 }
659
660 protected function get_auth_redirect_uri() {
661 $redirect_uri = $this->get_admin_url( 'get_token' );
662
663 switch ( $this->auth_mode ) {
664 case 'popup':
665 $redirect_uri = add_query_arg( [
666 'mode' => 'popup',
667 'callback_id' => esc_attr( $_REQUEST['callback_id'] ),
668 ], $redirect_uri );
669 break;
670 }
671
672 return $redirect_uri;
673 }
674
675
676 protected function print_notices( $notices ) {
677 switch ( $this->auth_mode ) {
678 case 'cli':
679 foreach ( $notices as $notice ) {
680 printf( '[%s] %s', wp_kses_post( $notice['type'] ), wp_kses_post( $notice['content'] ) );
681 }
682 break;
683 default:
684 /**
685 * @var Admin_Notices $admin_notices
686 */
687 $admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
688
689 foreach ( $notices as $notice ) {
690 $options = [
691 'description' => wp_kses_post( wpautop( $notice['content'] ) ),
692 'type' => $notice['type'],
693 'icon' => false,
694 ];
695
696 $admin_notices->print_admin_notice( $options );
697 }
698 }
699 }
700
701 protected function get_app_info() {
702 return [];
703 }
704
705 protected function print_app_info() {
706 $app_info = $this->get_app_info();
707
708 foreach ( $app_info as $key => $item ) {
709 if ( $item['value'] ) {
710 $status = 'Exist';
711 $color = 'green';
712 } else {
713 $status = 'Empty';
714 $color = 'red';
715 }
716
717 // PHPCS - the values of $item['label'], $color, $status are plain strings.
718 printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
719 }
720
721 }
722
723 private function get_generated_urls( $endpoint ) {
724 $base_urls = $this->get_api_url();
725
726 if ( ! is_array( $base_urls ) ) {
727 $base_urls = [ $base_urls ];
728 }
729
730 return array_map( function ( $base_url ) use ( $endpoint ) {
731 return trailingslashit( $base_url ) . $endpoint;
732 }, $base_urls );
733 }
734
735 private function init_auth_mode() {
736 $is_rest = defined( 'REST_REQUEST' ) && REST_REQUEST;
737 $is_ajax = wp_doing_ajax();
738
739 if ( $is_rest || $is_ajax ) {
740 // Set default to 'xhr' if rest or ajax request.
741 $this->auth_mode = 'xhr';
742 }
743
744 if ( isset( $_REQUEST['mode'] ) ) { // phpcs:ignore -- nonce validation is not require here.
745 $allowed_auth_modes = [
746 'popup',
747 ];
748
749 if ( defined( 'WP_CLI' ) && WP_CLI ) {
750 $allowed_auth_modes[] = 'cli';
751 }
752
753 $mode = $_REQUEST['mode']; // phpcs:ignore -- nonce validation is not require here.
754
755 if ( in_array( $mode, $allowed_auth_modes, true ) ) {
756 $this->auth_mode = $mode;
757 }
758 }
759 }
760
761 /**
762 * @since 2.3.0
763 * @access public
764 */
765 public function __construct() {
766 add_action( 'admin_notices', [ $this, 'admin_notice' ] );
767
768 $this->init_auth_mode();
769
770 $this->http = new Http();
771
772 /**
773 * Allow extended apps to customize the __construct without call parent::__construct.
774 */
775 $this->init();
776 }
777 }
778