self::PAGE_SLUG ), $args ), admin_url( 'options-general.php' ) ); } /** * Menu entry under Settings. */ public static function add_menu() { self::$hook = (string) add_options_page( __( 'Taseli', 'taseli' ), __( 'Taseli', 'taseli' ), 'manage_options', self::PAGE_SLUG, array( __CLASS__, 'render' ) ); } /** * Link on the Plugins screen. * * @param string[] $links Links. * @return string[] */ public static function action_links( $links ) { $label = Taseli_Settings::is_signed() ? __( 'Settings', 'taseli' ) : __( 'Connect', 'taseli' ); array_unshift( $links, '' . esc_html( $label ) . '' ); return $links; } /** * Styles and the small confirm script, on our screen only. * * @param string $hook_suffix Current screen. */ public static function enqueue( $hook_suffix ) { if ( '' === self::$hook || $hook_suffix !== self::$hook ) { return; } wp_enqueue_style( 'taseli-admin', TASELI_URL . 'assets/css/taseli-admin.css', array(), TASELI_VERSION ); wp_enqueue_script( 'taseli-admin', TASELI_URL . 'assets/js/taseli-admin.js', array(), TASELI_VERSION, true ); } /** * Stop unless the current user may manage this site's settings. */ private static function require_admin() { if ( ! current_user_can( 'manage_options' ) ) { wp_die( esc_html__( 'You are not allowed to do this.', 'taseli' ), 403 ); } } /** * Back to the settings screen with a notice. * * @param string $notice Notice id. */ private static function back( $notice ) { wp_safe_redirect( self::page_url( array( 'taseli_notice' => $notice ) ) ); exit; } /** * Lets wp_safe_redirect() go to the Taseli service host (the connect page). * * @param string[] $hosts Allowed hosts. * @return string[] */ public static function allow_service_host( $hosts ) { $host = wp_parse_url( Taseli_Settings::service_url(), PHP_URL_HOST ); if ( is_string( $host ) && '' !== $host ) { $hosts[] = $host; } return $hosts; } /** * Connect: start the handshake and send the administrator to Taseli. */ public static function handle_connect() { self::require_admin(); check_admin_referer( 'taseli_connect' ); $state = wp_generate_password( 32, false, false ); $verifier = wp_generate_password( 64, false, false ); set_transient( 'taseli_connect_' . get_current_user_id(), array( 'state' => $state, 'verifier' => $verifier, ), self::CONNECT_TTL ); $return = add_query_arg( array( 'action' => 'taseli_finish', '_wpnonce' => wp_create_nonce( 'taseli_finish' ), ), admin_url( 'admin-post.php' ) ); $url = Taseli_Helpers::build_connect_url( Taseli_Settings::service_url() . '/connect/wordpress', array( 'site' => home_url( '/' ), 'rest' => rest_url(), 'v' => TASELI_VERSION, 'wc' => Taseli_Settings::has_woocommerce(), 'challenge' => Taseli_Helpers::connect_challenge( $verifier ), 'state' => $state, 'return' => $return, ) ); add_filter( 'allowed_redirect_hosts', array( __CLASS__, 'allow_service_host' ) ); wp_safe_redirect( $url ); exit; } /** * Back from Taseli: check the state, exchange the one-time code for the per-site secret. */ public static function handle_finish() { self::require_admin(); check_admin_referer( 'taseli_finish' ); $key = 'taseli_connect_' . get_current_user_id(); $pending = get_transient( $key ); delete_transient( $key ); $state = isset( $_GET['taseli_state'] ) ? sanitize_text_field( wp_unslash( $_GET['taseli_state'] ) ) : ''; if ( ! is_array( $pending ) || empty( $pending['state'] ) || empty( $pending['verifier'] ) || ! hash_equals( (string) $pending['state'], $state ) ) { self::back( 'expired' ); } if ( isset( $_GET['taseli_error'] ) ) { self::back( 'declined' ); } $code = Taseli_Helpers::token( isset( $_GET['taseli_code'] ) ? sanitize_text_field( wp_unslash( $_GET['taseli_code'] ) ) : '', 'code' ); if ( '' === $code ) { self::back( 'failed' ); } $response = wp_remote_post( Taseli_Settings::service_url() . '/api/wordpress/exchange', array( 'timeout' => 20, 'headers' => array( 'Content-Type' => 'application/json' ), 'body' => wp_json_encode( array( 'code' => $code, 'verifier' => (string) $pending['verifier'], 'site' => home_url( '/' ), 'v' => TASELI_VERSION, ) ), ) ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { self::back( 'failed' ); } $data = json_decode( (string) wp_remote_retrieve_body( $response ), true ); $data = is_array( $data ) ? $data : array(); $site_id = Taseli_Helpers::token( isset( $data['site_id'] ) ? $data['site_id'] : '', 'site_id' ); $secret = Taseli_Helpers::token( isset( $data['secret'] ) ? $data['secret'] : '', 'secret' ); $wkey = Taseli_Helpers::sanitize_workspace_key( isset( $data['workspace_key'] ) ? $data['workspace_key'] : '' ); if ( '' === $site_id || '' === $secret || '' === $wkey ) { self::back( 'failed' ); } Taseli_Settings::link( $site_id, $secret, $wkey, Taseli_Helpers::sanitize_business_name( isset( $data['business'] ) ? $data['business'] : '' ), Taseli_Helpers::sanitize_pixel_settings( isset( $data['pixel'] ) ? $data['pixel'] : array() ), get_current_user_id() ); /** * Fires after the site was connected to a Taseli workspace. * * @param string $site_id Taseli's id for this site. */ do_action( 'taseli_linked', $site_id ); self::back( 'connected' ); } /** * Save the pixel switch. */ public static function handle_save_pixel() { self::require_admin(); check_admin_referer( 'taseli_save_pixel' ); Taseli_Settings::set_pixel_enabled( ! empty( $_POST['taseli_pixel_enabled'] ) ); self::back( 'saved' ); } /** * Disconnect: tell Taseli (signed, best effort), then forget the link and the secret. */ public static function handle_disconnect() { self::require_admin(); check_admin_referer( 'taseli_disconnect' ); Taseli_Auth::post_to_taseli( '/api/wordpress/nudge', array( 'scope' => 'disconnect' ) ); Taseli_Settings::disconnect(); do_action( 'taseli_unlinked' ); self::back( 'disconnected' ); } /** * Notice from the last action. */ private static function render_notice() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Display only; the value is matched against a fixed list. $notice = isset( $_GET['taseli_notice'] ) ? sanitize_key( wp_unslash( $_GET['taseli_notice'] ) ) : ''; $messages = array( 'saved' => array( 'success', __( 'Saved.', 'taseli' ) ), 'disconnected' => array( 'success', __( 'Disconnected. Taseli can no longer read this site.', 'taseli' ) ), 'connected' => array( 'success', __( 'Connected. Taseli can start working.', 'taseli' ) ), 'declined' => array( 'info', __( 'Connecting was cancelled. Nothing changed.', 'taseli' ) ), 'expired' => array( 'warning', __( 'That connect link expired. Click Connect to Taseli again.', 'taseli' ) ), 'failed' => array( 'error', __( 'Taseli could not finish connecting. Try again in a minute.', 'taseli' ) ), ); if ( isset( $messages[ $notice ] ) ) { printf( '

%2$s

', esc_attr( $messages[ $notice ][0] ), esc_html( $messages[ $notice ][1] ) ); } } /** * Exactly what Taseli can read and change (shown before connecting and while connected). */ private static function render_access() { $has_wc = Taseli_Settings::has_woocommerce(); ?>