Passed
Pull Request — develop (#52)
by Peter
11:22
created

Tiqr_Message_FCM   A

Complexity

Total Complexity 13

Size/Duplication

Total Lines 131
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 12
Bugs 0 Features 0
Metric Value
wmc 13
eloc 71
c 12
b 0
f 0
dl 0
loc 131
ccs 0
cts 90
cp 0
rs 10

3 Methods

Rating   Name   Duplication   Size   Complexity  
A send() 0 12 1
B _sendFirebase() 0 62 9
A getGoogleAccessToken() 0 30 3
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
        $url = $this->getCustomProperty('challenge');
45
46
        $this->_sendFirebase($translatedAddress, $alertText, $url, $projectId, $credentialsFile, $cacheTokens, $tokenCacheDir);
47
    }
48
49
    /**
50
     * @throws Tiqr_Message_Exception_SendFailure
51
     */
52
    private function getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCacheDir )
53
    {
54
        $client = new Google_Client();
55
        $client->setLogger($this->logger);
56
        // Try to add a file based cache for accesstokens, if configured
57
        if ($cacheTokens) {
58
            //set up the cache
59
            $filesystemAdapter = new Local($tokenCacheDir);
60
            $filesystem = new Filesystem($filesystemAdapter);
61
            $pool = new FilesystemCachePool($filesystem);
62
63
            //set up a callback to log token refresh
64
            $logger=$this->logger;
65
            $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

65
            $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...
66
                $logger->info(sprintf('New access token received at cache key %s', $cacheKey));
67
            };
68
            $client->setTokenCallback($tokenCallback);
69
            $client->setCache($pool);
70
        } else {
71
            $this->logger->warning("Cache for oAuth tokens is disabled");
72
        }
73
        try {
74
            $client->setAuthConfig($credentialsFile);
75
        } catch (\Google\Exception $e) {
76
            throw new Tiqr_Message_Exception_SendFailure(sprintf("Error setting Google credentials for FCM : %s", $e->getMessage()), true, $e);
77
        }
78
        $client->addScope('https://www.googleapis.com/auth/firebase.messaging');
79
        $client->fetchAccessTokenWithAssertion();
80
        $token = $client->getAccessToken();
81
        return $token['access_token'];
82
    }
83
84
    /**
85
     * Send a message to a device using the firebase API key.
86
     *
87
     * @param  $deviceToken     string device ID
88
     * @param  $alert           string alert message
89
     * @param  $challenge       string tiqr challenge url
90
     * @param  $projectId       string the id of the firebase project
91
     * @param  $credentialsFile string The location of the firebase secret json
92
     * @param  $cacheTokens     bool Enable caching the accesstokens for accessing the Google API
93
     * @param  $tokenCacheDir   string Location for storing the accesstoken cache
94
     * @param  $retry           boolean is this a 2nd attempt
95
     * @throws Tiqr_Message_Exception_SendFailure
96
     */
97
    private function _sendFirebase(string $deviceToken, string $alert, string $challenge, string $projectId, string $credentialsFile, bool $cacheTokens, string $tokenCacheDir, bool $retry=false)
98
    {
99
        $apiurl = sprintf('https://fcm.googleapis.com/v1/projects/%s/messages:send',$projectId);
100
101
        $fields = [
102
            'message' => [
103
                'token' => $deviceToken,
104
                'data' => [
105
                    'challenge' => $challenge,
106
                    'text'      => $alert,
107
                ],
108
                "android" => [
109
                    "ttl" => "300s",
110
                ],
111
            ],
112
        ];
113
114
        try {
115
            $headers = array(
116
                'Authorization: Bearer ' . $this->getGoogleAccessToken($credentialsFile, $cacheTokens, $tokenCacheDir),
117
                'Content-Type: application/json',
118
            );
119
        } catch (\Google\Exception $e) {
120
            throw new Tiqr_Message_Exception_SendFailure(sprintf("Error getting Goosle access token : %s", $e->getMessage()), true);
121
        }
122
123
        $ch = curl_init();
124
        curl_setopt($ch, CURLOPT_URL, $apiurl);
125
        curl_setopt($ch, CURLOPT_POST, true);
126
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
127
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
128
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
129
        $result = curl_exec($ch);
130
        $errors = curl_error($ch);
131
        $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
132
        $remoteip = curl_getinfo($ch, CURLINFO_PRIMARY_IP);
133
        curl_close($ch);
134
135
        if ($result === false) {
136
            throw new Tiqr_Message_Exception_SendFailure("Server unavailable", true);
137
        }
138
139
        if (!empty($errors)) {
140
            throw new Tiqr_Message_Exception_SendFailure("Http error occurred: ". $errors, true);
141
        }
142
143
        // Wait and retry once in case of a 502 Bad Gateway error
144
        if ($statusCode === 502 && !($retry)) {
145
            sleep(2);
146
            $this->_sendFirebase($deviceToken, $alert, $challenge, $projectId, $credentialsFile,  $cacheTokens,  $tokenCacheDir, true);
147
            return;
148
        }
149
150
        if ($statusCode !== 200) {
151
            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

151
            throw new Tiqr_Message_Exception_SendFailure(sprintf('Invalid status code : %s. Server : %s. Response : "%s".', $statusCode, $remoteip, /** @scrutinizer ignore-type */ $result), true);
Loading history...
152
        }
153
154
        // handle errors, ignoring registration_id's
155
        $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

155
        $response = json_decode(/** @scrutinizer ignore-type */ $result, true);
Loading history...
156
        foreach ($response as $k => $v) {
157
            if ($k=="error") {
158
                throw new Tiqr_Message_Exception_SendFailure(sprintf("Error in FCM response: %s", $result), true);
159
            }
160
        }
161
    }
162
}
163