Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
9.47% covered (danger)
9.47%
9 / 95
28.57% covered (danger)
28.57%
2 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
StravaUtils
9.47% covered (danger)
9.47%
9 / 95
28.57% covered (danger)
28.57%
2 / 7
451.31
0.00% covered (danger)
0.00%
0 / 1
 getClientId
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClientSecret
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getRegistrationUrl
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 linkStrava
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 getAccessToken
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
56
 fetchTokenDataForCode
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
6
 callStravaApi
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Olz\Utils;
4
5use Olz\Entity\StravaLink;
6use PhpTypeScriptApi\HttpError;
7
8class StravaUtils {
9    use WithUtilsTrait;
10
11    public function getClientId(): string {
12        return $this->envUtils()->getStravaClientId();
13    }
14
15    protected function getClientSecret(): string {
16        return $this->envUtils()->getStravaClientSecret();
17    }
18
19    /** @param array<string> $scopes */
20    public function getRegistrationUrl(
21        array $scopes = ['read'],
22        ?string $redirect_url = null,
23    ): string {
24        $client_id = $this->getClientId();
25        $base_href = $this->envUtils()->getBaseHref();
26        $code_href = $this->envUtils()->getCodeHref();
27        $redirect_url_suffix = $redirect_url ? "?redirect_url=".urlencode($redirect_url) : '';
28        $strava_redirect_url = "{$base_href}{$code_href}strava_redirect{$redirect_url_suffix}";
29        $enc_strava_redirect_url = urlencode($strava_redirect_url);
30        $enc_scopes = urlencode(implode(',', $scopes));
31        return "https://www.strava.com/oauth/authorize?client_id={$client_id}&response_type=code&redirect_uri={$enc_strava_redirect_url}&approval_prompt=force&scope={$enc_scopes}";
32    }
33
34    public function linkStrava(string $code): ?StravaLink {
35        $data = $this->fetchTokenDataForCode([
36            'client_id' => $this->getClientId(),
37            'client_secret' => $this->getClientSecret(),
38            'code' => $code,
39            'grant_type' => 'authorization_code',
40        ]);
41        $athlete_id = $data['athlete']['id'] ?? null;
42        $access_token = $data['access_token'] ?? null;
43        $refresh_token = $data['refresh_token'] ?? null;
44        $expires_at = $data['expires_at'] ?? null;
45        if ($athlete_id === null || $access_token === null || $refresh_token === null || $expires_at === null) {
46            return null;
47        }
48
49        $now = new \DateTime($this->dateUtils()->getIsoNow());
50        $user = $this->authUtils()->getCurrentUser();
51        if (!$user) {
52            throw new HttpError(401, 'Nicht eingeloggt!');
53        }
54
55        $strava_link_repo = $this->entityManager()->getRepository(StravaLink::class);
56        $strava_link = $strava_link_repo->findOneBy(['strava_user' => $athlete_id]);
57        if ($strava_link === null) {
58            $strava_link = new StravaLink();
59            $strava_link->setCreatedAt($now);
60        } else {
61            $previous_user_id = $strava_link->getUser()->getId();
62            $new_user_id = $user->getId();
63            if ($previous_user_id !== $new_user_id) {
64                $this->log()->notice("{$strava_link} changed user: {$previous_user_id} => {$new_user_id}");
65            }
66        }
67        $strava_link->setUser($user);
68        $strava_link->setAccessToken($access_token);
69        $strava_link->setRefreshToken($refresh_token);
70        $strava_link->setExpiresAt(new \DateTime(date('Y-m-d H:i:s', $expires_at)));
71        $strava_link->setStravaUser($athlete_id);
72        $strava_link->setLinkedAt($now);
73
74        $this->entityManager()->persist($strava_link);
75        $this->entityManager()->flush();
76        return $strava_link;
77    }
78
79    public function getAccessToken(StravaLink $strava_link): ?string {
80        $now_iso = $this->dateUtils()->getIsoNow();
81        $expires_at_iso = $strava_link->getExpiresAt()->format('Y-m-d H:i:s');
82        $access_token = $strava_link->getAccessToken();
83        if ($access_token && $expires_at_iso > $now_iso) {
84            return $access_token;
85        }
86        $this->log()->debug('Strava token refresh...');
87        $refresh_token = $strava_link->getRefreshToken();
88
89        $data = $this->fetchTokenDataForCode([
90            'client_id' => $this->getClientId(),
91            'client_secret' => $this->getClientSecret(),
92            'refresh_token' => $refresh_token,
93            'grant_type' => 'refresh_token',
94        ]);
95        $this->log()->debug('Strava token refresh response:', [$data]);
96        $access_token = $data['access_token'] ?? null;
97        $refresh_token = $data['refresh_token'] ?? null;
98        $expires_at = $data['expires_at'] ?? null;
99
100        if (!$access_token || !$refresh_token || !$expires_at) {
101            $json_data = json_encode($data) ?: '';
102            $this->log()->notice("Refreshing strava token failed: {$json_data}");
103            return null;
104        }
105        $this->log()->debug('Strava token refreshed');
106        $strava_link->setAccessToken($access_token);
107        $strava_link->setRefreshToken($refresh_token);
108        $strava_link->setExpiresAt(new \DateTime(date('Y-m-d H:i:s', $expires_at)));
109        $this->entityManager()->flush();
110        return $access_token;
111    }
112
113    /**
114     * @param array<string, mixed> $token_request_data
115     *
116     * @return ?array<string, mixed>
117     */
118    public function fetchTokenDataForCode(array $token_request_data): ?array {
119        $strava_token_url = 'https://www.strava.com/api/v3/oauth/token';
120
121        $ch = curl_init();
122        curl_setopt($ch, CURLOPT_URL, $strava_token_url);
123        curl_setopt($ch, CURLOPT_POST, true);
124        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($token_request_data, '', '&'));
125        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
126        $token_result = curl_exec($ch);
127        return json_decode(!is_bool($token_result) ? $token_result : '', true);
128    }
129
130    /**
131     * @param 'GET'|'POST'          $method
132     * @param array<string, string> $query
133     */
134    public function callStravaApi(
135        string $method,
136        string $path,
137        array $query,
138        string $access_token,
139    ): mixed {
140        $strava_url = 'https://www.strava.com/api/v3';
141        $query_string = http_build_query($query, '', '&');
142
143        $ch = curl_init();
144        curl_setopt($ch, CURLOPT_URL, "{$strava_url}{$path}");
145        curl_setopt($ch, CURLOPT_HTTPHEADER, [
146            "Authorization: Bearer {$access_token}",
147            "Content-Type: application/x-www-form-urlencoded",
148        ]);
149        curl_setopt($ch, CURLOPT_HEADER, false);
150        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
151        curl_setopt($ch, CURLOPT_POST, $method === 'POST');
152        if ($query_string) {
153            curl_setopt($ch, CURLOPT_POSTFIELDS, $query_string);
154        }
155        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
156        $result = curl_exec($ch);
157        return json_decode(!is_bool($result) ? $result : '', true);
158    }
159}