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

818 lines 18.6 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 $this->delete( 'state' );
205 $this->set( (array) $response );
206
207 if ( ! empty( $response->data_share_opted_in ) && current_user_can( 'manage_options' ) ) {
208 Tracker::set_opt_in( true );
209 }
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 Base Connect Info
381 *
382 * Returns an array of connect info.
383 *
384 * @return array
385 */
386 protected function get_base_connect_info() {
387 return [
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 }
396
397 /**
398 * Get all the connect information
399 *
400 * @return array
401 */
402 protected function get_connect_info() {
403 $connect_info = $this->get_base_connect_info();
404
405 $additional_info = [];
406
407 /**
408 * Additional connect info.
409 *
410 * Filters the connection information when connecting to Elementor servers.
411 * This hook can be used to add more information or add more data.
412 *
413 * @param array $additional_info Additional connecting information array.
414 * @param Base_App $this The base app instance.
415 */
416 $additional_info = apply_filters( 'elementor/connect/additional-connect-info', $additional_info, $this );
417
418 return array_merge( $connect_info, $additional_info );
419 }
420
421 /**
422 * @param $endpoint
423 *
424 * @return array
425 */
426 protected function generate_authentication_headers( $endpoint ) {
427 $connect_info = ( new Collection( $this->get_connect_info() ) )
428 ->map_with_keys( function ( $value, $key ) {
429 // For bc `get_connect_info` returns the connect info with underscore,
430 // headers with underscore are not valid, so all the keys with underscore will be replaced to hyphen.
431 return [ str_replace( '_', '-', $key ) => $value ];
432 } )
433 ->replace_recursive( [ 'endpoint' => $endpoint ] )
434 ->sort_keys();
435
436 return $connect_info
437 ->merge( [ 'X-Elementor-Signature' => $this->generate_signature( $connect_info->all() ) ] )
438 ->all();
439 }
440
441 /**
442 * Send an http request
443 *
444 * @param $method
445 * @param $endpoint
446 * @param array $args
447 * @param array $options
448 *
449 * @return mixed|\WP_Error
450 */
451 protected function http_request( $method, $endpoint, $args = [], $options = [] ) {
452 $options = wp_parse_args( $options, [
453 'return_type' => static::HTTP_RETURN_TYPE_OBJECT,
454 ] );
455
456 $args = array_replace_recursive( [
457 'headers' => $this->is_connected() ? $this->generate_authentication_headers( $endpoint ) : [],
458 'method' => $method,
459 'timeout' => 10,
460 ], $args );
461
462 $response = $this->http->request_with_fallback(
463 $this->get_generated_urls( $endpoint ),
464 $args
465 );
466
467 if ( is_wp_error( $response ) ) {
468 // PHPCS - the variable $response does not contain a user input value.
469 wp_die( $response, [ 'back_link' => true ] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
470 }
471
472 $body = wp_remote_retrieve_body( $response );
473 $response_code = (int) wp_remote_retrieve_response_code( $response );
474
475 if ( ! $response_code ) {
476 return new \WP_Error( 500, 'No Response' );
477 }
478
479 // Server sent a success message without content.
480 if ( 'null' === $body ) {
481 $body = true;
482 }
483
484 $body = json_decode( $body, static::HTTP_RETURN_TYPE_ARRAY === $options['return_type'] );
485
486 if ( false === $body ) {
487 return new \WP_Error( 422, 'Wrong Server Response' );
488 }
489
490 if ( 200 !== $response_code ) {
491 // In case $as_array = true.
492 $body = (object) $body;
493
494 $message = isset( $body->message ) ? $body->message : wp_remote_retrieve_response_message( $response );
495 $code = (int) ( isset( $body->code ) ? $body->code : $response_code );
496
497 if ( 401 === $code ) {
498 $this->delete();
499
500 if ( 'xhr' !== $this->auth_mode ) {
501 $this->action_authorize();
502 }
503 }
504
505 return new \WP_Error( $code, $message );
506 }
507
508 return $body;
509 }
510
511 /**
512 * Create a signature for the http request
513 *
514 * @param array $payload
515 *
516 * @return false|string
517 */
518 private function generate_signature( $payload = [] ) {
519 return hash_hmac(
520 'sha256',
521 wp_json_encode( $payload, JSON_NUMERIC_CHECK ),
522 $this->get( 'access_token_secret' )
523 );
524 }
525
526 /**
527 * @since 2.3.0
528 * @access protected
529 */
530 protected function get_api_url() {
531 return static::API_URL . '/' . $this->get_slug();
532 }
533 /**
534 * @since 2.3.0
535 * @access protected
536 */
537 protected function get_remote_site_url() {
538 return static::SITE_URL . '/' . $this->get_slug();
539 }
540
541 /**
542 * @since 2.3.0
543 * @access protected
544 */
545 protected function get_remote_authorize_url() {
546 $redirect_uri = $this->get_auth_redirect_uri();
547
548 $allowed_query_params_to_propagate = [
549 'utm_source',
550 'utm_medium',
551 'utm_campaign',
552 'utm_term',
553 'utm_content',
554 'source',
555 'screen_hint',
556 ];
557
558 $query_params = ( new Collection( $_GET ) ) // phpcs:ignore
559 ->only( $allowed_query_params_to_propagate )
560 ->merge( [
561 'action' => 'authorize',
562 'response_type' => 'code',
563 'client_id' => $this->get( 'client_id' ),
564 'auth_secret' => $this->get( 'auth_secret' ),
565 'state' => $this->get( 'state' ),
566 'redirect_uri' => rawurlencode( $redirect_uri ),
567 'may_share_data' => current_user_can( 'manage_options' ) && ! Tracker::is_allow_track(),
568 'reconnect_nonce' => wp_create_nonce( $this->get_slug() . 'reconnect' ),
569 ] );
570
571 return add_query_arg( $query_params->all(), $this->get_remote_site_url() );
572 }
573
574 /**
575 * @since 2.3.0
576 * @access protected
577 */
578 protected function redirect_to_admin_page( $url = '' ) {
579 if ( ! $url ) {
580 $url = Admin::$url;
581 }
582
583 switch ( $this->auth_mode ) {
584 case 'popup':
585 $this->print_popup_close_script( $url );
586 break;
587
588 case 'cli':
589 $this->admin_notice();
590 die;
591
592 default:
593 wp_safe_redirect( $url );
594 die;
595 }
596 }
597
598 /**
599 * @since 2.3.0
600 * @access protected
601 */
602 protected function set_client_id() {
603 $response = $this->request(
604 'get_client_id',
605 [
606 // phpcs:ignore WordPress.Security.NonceVerification
607 'source' => isset( $_REQUEST['source'] ) ? esc_attr( $_REQUEST['source'] ) : '',
608 ]
609 );
610
611 if ( is_wp_error( $response ) ) {
612 // PHPCS - the variable $response does not contain a user input value.
613 wp_die( $response, $response->get_error_message() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
614 }
615
616 $this->set( 'client_id', $response->client_id );
617 $this->set( 'auth_secret', $response->auth_secret );
618 }
619
620 /**
621 * @since 2.3.0
622 * @access protected
623 */
624 protected function set_request_state() {
625 $this->set( 'state', wp_generate_password( 12, false ) );
626 }
627
628 protected function get_popup_success_event_data() {
629 return [];
630 }
631
632 /**
633 * @since 2.3.0
634 * @access protected
635 */
636 protected function print_popup_close_script( $url ) {
637 $data = $this->get_popup_success_event_data();
638
639 ?>
640 <script>
641 if ( opener && opener !== window ) {
642 opener.jQuery( 'body' ).trigger(
643 'elementor/connect/success/<?php echo esc_attr( $_REQUEST['callback_id'] ); ?>',
644 <?php echo wp_json_encode( $data ); ?>
645 );
646
647 window.close();
648 opener.focus();
649 } else {
650 location = '<?php echo esc_url( $url ); ?>';
651 }
652 </script>
653 <?php
654 die;
655 }
656
657 /**
658 * @since 2.3.0
659 * @access protected
660 */
661 protected function disconnect() {
662 if ( $this->is_connected() ) {
663 // Try update the server, but not needed to handle errors.
664 $this->request( 'disconnect' );
665 }
666
667 $this->delete();
668 }
669
670 /**
671 * @since 2.3.0
672 * @access protected
673 */
674 public function get_site_key() {
675 $site_key = get_option( static::OPTION_CONNECT_SITE_KEY );
676
677 if ( ! $site_key ) {
678 $site_key = md5( uniqid( wp_generate_password() ) );
679 update_option( static::OPTION_CONNECT_SITE_KEY, $site_key );
680 }
681
682 return $site_key;
683 }
684
685 protected function redirect_to_remote_authorize_url() {
686 switch ( $this->auth_mode ) {
687 case 'cli':
688 $this->get_app_token_from_cli_token( $_REQUEST['token'] );
689 return;
690 default:
691 wp_redirect( $this->get_remote_authorize_url() );
692 die;
693 }
694 }
695
696 protected function get_auth_redirect_uri() {
697 $redirect_uri = $this->get_admin_url( 'get_token' );
698
699 switch ( $this->auth_mode ) {
700 case 'popup':
701 $redirect_uri = add_query_arg( [
702 'mode' => 'popup',
703 'callback_id' => esc_attr( $_REQUEST['callback_id'] ),
704 ], $redirect_uri );
705 break;
706 }
707
708 return $redirect_uri;
709 }
710
711
712 protected function print_notices( $notices ) {
713 switch ( $this->auth_mode ) {
714 case 'cli':
715 foreach ( $notices as $notice ) {
716 printf( '[%s] %s', wp_kses_post( $notice['type'] ), wp_kses_post( $notice['content'] ) );
717 }
718 break;
719 default:
720 /**
721 * @var Admin_Notices $admin_notices
722 */
723 $admin_notices = Plugin::$instance->admin->get_component( 'admin-notices' );
724
725 foreach ( $notices as $notice ) {
726 $options = [
727 'description' => wp_kses_post( wpautop( $notice['content'] ) ),
728 'type' => $notice['type'],
729 'icon' => false,
730 ];
731
732 $admin_notices->print_admin_notice( $options );
733 }
734 }
735 }
736
737 protected function get_app_info() {
738 return [];
739 }
740
741 protected function print_app_info() {
742 $app_info = $this->get_app_info();
743
744 foreach ( $app_info as $key => $item ) {
745 if ( $item['value'] ) {
746 $status = 'Exist';
747 $color = 'green';
748 } else {
749 $status = 'Empty';
750 $color = 'red';
751 }
752
753 // PHPCS - the values of $item['label'], $color, $status are plain strings.
754 printf( '%s: <strong style="color:%s">%s</strong><br>', $item['label'], $color, $status ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
755 }
756
757 }
758
759 private function get_generated_urls( $endpoint ) {
760 $base_urls = $this->get_api_url();
761
762 if ( ! is_array( $base_urls ) ) {
763 $base_urls = [ $base_urls ];
764 }
765
766 return array_map( function ( $base_url ) use ( $endpoint ) {
767 return trailingslashit( $base_url ) . $endpoint;
768 }, $base_urls );
769 }
770
771 private function init_auth_mode() {
772 $is_rest = defined( 'REST_REQUEST' ) && REST_REQUEST;
773 $is_ajax = wp_doing_ajax();
774
775 if ( $is_rest || $is_ajax ) {
776 // Set default to 'xhr' if rest or ajax request.
777 $this->set_auth_mode( 'xhr' );
778 }
779
780 if ( isset( $_REQUEST['mode'] ) ) { // phpcs:ignore -- nonce validation is not require here.
781 $allowed_auth_modes = [
782 'popup',
783 ];
784
785 if ( defined( 'WP_CLI' ) && WP_CLI ) {
786 $allowed_auth_modes[] = 'cli';
787 }
788
789 $mode = $_REQUEST['mode']; // phpcs:ignore -- nonce validation is not require here.
790
791 if ( in_array( $mode, $allowed_auth_modes, true ) ) {
792 $this->set_auth_mode( $mode );
793 }
794 }
795 }
796
797 public function set_auth_mode( $mode ) {
798 $this->auth_mode = $mode;
799 }
800
801 /**
802 * @since 2.3.0
803 * @access public
804 */
805 public function __construct() {
806 add_action( 'admin_notices', [ $this, 'admin_notice' ] );
807
808 $this->init_auth_mode();
809
810 $this->http = new Http();
811
812 /**
813 * Allow extended apps to customize the __construct without call parent::__construct.
814 */
815 $this->init();
816 }
817 }
818