Tiqr_Message_FCM::getGoogleAccessToken()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 21
c 1
b 0
f 0
dl 0
loc 30
ccs 0
cts 21
cp 0
rs 9.584
cc 3
nc 4
nop 3
crap 12
1
<?php
2
/**
3
 * This file is part of the tiqr project.
4
 *
5
 * The tiqr project aims to provide an open implementation for
6
 * authentication using mobile devices. It was initiated by
7
 * SURFnet and developed by Egeniq.
8
 *
9
 * More information: http://www.tiqr.org
10
 *
11
 * @author Joost van Dijk <[email protected]>
12
 *
13
 * @package tiqr
14
 *
15
 * @license New BSD License - See LICENSE file for details.
16
 *
17
 * @copyright (C) 2010-2024 SURF BV
18
 */
19
use League\Flysystem\Adapter\Local;
20
use League\Flysystem\Filesystem;
21
use Cache\Adapter\Filesystem\FilesystemCachePool;
22
23
/**
24
 * Android Cloud To Device Messaging message.
25
 *
26
 * @author peter
27
 */
28
class Tiqr_Message_FCM extends Tiqr_Message_Abstract
29
{
30
    /**
31
     * Send message.
32
     *
33
     * @throws Tiqr_Message_Exception_SendFailure
34
     */
35
    public function send()
36
    {
37
        $options = $this->getOptions();
38
        $projectId = $options['firebase.projectId'];
39
        $credentialsFile = $options['firebase.credentialsFile'];
40
        $cacheTokens = $options['firebase.cacheTokens'] ?? false;
41
        $tokenCacheDir = $options['firebase.tokenCacheDir'] ?? __DIR__;
42
        $translatedAddress = $this->getAddress();
43
        $alertText = $this->getText();
44
        $properties = $this->getCustomProperties();
45
46
        $this->_sendFirebase($translatedAddress, $alertText, $properties, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir);
47
48
        $this->logger->notice(sprintf('Successfully sent FCM push notification. projectId: "%s"; deviceToken: "%s"', $projectId, $translatedAddress));
49
    }
50
51
    /**
52
     * @throws Tiqr_Message_Exception_SendFailure
53
     */
54
    private function getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCacheDir )
55
    {
56
        $client = new Google_Client();
57
        $client->setLogger($this->logger);
58
        // Try to add a file based cache for accesstokens, if configured
59
        if ($cacheTokens) {
60
            //set up the cache
61
            $filesystemAdapter = new Local($tokenCacheDir);
62
            $filesystem = new Filesystem($filesystemAdapter);
63
            $pool = new FilesystemCachePool($filesystem);
64
65
            //set up a callback to log token refresh
66
            $logger=$this->logger;
67
            $tokenCallback = function ($cacheKey, $accessToken) use ($logger) {
0 ignored issues
show
Unused Code introduced by
The parameter $accessToken is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

67
            $tokenCallback = function ($cacheKey, /** @scrutinizer ignore-unused */ $accessToken) use ($logger) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
68
                $logger->notice(sprintf('New access token received at cache key %s', $cacheKey));
69
            };
70
            $client->setTokenCallback($tokenCallback);
71
            $client->setCache($pool);
72
        } else {
73
            $this->logger->warning("Cache for oAuth tokens is disabled");
74
        }
75
        try {
76
            $client->setAuthConfig($credentialsFile);
77
        } catch (\Google\Exception $e) {
78
            throw new Tiqr_Message_Exception_SendFailure(sprintf("Error setting Google credentials for FCM: %s", $e->getMessage()), true, $e);
79
        }
80
        $client->addScope('https://www.googleapis.com/auth/firebase.messaging');
81
        $client->fetchAccessTokenWithAssertion();
82
        $token = $client->getAccessToken();
83
        return $token['access_token'];
84
    }
85
86
    /**
87
     * Send a message to a device using the firebase API key.
88
     *
89
     * @param  $deviceToken      string device ID
90
     * @param  $alert            string alert message
91
     * @param  $customProperties array Additional properties to send with the message like the challenge.
92
     *                                 Array of string->string (property name -> property value)
93
     * @param  $projectId        string the id of the firebase project
94
     * @param  $credentialsFile  string The location of the firebase secret json
95
     * @param  $cacheTokens      bool Enable caching the accesstokens for accessing the Google API
96
     * @param  $tokenCacheDir    string Location for storing the accesstoken cache
97
     * @param  $retry            boolean is this a 2nd attempt
98
     * @throws Tiqr_Message_Exception_SendFailure
99
     */
100
    private function _sendFirebase(string $deviceToken, string $alert, array $properties, string $projectId, string $credentialsFile, bool $cacheTokens, string $tokenCacheDir, bool $retry=false)
101
    {
102
        $apiurl = sprintf('https://fcm.googleapis.com/v1/projects/%s/messages:send',$projectId);
103
104
        $fields = [
105
            'message' => [
106
                'token' => $deviceToken,
107
                'data' => array(),
108
                "android" => [
109
                    "ttl" => "300s",
110
                ],
111
            ],
112
        ];
113
114
        // Add custom properties
115
        foreach ($properties as $k => $v) {
116
            $fields['message']['data'][(string)$k] = (string)$v;
117
        }
118
        // Add message
119
        $fields['message']['data']['text'] = $alert;
120
121
        try {
122
            $headers = array(
123
                'Authorization: Bearer ' . $this->getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCacheDir),
124
                'Content-Type: application/json',
125
            );
126
        } catch (\Google\Exception $e) {
127
            throw new Tiqr_Message_Exception_SendFailure(sprintf("Error getting Google access token : %s", $e->getMessage()), true);
128
        }
129
130
        $payload = json_encode($fields);
131
        $this->logger->debug(sprintf("JSON payload: %s", $payload));
132
133
        $ch = curl_init();
134
        curl_setopt($ch, CURLOPT_URL, $apiurl);
135
        curl_setopt($ch, CURLOPT_POST, true);
136
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
137
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
138
        curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
139
        $result = curl_exec($ch);
140
        $errors = curl_error($ch);
141
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
142
        $remoteip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
143
        curl_close($ch);
144
145
        if ($result === false) {
146
            throw new Tiqr_Message_Exception_SendFailure("Server unavailable", true);
147
        }
148
149
        if (!empty($errors)) {
150
            throw new Tiqr_Message_Exception_SendFailure("Http error occurred: ". $errors, true);
151
        }
152
153
        // Wait and retry once in case of a 502 Bad Gateway error
154
        if ($statusCode === 502 && !($retry)) {
155
            $this->logger->warning("Received HTTP 502 Bad Gateway error, retrying once");
156
            sleep(2);
157
            $this->_sendFirebase($deviceToken, $alert, $properties, $projectId, $credentialsFile,  $cacheTokens,  $tokenCacheDir, true);
158
            return;
159
        }
160
161
        if ($statusCode !== 200) {
162
            throw new Tiqr_Message_Exception_SendFailure(sprintf('Invalid status code : %s. Server : %s. Response : "%s".', $statusCode, $remoteip, $result), true);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type true; however, parameter $values of sprintf() does only seem to accept double|integer|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
            throw new Tiqr_Message_Exception_SendFailure(sprintf('Invalid status code : %s. Server : %s. Response : "%s".', $statusCode, $remoteip, /** @scrutinizer ignore-type */ $result), true);
Loading history...
163
        }
164
165
        // handle errors, ignoring registration_id's
166
        $response = json_decode($result, true);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type true; however, parameter $json of json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

166
        $response = json_decode(/** @scrutinizer ignore-type */ $result, true);
Loading history...
167
        foreach ($response as $k => $v) {
168
            if ($k=="error") {
169
                throw new Tiqr_Message_Exception_SendFailure(sprintf("Error in FCM response: %s", $result), true);
170
            }
171
        }
172
    }
173
}
174