Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 3
CRAP
0.00% covered (danger)
0.00%
0 / 1
StravaHackController
0.00% covered (danger)
0.00%
0 / 94
0.00% covered (danger)
0.00%
0 / 3
702
0.00% covered (danger)
0.00%
0 / 1
 stravaScript
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 registerStravaRun
0.00% covered (danger)
0.00%
0 / 86
0.00% covered (danger)
0.00%
0 / 1
462
 withCors
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3namespace Olz\Controller;
4
5use Olz\Anniversary\Endpoints\RunEndpointTrait;
6use Olz\Entity\Anniversary\RunRecord;
7use Olz\Entity\Users\User;
8use Olz\Parsers\StravaActivityParser;
9use Olz\Utils\WithUtilsTrait;
10use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
11use Symfony\Component\HttpFoundation\JsonResponse;
12use Symfony\Component\HttpFoundation\Request;
13use Symfony\Component\HttpFoundation\Response;
14use Symfony\Component\Routing\Attribute\Route;
15
16class StravaHackController extends AbstractController {
17    use WithUtilsTrait;
18    use RunEndpointTrait;
19
20    #[Route('/api-cors/strava_script.js', methods: ['GET'])]
21    public function stravaScript(): Response {
22        $content = file_get_contents(__DIR__.'/../Anniversary/Components/OlzAnniversary/strava_script.js') ?: '';
23        return new Response($content, 200);
24    }
25
26    #[Route('/api-cors/registerStravaRun', methods: ['POST', 'OPTIONS'])]
27    public function registerStravaRun(Request $request): Response {
28        // Handle preflight OPTIONS
29        if ($request->getMethod() === 'OPTIONS') {
30            $response = $this->withCors($request, new JsonResponse([], 204)); // No Content
31            $response->headers->set('Access-Control-Allow-Methods', 'POST, OPTIONS');
32            $response->headers->set('Access-Control-Allow-Headers', 'Content-Type, Authorization');
33            $response->headers->set('Cache-Control', 'max-age=3600');
34            return $response;
35        }
36
37        // Handle POST
38        if ($request->getMethod() === 'POST') {
39            $json_content = $request->getContent();
40            $payload = json_decode($json_content, true);
41
42            $token = $payload['token'] ?? null;
43            $key = $this->envUtils()->getEncryptionKey('strava-hack-token');
44            $data = null;
45            try {
46                $data = $this->generalUtils()->decrypt($key, $token);
47            } catch (\Throwable $th) {
48                // ignore
49            }
50            $user_id = $data['user_id'] ?? null;
51            if (!$data || !$user_id || !isset($data['token_time'])) {
52                return $this->withCors($request, new JsonResponse(['msg' => "🚫 Ungültiger Token"], 400));
53            }
54            $user_repo = $this->entityManager()->getRepository(User::class);
55            $user = $user_repo->findOneBy(['id' => $user_id]);
56            if (!$user) {
57                $this->log()->notice("registerStravaRun denied for invalid user ID {$user_id}");
58                return $this->withCors($request, new JsonResponse(['msg' => "🚫 Token von ungültigem Benutzer"], 400));
59            }
60            // TODO: Check token time after September
61            $this->log()->debug("registerStravaRun: {$json_content}");
62
63            $activityId = $payload['activityId'] ?? null;
64            if (!$activityId || $activityId < 100000000 || $activityId > 100000000000000) {
65                $this->log()->notice("registerStravaRun denied for activityId {$activityId} by {$user}");
66                return $this->withCors($request, new JsonResponse(['msg' => "🚫 Ungültige Aktivitäts-ID"], 400));
67            }
68            $source = "strava-id{$activityId}";
69            $runs_repo = $this->entityManager()->getRepository(RunRecord::class);
70            $run = $runs_repo->findOneBy(['source' => $source]);
71            $is_update = (bool) $run;
72
73            $html = $payload['html'] ?? null;
74            $this->generalUtils()->checkNotNull($html, 'HTML must be provided');
75            $parser = new StravaActivityParser();
76            $data = $parser->parse_strava_activity_html($html);
77            if (
78                !$data['name']
79                || !$data['sportType']
80                || !$data['runAt']
81                || !$data['distanceMeters']
82                || !$data['elevationMeters']
83            ) {
84                $enc_data = json_encode($data);
85                $this->log()->notice("registerStravaRun parse error for activityId {$activityId} by {$user}{$enc_data}");
86                return $this->withCors($request, new JsonResponse(['msg' => "🚫 HTML konnte nicht (vollständig) gelesen werden."], 400));
87            }
88            $name_arr = explode(' ', $data['name']);
89            $name = $name_arr[0].' '.implode(' ', array_map(
90                fn ($part) => substr($part, 0, 1).'.',
91                array_slice($name_arr, 1),
92            ));
93            $sport_type = $data['sportType'];
94            $is_sport_type_valid = [
95                'Run' => true,
96                'TrailRun' => true,
97                'Hike' => true,
98                'Walk' => true,
99                'Laufen' => true,
100                'Traillauf' => true,
101                'Wanderung' => true,
102                'Spaziergang' => true,
103            ];
104            $is_counting = $is_sport_type_valid[$sport_type] ?? false;
105
106            if (!$run) {
107                $run = new RunRecord();
108                $this->entityUtils()->createOlzEntity($run, ['ownerUserId' => $user_id, 'onOff' => true]);
109            } else {
110                $this->entityUtils()->updateOlzEntity($run, ['ownerUserId' => $user_id, 'onOff' => true]);
111            }
112            if ($is_update) {
113                $old_data = $this->getEntityData($run);
114                $this->log()->notice('OLD:', [$old_data]);
115            }
116            $run->setUser(null);
117            $run->setRunnerName($name);
118            $run->setRunAt($data['runAt']);
119            $run->setIsCounting($is_counting);
120            $run->setDistanceMeters(intval($data['distanceMeters']));
121            $run->setElevationMeters(intval($data['elevationMeters']));
122            $run->setSportType($sport_type);
123            $run->setSource($source);
124            $run->setInfo(json_encode($data) ?: null);
125            if ($is_update) {
126                $new_data = $this->getEntityData($run);
127                $this->log()->notice('NEW:', [$new_data]);
128            }
129            $this->entityManager()->persist($run);
130            $this->entityManager()->flush();
131
132            $msg = $is_update
133                ? '🔄 Existierender Höhenmeter-Challenge-Eintrag wurde aktualisiert'
134                : '✅ Neuer Höhenmeter-Challenge-Eintrag wurde erstellt';
135            return $this->withCors($request, new JsonResponse(['msg' => $msg], 200));
136        }
137
138        throw new \Exception("Tertium non datur!");
139    }
140
141    protected function withCors(Request $request, Response $response): Response {
142        $allowedOrigins = ['https://www.strava.com'];
143        $origin = $request->headers->get('Origin');
144        if ($origin && in_array($origin, $allowedOrigins)) {
145            $response->headers->set('Access-Control-Allow-Origin', $origin);
146        }
147        $response->headers->set('Access-Control-Allow-Credentials', 'true');
148        return $response;
149    }
150}