PluginProbe
Elementor Website Builder – more than just a page builder / 3.4.6
Elementor Website Builder – more than just a page builder v3.4.6
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.4.6, at core/common/modules/connect/apps/base-app.php

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