Tiqr_Message_FCM::_sendFirebase()   B
last analyzed

Complexity

Conditions 10
Paths 16

Size

Total Lines 70
Code Lines 44

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 110

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 44
c 7
b 0
f 0
dl 0
loc 70
ccs 0
cts 48
cp 0
rs 7.6666
cc 10
nc 16
nop 8
crap 110

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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