guard('ladon_connect'); $verifier = $this->base64url(random_bytes(48)); $state = $this->base64url(random_bytes(24)); set_transient($this->transientKey(), array( 'verifier' => $verifier, 'state' => $state, ), 10 * MINUTE_IN_SECONDS); $url = add_query_arg(array( 'site_url' => rawurlencode(home_url('/')), 'name' => rawurlencode(get_bloginfo('name')), 'callback' => rawurlencode(admin_url('admin-post.php?action=ladon_callback')), 'state' => $state, 'code_challenge' => $this->base64url(hash('sha256', $verifier, true)), 'code_challenge_method' => 'S256', 'product' => 'ladon', ), Client::connectUrl()); $host = wp_parse_url(Client::connectUrl(), PHP_URL_HOST); add_filter('allowed_redirect_hosts', function ($hosts) use ($host) { $hosts[] = $host; return $hosts; }); wp_safe_redirect($url); exit; } public function callback() { if (!current_user_can('manage_options')) { wp_die(esc_html__('You are not allowed to connect this site.', 'hackindex-ladon'), 403); } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- the state from the connect step is checked below with hash_equals, a nonce cannot survive the trip to Ladon $stored = get_transient($this->transientKey()); delete_transient($this->transientKey()); $state = isset($_GET['state']) ? sanitize_text_field(wp_unslash($_GET['state'])) : ''; if (!is_array($stored) || empty($stored['state']) || !hash_equals($stored['state'], $state)) { $this->back(array('ladon_error' => 'state')); } if (isset($_GET['error'])) { $this->back(array('ladon_error' => 'denied')); } $code = isset($_GET['code']) ? sanitize_text_field(wp_unslash($_GET['code'])) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ($code === '') { $this->back(array('ladon_error' => 'code')); } $result = (new Client())->exchangeToken($code, $stored['verifier']); if (!$result['ok'] || empty($result['body']['token'])) { $this->back(array('ladon_error' => 'exchange')); } $body = $result['body']; $site = isset($body['site']) && is_array($body['site']) ? $body['site'] : array(); $team = isset($body['team']) && is_array($body['team']) ? $body['team'] : array(); Settings::merge(array( 'token' => $body['token'], 'site_id' => isset($site['id']) ? sanitize_text_field($site['id']) : null, 'site_name' => isset($site['name']) ? sanitize_text_field($site['name']) : null, 'team_slug' => isset($team['slug']) ? sanitize_text_field($team['slug']) : null, 'plan' => isset($team['plan']) ? sanitize_text_field($team['plan']) : null, 'connected_at' => time(), 'inventory_hash' => null, )); Plugin::instance()->sync->run('connect'); $this->back(array('ladon_connected' => '1')); } public function disconnect() { $this->guard('ladon_disconnect'); // Best effort: the site disconnects locally even when Ladon cannot be reached, a team admin can still revoke it there (new Client())->revokeToken(); Settings::forget(); $this->back(array('ladon_disconnected' => '1')); } private function guard($action) { if (!current_user_can('manage_options')) { wp_die(esc_html__('You are not allowed to do this.', 'hackindex-ladon'), 403); } check_admin_referer($action); } private function transientKey() { return self::TRANSIENT_PREFIX . get_current_user_id(); } private function base64url($bytes) { return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '='); } /** * @param array $args */ private function back(array $args) { wp_safe_redirect(add_query_arg($args, admin_url('options-general.php?page=ladon'))); exit; } }