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

809 lines 18.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\Admin\Admin_Notices;
5 use Elementor\Core\Common\Modules\Connect\Admin;
6 use Elementor\Core\Utils\Collection;
7 use Elementor\Core\Utils\Http;
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 OPTION_CONNECT_SITE_KEY = self::OPTION_NAME_PREFIX . 'site_key';
21
22 const SITE_URL = 'https://my.elementor.com/connect/v1';
23
24 const API_URL = 'https://my.elementor.com/api/connect/v1';
25
26 const HTTP_RETURN_TYPE_OBJECT = 'object';
27 const HTTP_RETURN_TYPE_ARRAY = 'array';
28
29 protected $data = [];
30
31 protected $auth_mode = '';
32
33 /**
34 * @var Http
35 */
36 protected $http;
37
38 /**
39 * @since 2.3.0
40 * @access protected
41 * @abstract
42 * TODO: make it public.
43 */
44 abstract protected function get_slug();
45
46 /**
47 * @since 2.8.0
48 * @access public
49 * TODO: make it abstract.
50 */
51 public function get_title() {
52 return $this->get_slug();
53 }
54
55 /**
56 * @since 2.3.0
57 * @access protected
58 * @abstract
59 */
60 abstract protected function update_settings();
61
62 /**
63 * @since 2.3.0
64 * @access public
65 * @static
66 */
67 public static function get_class_name() {
68 return get_called_class();
69 }
70
71 /**
72 * @access public
73 * @abstract
74 */
75 public function render_admin_widget() {
76 // PHPCS - the method get_title return a plain string.
77 echo '<h2>' . $this->get_title() . '</h2>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
78
79 if ( $this->is_connected() ) {
80 $remote_user = $this->get( 'user' );
81 /* translators: %s: Remote user. */
82 $title = sprintf( esc_html__( 'Connected as %s', 'elementor' ), '<strong>' . esc_html( $remote_user->email ) . '</strong>' );
83 $label = esc_html__( 'Disconnect', 'elementor' );
84 $url = $this->get_admin_url( 'disconnect' );
85 $attr = '';
86
87 echo sprintf(
88 '%s <a %s href="%s">%s</a>',
89 // PHPCS - the variable $title is already escaped above.
90 $title, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
91 // PHPCS - the variable $attr is a plain string.
92 $attr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
93 esc_attr( $url ),
94 esc_html( $label )
95 );
96 } else {
97 echo 'Not Connected';
98 }
99
100 echo '<hr>';
101
102 $this->print_app_info();
103
104 if ( current_user_can( 'manage_options' ) ) {
105 printf( '<div><a href="%s">%s</a></div>', esc_url( $this->get_admin_url( 'reset' ) ), esc_html__( 'Reset Data', 'elementor' ) );
106 }
107
108 echo '<hr>';
109 }
110
111
112 /**
113 * @since 2.3.0
114 * @access protected
115 */
116 protected function get_option_name() {
117 return static::OPTION_NAME_PREFIX . $this->get_slug();
118 }
119
120 /**
121 * @since 2.3.0
122 * @access public
123 */
124 public function admin_notice() {
125 $notices = $this->get( 'notices' );
126
127 if ( ! $notices ) {
128 return;
129 }
130
131 $this->print_notices( $notices );
132
133 $this->delete( 'notices' );
134 }
135
136
137 public function get_app_token_from_cli_token( $cli_token ) {
138 $response = $this->request( 'get_app_token_from_cli_token', [
139 'cli_token' => $cli_token,
140 ] );
141
142 if ( is_wp_error( $response ) ) {
143 // PHPCS - the variable $response does not contain a user input value.
144 wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
145 }
146
147 // Use state as usual.
148 $_REQUEST['state'] = $this->get( 'state' );
149 $_REQUEST['code'] = $response->code;
150 }
151 /**
152 * @since 2.3.0
153 * @access public
154 */
155 public function action_authorize() {
156 if ( $this->is_connected() ) {
157 $this->add_notice( esc_html__( 'Already connected.', 'elementor' ), 'info' );
158 $this->redirect_to_admin_page();
159 return;
160 }
161
162 $this->set_client_id();
163 $this->set_request_state();
164
165 $this->redirect_to_remote_authorize_url();
166 }
167
168 public function action_reset() {
169 if ( current_user_can( 'manage_options' ) ) {
170 delete_option( static::OPTION_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 $allowed_query_params_to_propagate = [
538 'utm_source',
539 'utm_medium',
540 'utm_campaign',
541 'utm_term',
542 'utm_content',
543 ];
544
545 $query_params = ( new Collection( $_GET ) ) // phpcs:ignore
546 ->only( $allowed_query_params_to_propagate )
547 ->merge( [
548 'action' => 'authorize',
549 'response_type' => 'code',
550 'client_id' => $this->get( 'client_id' ),
551 'auth_secret' => $this->get( 'auth_secret' ),
552 'state' => $this->get( 'state' ),
553 'redirect_uri' => rawurlencode( $redirect_uri ),
554 'may_share_data' => current_user_can( 'manage_options' ) && ! Tracker::is_allow_track(),
555 'reconnect_nonce' => wp_create_nonce( $this->get_slug() . 'reconnect' ),
556 ] );
557
558 return add_query_arg( $query_params->all(), $this->get_remote_site_url() );
559 }
560
561 /**
562 * @since 2.3.0
563 * @access protected
564 */
565 protected function redirect_to_admin_page( $url = '' ) {
566 if ( ! $url ) {
567 $url = Admin::$url;
568 }
569
570 switch ( $this->auth_mode ) {
571 case 'popup':
572 $this->print_popup_close_script( $url );
573 break;
574
575 case 'cli':
576 $this->admin_notice();
577 die;
578
579 default:
580 wp_safe_redirect( $url );
581 die;
582 }
583 }
584
585 /**
586 * @since 2.3.0
587 * @access protected
588 */
589 protected function set_client_id() {
590 if ( $this->get( 'client_id' ) ) {
591 return;
592 }
593
594 $response = $this->request(
595 'get_client_id',
596 [
597 // phpcs:ignore WordPress.Security.NonceVerification
598 'source' => isset( $_REQUEST['source'] ) ? esc_attr( $_REQUEST['source'] ) : '',
599 ]
600 );
601
602 if ( is_wp_error( $response ) ) {
603 // PHPCS - the variable $response does not contain a user input value.
604 wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
605 }
606
607 $this->set( 'client_id', $response->client_id );
608 $this->set( 'auth_secret', $response->auth_secret );
609 }
610
611 /**
612 * @since 2.3.0
613 * @access protected
614 */
615 protected function set_request_state() {
616 $this->set( 'state', wp_generate_password( 12, false ) );
617 }
618
619 protected function get_popup_success_event_data() {
620 return [];
621 }
622
623 /**
624 * @since 2.3.0
625 * @access protected
626 */
627 protected function print_popup_close_script( $url ) {
628 $data = $this->get_popup_success_event_data();
629
630 ?>
631 <script>
632 if ( opener && opener !== window ) {
633 opener.jQuery( 'body' ).trigger(
634 'elementor/connect/success/<?php echo esc_attr( $_REQUEST['callback_id'] ); ?>',
635 <?php echo wp_json_encode( $data ); ?>
636 );
637
638 window.close();
639 opener.focus();
640 } else {
641 location = '<?php echo esc_url( $url ); ?>';
642 }
643 </script>
644 <?php
645 die;
646 }
647
648 /**
649 * @since 2.3.0
650 * @access protected
651 */
652 protected function disconnect() {
653 if ( $this->is_connected() ) {
654 // Try update the server, but not needed to handle errors.
655 $this->request( 'disconnect' );
656 }
657
658 $this->delete();
659 }
660
661 /**
662 * @since 2.3.0
663 * @access protected
664 */
665 public function get_site_key() {
666 $site_key = get_option( static::OPTION_CONNECT_SITE_KEY );
667
668 if ( ! $site_key ) {
669 $site_key = md5( uniqid( wp_generate_password() ) );
670 update_option( static::OPTION_CONNECT_SITE_KEY, $site_key );
671 }
672
673 return $site_key;
674 }
675
676 protected function redirect_to_remote_authorize_url() {
677 switch ( $this->auth_mode ) {
678 case 'cli':
679 $this->get_app_token_from_cli_token( $_REQUEST['token'] );
680 return;
681 default:
682 wp_redirect( $this->get_remote_authorize_url() );
683 die;
684 }
685 }
686
687 protected function get_auth_redirect_uri() {
688 $redirect_uri = $this->get_admin_url( 'get_token' );
689
690 switch ( $this->auth_mode ) {
691 case 'popup':
692 $redirect_uri = add_query_arg( [
693 'mode' => 'popup',
694 'callback_id' => esc_attr( $_REQUEST['callback_id'] ),
695 ], $redirect_uri );
696 break;
697 }
698
699 return $redirect_uri;
700 }
701
702
703 protected function print_notices( $notices ) {
704 switch ( $this->auth_mode ) {
705 case 'cli':
706 foreach ( $notices as $notice ) {
707 printf( '[%s] %s', wp_kses_post( $notice['type'] ), wp_kses_post( $notice['content'] ) );
708 }
709 break;
710 default:
711 /**
712 * @var Admin_Notices $admin_notices
713 */
714 $admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
715
716 foreach ( $notices as $notice ) {
717 $options = [
718 'description' => wp_kses_post( wpautop( $notice['content'] ) ),
719 'type' => $notice['type'],
720 'icon' => false,
721 ];
722
723 $admin_notices->print_admin_notice( $options );
724 }
725 }
726 }
727
728 protected function get_app_info() {
729 return [];
730 }
731
732 protected function print_app_info() {
733 $app_info = $this->get_app_info();
734
735 foreach ( $app_info as $key => $item ) {
736 if ( $item['value'] ) {
737 $status = 'Exist';
738 $color = 'green';
739 } else {
740 $status = 'Empty';
741 $color = 'red';
742 }
743
744 // PHPCS - the values of $item['label'], $color, $status are plain strings.
745 printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
746 }
747
748 }
749
750 private function get_generated_urls( $endpoint ) {
751 $base_urls = $this->get_api_url();
752
753 if ( ! is_array( $base_urls ) ) {
754 $base_urls = [ $base_urls ];
755 }
756
757 return array_map( function ( $base_url ) use ( $endpoint ) {
758 return trailingslashit( $base_url ) . $endpoint;
759 }, $base_urls );
760 }
761
762 private function init_auth_mode() {
763 $is_rest = defined( 'REST_REQUEST' ) && REST_REQUEST;
764 $is_ajax = wp_doing_ajax();
765
766 if ( $is_rest || $is_ajax ) {
767 // Set default to 'xhr' if rest or ajax request.
768 $this->set_auth_mode( 'xhr' );
769 }
770
771 if ( isset( $_REQUEST['mode'] ) ) { // phpcs:ignore -- nonce validation is not require here.
772 $allowed_auth_modes = [
773 'popup',
774 ];
775
776 if ( defined( 'WP_CLI' ) && WP_CLI ) {
777 $allowed_auth_modes[] = 'cli';
778 }
779
780 $mode = $_REQUEST['mode']; // phpcs:ignore -- nonce validation is not require here.
781
782 if ( in_array( $mode, $allowed_auth_modes, true ) ) {
783 $this->set_auth_mode( $mode );
784 }
785 }
786 }
787
788 public function set_auth_mode( $mode ) {
789 $this->auth_mode = $mode;
790 }
791
792 /**
793 * @since 2.3.0
794 * @access public
795 */
796 public function __construct() {
797 add_action( 'admin_notices', [ $this, 'admin_notice' ] );
798
799 $this->init_auth_mode();
800
801 $this->http = new Http();
802
803 /**
804 * Allow extended apps to customize the __construct without call parent::__construct.
805 */
806 $this->init();
807 }
808 }
809