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

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