diff --git a/.gitignore b/.gitignore
index 9b68f68..187e198 100644
--- a/.gitignore
+++ b/.gitignore
@@ -27,4 +27,6 @@ docker-compose.override.yaml
/phpcs.xml
###< squizlabs/php_codesniffer ###
-.idea/
\ No newline at end of file
+.idea/
+
+garmin_credentials.json
diff --git a/composer.json b/composer.json
index b301c0c..c637995 100644
--- a/composer.json
+++ b/composer.json
@@ -6,7 +6,7 @@
"ext-ctype": "*",
"ext-iconv": "*",
"ext-json": "*",
- "dawguk/php-garmin-connect": "^1.7",
+ "ext-openssl": "*",
"doctrine/collections": "^1.6",
"league/csv": "^9.6",
"symfony/console": "6.4.*",
diff --git a/config/services.yaml b/config/services.yaml
index c34e723..28f587b 100644
--- a/config/services.yaml
+++ b/config/services.yaml
@@ -12,8 +12,6 @@ services:
autoconfigure: true # Automatically registers your services as commands, event subscribers, etc.
bind:
- $garminUsername: '%env(GARMIN_USERNAME)%'
- $garminPassword: '%env(GARMIN_PASSWORD)%'
_instanceof:
App\Library\Handler\HandlerInterface:
diff --git a/src/Http/GarminClient/ClientException.php b/src/Http/GarminClient/ClientException.php
new file mode 100644
index 0000000..8dd4e2d
--- /dev/null
+++ b/src/Http/GarminClient/ClientException.php
@@ -0,0 +1,10 @@
+ 'gauth-widget',
+ 'embedWidget' => 'true',
+ ];
+
+ public const GARMIN_EMBED_PARAMETERS = [
+ ...self::GARMIN_DEFAULT_PARAMETERS,
+ 'gauthHost' => self::GARMIN_SSO_URL,
+ ];
+
+ public const GARMIN_SIGN_IN_PARAMETERS = [
+ ...self::GARMIN_DEFAULT_PARAMETERS,
+ 'gauthHost' => self::GARMIN_SSO_EMBED_URL,
+ 'service' => self::GARMIN_SSO_EMBED_URL,
+ 'source' => self::GARMIN_SSO_EMBED_URL,
+ 'redirectAfterAccountLoginUrl' => self::GARMIN_SSO_EMBED_URL,
+ 'redirectAfterAccountCreationUrl' => self::GARMIN_SSO_EMBED_URL,
+ ];
+
+ protected string $consumerKey = '';
+ protected string $consumerSecret = '';
+
+ public function __construct(
+ private HttpClientInterface $httpClient,
+ #[Autowire(env: '%env(GARMIN_USERNAME)%')]
+ private string $garminUsername,
+ #[Autowire(env: '%env(GARMIN_PASSWORD)%')]
+ private string $garminPassword,
+ #[Autowire('%kernel.project_dir%')]
+ private readonly string $projectDirectory
+ ) {
+ }
+
+ public function authenticate()
+ {
+ $filePath = $this->projectDirectory . DIRECTORY_SEPARATOR . self::GARMIN_AUTHENTICATION_FILE;
+
+ // Load file from path if it exists
+ if (file_exists($filePath)) {
+ $json = file_get_contents($filePath);
+ $oauthData = json_decode($json, true);
+
+ if ($oauthData['expires_at'] > time()) {
+ // Great the access token isn't expired so lets reuse it from the file
+ return $oauthData['access_token'];
+ }
+ // Access token must be expired so lets refresh it
+ if ($oauthData['refresh_token_expires_at'] < time()) {
+ $oauthData = $this->exchangeOauth1TokenForOauth2Token($oauthData['token'], $oauthData['token_secret']);
+
+ $this->refreshFile($filePath, $oauthData);
+ return $oauthData['access_token'];
+ }
+ }
+
+ // Run through the login process which is ridiculous
+ $this->fetchConsumerCredentials();
+ $this->initializeCookies();
+ $csrfToken = $this->fetchCSRFToken();
+ $ticket = $this->submitLoginRequest($csrfToken);
+ $oauth1Token = $this->getOauthToken($ticket);
+ $oauthToken = $oauth1Token['oauth_token'];
+ $oauthTokenSecret = $oauth1Token['oauth_token_secret'];
+ $oauthData = $this->exchangeOauth1TokenForOauth2Token($oauthToken, $oauthTokenSecret);
+ $oauthData['token'] = $oauthToken;
+ $oauthData['token_secret'] = $oauthTokenSecret;
+
+ $this->refreshFile($filePath, $oauthData);
+ return $oauthData['access_token'];
+ }
+
+ protected function refreshFile(string $filePath, array $oauthData): void
+ {
+ file_put_contents($filePath, json_encode($oauthData));
+ }
+
+ protected function fetchConsumerCredentials(): void
+ {
+ $response = $this->httpClient->request('GET', self::GARTH_SSO_TOKENS);
+ $oauth = $response->toArray();
+ $this->consumerKey = $oauth['consumer_key'];
+ $this->consumerSecret = $oauth['consumer_secret'];
+ }
+
+ protected function initializeCookies(): void
+ {
+ $this->httpClient->request('GET', self::GARMIN_SSO_EMBED_URL, [
+ 'query' => self::GARMIN_EMBED_PARAMETERS,
+ ]);
+ }
+
+ protected function fetchCSRFToken(): string
+ {
+ $response = $this->httpClient->request('GET', self::GARMIN_SSO_SIGN_IN_URL, [
+ 'query' => self::GARMIN_SIGN_IN_PARAMETERS,
+ ]);
+
+ $responseBody = $response->getContent();
+
+ preg_match('/name="_csrf"\s+value="(.+?)"/', $responseBody, $csrfTokens);
+
+ if (! isset($csrfTokens[1])) {
+ throw new ClientException('CSRF token is missing.');
+ }
+
+ return $csrfTokens[1];
+ }
+
+ protected function submitLoginRequest(string $csrfToken)
+ {
+ $response = $this->httpClient->request('POST', self::GARMIN_SSO_SIGN_IN_URL, [
+ 'query' => self::GARMIN_SIGN_IN_PARAMETERS,
+ 'headers' => [
+ 'referer' => self::GARMIN_SSO_SIGN_IN_URL,
+ ],
+ 'body' => [
+ 'username' => $this->garminUsername,
+ 'password' => $this->garminPassword,
+ 'embed' => true,
+ '_csrf' => $csrfToken,
+ ]
+ ]);
+
+
+ $responseBody = $response->getContent(false);
+
+ preg_match('/
(.+?)<\/title>/', $responseBody, $titles);
+
+ if (! isset($titles[1])) {
+ throw new ClientException('TITLE is missing.');
+ }
+
+ $title = $titles[1];
+
+ // YA!!!!! we got into Garmin
+ if ($title === 'Success') {
+ preg_match('/embed\?ticket=([^"]+)"/', $responseBody, $tokens);
+
+ if (isset($tokens[1])) {
+ return $tokens[1];
+ }
+ }
+
+ throw new ClientException('Invalid title!');
+ }
+
+ public function getOauthToken(string $ticket): array
+ {
+ $oauthRequest = new OauthHttpDecorator($this->consumerKey, $this->consumerSecret);
+
+ $response = $oauthRequest->request(
+ $this->httpClient,
+ 'GET',
+ GarminClient::GARMIN_API_URL . '/oauth-service/oauth/preauthorized',
+ [
+ 'query' => [
+ 'ticket' => $ticket,
+ 'login-url' => self::GARMIN_SSO_EMBED_URL,
+ 'accepts-mfa-tokens' => 'true',
+ ]
+ ]
+ );
+
+ $oauth1Body = $response->getContent();
+
+ parse_str($oauth1Body, $oauthResponseBody);
+
+ return $oauthResponseBody;
+ }
+
+ public function exchangeOauth1TokenForOauth2Token(
+ string $oauthToken,
+ string $oauthTokenSecret,
+ ) {
+ $oauthRequest = new OauthHttpDecorator(
+ $this->consumerKey,
+ $this->consumerSecret,
+ $oauthToken,
+ $oauthTokenSecret
+ );
+
+ $oauth2Response = $oauthRequest->request(
+ $this->httpClient,
+ 'POST',
+ GarminClient::GARMIN_API_URL . '/oauth-service/oauth/exchange/user/2.0',
+ [
+ 'headers' => [
+ 'User-Agent' => GarminClient::USER_AGENT,
+ 'Content-Type' => 'application/x-www-form-urlencoded'
+ ]
+ ]
+ );
+
+ $oauth2Data = $oauth2Response->toArray();
+
+ $oauth2Data['expires_at'] = $oauth2Data['expires_in'] + time();
+ $oauth2Data['refresh_token_expires_at'] = $oauth2Data['refresh_token_expires_in'] + time();
+
+ return $oauth2Data;
+ }
+
+ public function setGarminUsername(string $garminUsername): void
+ {
+ $this->garminUsername = $garminUsername;
+ }
+
+ public function setGarminPassword(string $garminPassword): void
+ {
+ $this->garminPassword = $garminPassword;
+ }
+}
diff --git a/src/Http/GarminClient/GarminClient.php b/src/Http/GarminClient/GarminClient.php
new file mode 100644
index 0000000..fdf7277
--- /dev/null
+++ b/src/Http/GarminClient/GarminClient.php
@@ -0,0 +1,182 @@
+setup();
+ }
+
+ public function addCredentials(string $username, string $password): void
+ {
+ $this->garminAuthenticator->setGarminUsername($username);
+ $this->garminAuthenticator->setGarminPassword($password);
+ }
+
+ public function setup(): void
+ {
+ $accessToken = $this->garminAuthenticator->authenticate();
+
+ $this->client = $this->client->withOptions(
+ (new HttpOptions())
+ ->setHeaders([
+ 'User-Agent' => self::USER_AGENT,
+ 'Authorization' => 'Bearer ' . $accessToken,
+ ])
+ ->toArray()
+ );
+ }
+
+ public function fetchPersonalInformation(): array
+ {
+ $response = $this->client->request('GET', self::GARMIN_API_URL . '/userprofile-service/userprofile/personal-information');
+
+ return $response->toArray();
+ }
+
+ public function getWorkoutList($intStart = 0, $intLimit = 10, $myWorkoutsOnly = true, $sharedWorkoutsOnly = false)
+ {
+ $queryParameters = array(
+ 'start' => $intStart,
+ 'limit' => $intLimit,
+ 'myWorkoutsOnly' => $myWorkoutsOnly,
+ 'sharedWorkoutsOnly' => $sharedWorkoutsOnly
+ );
+
+ $client = $this->client->withOptions(
+ (new HttpOptions())
+ ->setQuery($queryParameters)
+ ->toArray()
+ );
+
+ $response = $client->request(
+ 'GET',
+ self::GARMIN_API_URL . '/workout-service/workouts'
+ );
+
+ if ($response->getStatusCode() != 200) {
+ throw new ClientException('Response code - ' . $response->getStatusCode());
+ }
+
+ $objResponse = json_decode($response->getContent());
+ return $objResponse;
+ }
+
+ public function createWorkout($data)
+ {
+ if (empty($data)) {
+ throw new DataException('Data must be supplied to create a new workout.');
+ }
+
+ $headers = [
+ 'NK: NT',
+ 'Content-Type: application/json'
+ ];
+
+ $client = $this->client->withOptions(
+ (new HttpOptions())
+ ->setJson($data)
+ ->setHeaders($headers)
+ ->toArray()
+ );
+
+ $response = $client->request(
+ 'POST',
+ self::GARMIN_API_URL . '/workout-service/workout'
+ );
+
+ if ($response->getStatusCode() != 200) {
+ throw new ClientException('Response code - ' . $response->getStatusCode());
+ }
+
+ $objResponse = json_decode($response->getContent());
+ return $objResponse;
+ }
+
+ /**
+ * Delete a workout based upon the workout ID
+ *
+ * @param $id
+ * @return mixed
+ * @throws ClientException
+ */
+ public function deleteWorkout($id)
+ {
+ if (empty($id)) {
+ throw new DataException('Workout ID must be supplied to delete a workout.');
+ }
+
+ $headers = [
+ 'NK: NT',
+ 'X-HTTP-Method-Override: DELETE'
+ ];
+
+ $client = $this->client->withOptions(
+ (new HttpOptions())
+ ->setHeaders($headers)
+ ->toArray()
+ );
+
+ $response = $client->request(
+ 'POST',
+ self::GARMIN_API_URL . '/workout-service/workout/' . $id
+ );
+
+ if ($response->getStatusCode() != 204) {
+ throw new ClientException('Response code - ' . $response->getStatusCode());
+ }
+
+ $objResponse = json_decode($response->getContent());
+ return $objResponse;
+ }
+
+ /**
+ * Schedule a workout on the calendar
+ *
+ * @param $id
+ * @param $payload
+ * @return mixed
+ * @throws ClientException
+ */
+ public function scheduleWorkout($id, $data)
+ {
+ $headers = [
+ 'NK: NT',
+ 'Content-Type: application/json'
+ ];
+
+ if (empty($id)) {
+ throw new DataException('Workout ID must be supplied to delete a workout.');
+ }
+
+ $client = $this->client->withOptions(
+ (new HttpOptions())
+ ->setJson($data)
+ ->setHeaders($headers)
+ ->toArray()
+ );
+
+ $response = $client->request(
+ 'POST',
+ self::GARMIN_API_URL . '/workout-service/schedule/' . $id
+ );
+
+ if ($response->getStatusCode() != 200) {
+ throw new ClientException('Response code - ' . $response->getStatusCode());
+ }
+
+ $objResponse = json_decode($response->getContent());
+ return $objResponse;
+ }
+}
diff --git a/src/Http/OauthHttpDecorator.php b/src/Http/OauthHttpDecorator.php
new file mode 100644
index 0000000..75d8511
--- /dev/null
+++ b/src/Http/OauthHttpDecorator.php
@@ -0,0 +1,59 @@
+getQueryParameters($method, $requestUri, $options);
+
+ return $client->request($method, $requestUri, $options);
+ }
+
+ protected function getQueryParameters(string $method, string $requestUri, array $additionalParameters): array
+ {
+ $queryParameters = array_merge(
+ $additionalParameters['query'],
+ [
+ 'oauth_consumer_key' => $this->consumerKey,
+ 'oauth_signature_method' => 'HMAC-SHA1',
+ 'oauth_timestamp' => date('U'),
+ 'oauth_nonce' => OauthUtility::getNonce(),
+ 'oauth_version' => '1.0'
+ ]
+ );
+
+ if (! empty($this->token)) {
+ $queryParameters['oauth_token'] = $this->token;
+ }
+
+ ksort($queryParameters);
+ $baseString = OauthUtility::getBaseString($method, $requestUri, $queryParameters);
+ $signingKey = OauthUtility::generateSigningKey($this->consumerSecret, $this->tokenSecret);
+
+ $queryParameters['oauth_signature'] = OauthUtility::getSignature($baseString, $signingKey);
+
+ $additionalParameters['query'] = $queryParameters;
+
+ return $additionalParameters;
+ }
+}
diff --git a/src/Http/OauthUtility.php b/src/Http/OauthUtility.php
new file mode 100644
index 0000000..6ee0ec1
--- /dev/null
+++ b/src/Http/OauthUtility.php
@@ -0,0 +1,36 @@
+dispatcher->dispatch($event, HandlerEvents::CREATED_WORKOUTS_STARTED);
$this->garminHelper->createWorkouts($workouts);
- $this->garminHelper->attachNotes($workouts);
+// $this->garminHelper->attachNotes($workouts);
$debugMessages = $this->garminHelper->getDebugMessages();
diff --git a/src/Library/Parser/Model/Step/AbstractStep.php b/src/Library/Parser/Model/Step/AbstractStep.php
index 492422c..7db7bf9 100644
--- a/src/Library/Parser/Model/Step/AbstractStep.php
+++ b/src/Library/Parser/Model/Step/AbstractStep.php
@@ -31,7 +31,7 @@ abstract class AbstractStep implements \JsonSerializable
protected $notes;
/**
- * @var float|null
+ * @var int|null
*/
protected $garminID;
@@ -128,7 +128,7 @@ public function jsonSerialize(): array
'stepId' => null,
'stepOrder' => $this->order,
'childStepId' => null,
- 'description' => null,
+ 'description' => $this->notes,
'stepType' => [
'stepTypeId' => $this->getStepTypeId(),
'stepTypeKey' => $this->getStepTypeKey()
@@ -139,18 +139,18 @@ public function jsonSerialize(): array
}
/**
- * @return float|null
+ * @return int|null
*/
- public function getGarminID(): ?float
+ public function getGarminID(): ?int
{
return $this->garminID;
}
/**
- * @param float|null $garminID
+ * @param int|null $garminID
* @return AbstractStep
*/
- public function setGarminID(?float $garminID): AbstractStep
+ public function setGarminID(?int $garminID): AbstractStep
{
$this->garminID = $garminID;
return $this;
diff --git a/src/Library/Parser/Model/Step/RepeaterStep.php b/src/Library/Parser/Model/Step/RepeaterStep.php
index cc48dac..4bd0296 100644
--- a/src/Library/Parser/Model/Step/RepeaterStep.php
+++ b/src/Library/Parser/Model/Step/RepeaterStep.php
@@ -16,7 +16,7 @@ class RepeaterStep implements \JsonSerializable
protected $order;
/**
- * @var float|null
+ * @var int|null
*/
protected $garminID;
@@ -107,18 +107,18 @@ public function jsonSerialize(): array
}
/**
- * @return float|null
+ * @return int|null
*/
- public function getGarminID(): ?float
+ public function getGarminID(): ?int
{
return $this->garminID;
}
/**
- * @param float|null $garminID
+ * @param int|null $garminID
* @return RepeaterStep
*/
- public function setGarminID(?float $garminID): RepeaterStep
+ public function setGarminID(?int $garminID): RepeaterStep
{
$this->garminID = $garminID;
return $this;
diff --git a/src/Library/Parser/Model/Workout/AbstractWorkout.php b/src/Library/Parser/Model/Workout/AbstractWorkout.php
index 122bbb2..25ff8e1 100644
--- a/src/Library/Parser/Model/Workout/AbstractWorkout.php
+++ b/src/Library/Parser/Model/Workout/AbstractWorkout.php
@@ -25,7 +25,7 @@ abstract class AbstractWorkout implements \JsonSerializable
protected $prefix;
/**
- * @var float|null
+ * @var int|null
*/
protected $garminID;
@@ -149,7 +149,7 @@ public function jsonSerialize(): array
]
];
}
-
+
$workout = [
'sportType' => [
'sportTypeId' => $this->getSportTypeId(),
@@ -165,7 +165,7 @@ public function jsonSerialize(): array
'workoutSteps' => $this->steps->toArray()
]]
];
-
+
return array_merge($workout, $swimming);
}
@@ -206,9 +206,9 @@ public function setName(?string $name): AbstractWorkout
}
/**
- * @return float|null
+ * @return int|null
*/
- public function getGarminID(): ?float
+ public function getGarminID(): ?int
{
return $this->garminID;
}
@@ -232,10 +232,10 @@ public function setPrefix(?string $prefix): AbstractWorkout
}
/**
- * @param float|null $garminID
+ * @param int|null $garminID
* @return AbstractWorkout
*/
- public function setGarminID(?float $garminID): AbstractWorkout
+ public function setGarminID(?int $garminID): AbstractWorkout
{
$this->garminID = $garminID;
return $this;
diff --git a/src/Service/GarminHelper.php b/src/Service/GarminHelper.php
index eaf6354..dd9bb01 100644
--- a/src/Service/GarminHelper.php
+++ b/src/Service/GarminHelper.php
@@ -2,52 +2,27 @@
namespace App\Service;
+use App\Http\GarminClient\GarminClient;
use App\Library\Parser\Model\Day;
use App\Library\Parser\Model\Step\AbstractStep;
use App\Library\Parser\Model\Workout\AbstractWorkout;
use App\Model\DebugMessages;
-use dawguk\GarminConnect;
class GarminHelper
{
use DebugMessages;
- /**
- * @var GarminConnect $client
- */
- protected $client;
-
- /**
- * @var string
- */
- protected $username;
-
- /**
- * @var string
- */
- protected $password;
-
- public function __construct($garminUsername, $garminPassword)
- {
- $this->username = $garminUsername;
- $this->password = $garminPassword;
+ public function __construct(
+ private GarminClient $client
+ ) {
}
- public function createGarminClient($username, $password)
+ public function createGarminClient($username, $password): void
{
- $credentials = [
- 'username' => $username,
- 'password' => $password,
- ];
-
- if (empty($username) && empty($password)) {
- $credentials = [
- 'username' => $this->username,
- 'password' => $this->password,
- ];
+ if (! empty($username) && ! empty($password)) {
+ // Set user credentials based on what is added as an argument to command prompt
+ $this->client->addCredentials($username, $password);
}
-
- $this->client = new GarminConnect($credentials);
}
public function createWorkouts(array $workouts)
@@ -65,7 +40,7 @@ public function createWorkouts(array $workouts)
$workout->setGarminID($workoutID);
$debugMessages[] = 'Workout - ' . $workoutName . ' was previously created on the Garmin website with the id ' . $workoutID;
} else {
- $response = $this->client->createWorkout(json_encode($workout));
+ $response = $this->client->createWorkout($workout);
if (! isset($response, $response->workoutId)) {
$debugMessages[] = 'Workout - ' . $workoutName . ' failed to create';
continue;
@@ -154,13 +129,13 @@ public function scheduleWorkouts(array $days)
foreach ($day->getWorkouts() as $workoutKey => $workout) {
if ($day->getDate()) {
$formattedDate = $day->getDate()->format('Y-m-d');
- $data = json_encode(['date' => $formattedDate]);
+ $data = ['date' => $formattedDate];
$messageID = ' is going to be scheduled on ';
if ($workout->getGarminID()) {
$this->client->scheduleWorkout($workout->getGarminID(), $data);
- $messageID = ' with id ' . $workout->getGarminID() .' was scheduled on the Garmin website for ';
+ $messageID = ' with id ' . $workout->getGarminID() . ' was scheduled on the Garmin website for ';
}
$debugMessages[] = 'Workout - ' . $workout->getName() . $messageID . $formattedDate;
}
@@ -183,4 +158,4 @@ public function findWorkoutSteps($steps, $workoutSteps = [])
return $workoutSteps;
}
-}
\ No newline at end of file
+}
diff --git a/symfony.lock b/symfony.lock
index 5aed624..7821857 100644
--- a/symfony.lock
+++ b/symfony.lock
@@ -1,7 +1,4 @@
{
- "dawguk/php-garmin-connect": {
- "version": "v1.3.1"
- },
"doctrine/collections": {
"version": "1.6.4"
},