Tiqr_Message_APNS2::send()   C
last analyzed

Complexity

Conditions 13
Paths 47

Size

Total Lines 117
Code Lines 73

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 182

Importance

Changes 6
Bugs 0 Features 0
Metric Value
eloc 73
c 6
b 0
f 0
dl 0
loc 117
ccs 0
cts 77
cp 0
rs 5.8823
cc 13
nc 47
nop 0
crap 182

How to fix   Long Method    Complexity   

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:

1
<?php
2
3
use Pushok\AuthProvider;
4
use Pushok\Client;
5
use Pushok\Notification;
6
use Pushok\Payload;
7
use Pushok\Payload\Alert;
8
9
/**
10
 * Apple Push Notification Service message class for the HTTP/2 api.push.apple.com APNs API.
11
 */
12
class Tiqr_Message_APNS2 extends Tiqr_Message_Abstract
13
{
14
    /**
15
     * Send message.
16
     */
17
    public function send()
18
    {
19
        $curl_options = array();
20
        $options = $this->getOptions();
21
        if (isset($options['apns.proxy_host_url'])) {
22
            // Override CURL options to connect to a HTTP/1.1 to HTTP/2 proxy
23
            $curl_options[CURLOPT_URL] = $options['apns.proxy_host_url'] . '/3/device/' . $this->getAddress();
24
            $curl_options[CURLOPT_PORT] = $options['apns.proxy_host_port'] ?? 443;
25
            // Use HTTP/1.1 instead of HTTP/2
26
            $curl_options[CURLOPT_HTTP_VERSION] = CURL_HTTP_VERSION_1_1;
27
            $this->logger->notice(sprintf('Using HTTP/1.1 CURL Proxy URL: "%s" and port "%s"',  $curl_options[CURLOPT_URL], $curl_options[CURLOPT_URL]));
28
        }
29
        else {
30
            $version_info = curl_version();
31
            if ($version_info['features'] & CURL_VERSION_HTTP2 == 0) {
32
                throw new RuntimeException('APNS2 requires HTTP/2 support in curl');
33
            }
34
        }
35
36
        // Get the UID from the client certificate we use for authentication, this
37
        // is set to the bundle ID.
38
        $options=$this->getOptions();
39
        $cert_filename = $options['apns.certificate'];
40
        if (strlen($cert_filename) == 0) {
41
            throw new RuntimeException('apns.certificate option not set');
42
        }
43
        $cert_file_contents = file_get_contents($cert_filename);
44
        if (false === $cert_file_contents) {
45
            throw new RuntimeException(
46
                sprintf('Error reading APNS client certificate file: "%s"', $cert_filename)
47
            );
48
        }
49
50
        $cert=openssl_x509_parse( $cert_file_contents );
51
        if (false === $cert) {
52
            // Log openssl error information
53
            while ($msg = openssl_error_string()) {
54
                $this->logger->error('openssl_x509_parse(): ' . $msg);
55
            }
56
            throw new RuntimeException('Error parsing APNS client certificate');
57
        }
58
        $bundle_id = $cert['subject']['UID'] ?? NULL;
59
        if (NULL === $bundle_id) {
60
            throw new RuntimeException('No uid found in the certificate subject');
61
        }
62
        $this->logger->info(sprintf('Setting bundle_id to "%s" based on UID from certificate', $bundle_id));
63
        $this->logger->info(
64
            sprintf('Authenticating using certificate with subject "%s" valid until "%s"',
65
                $cert['name'],
66
                date(DATE_RFC2822, $cert['validTo_time_t'])
67
            )
68
        );
69
70
        $authProviderOptions = [
71
            'app_bundle_id' => $bundle_id, // The bundle ID for app obtained from Apple developer account
72
            'certificate_path' => $cert_filename,
73
            'certificate_secret' => null // Private key secret
74
        ];
75
76
        $authProvider = AuthProvider\Certificate::create($authProviderOptions);
77
78
        // Create the push message
79
        $alert=Alert::create();
80
        $alert->setBody($this->getText());
81
        // Note: It is possible to specify a title and a subtitle: $alert->setTitle() && $alert->setSubtitle()
82
        //       The tiqr service currently does not implement this.
83
        $payload=Payload::create()->setAlert($alert);
84
        $payload->setSound('default');
85
        foreach ($this->getCustomProperties() as $name => $value) {
86
            $payload->setCustomValue($name, $value);
87
        }
88
        // set "mutable-content": 1 in the notification. This will call the Tiqr app when the notification arrives so
89
        // that the app can read it (and also modify it). This allows the app to be notified of the arrival of the notification
90
        // without the user interacting with it.
91
        $payload->setMutableContent(true);
92
93
        $this->logger->debug(sprintf('JSON Payload: %s', $payload->toJson()));
94
        $notification=new Notification($payload, $this->getAddress());
95
        // Set expiration to 30 seconds from now, same as Message_APNS
96
        $now = new DateTime();
97
        $expirationInstant=$now->add(new DateInterval('PT30S'));
98
        $notification->setExpirationAt($expirationInstant);
99
100
        // Send the push message
101
        $client = new Client($authProvider, $options['apns.environment'] == 'production', $curl_options);
102
        $client->addNotification($notification);
103
        $responses=$client->push();
104
        if ( sizeof($responses) != 1) {
105
            $this->logger->warning(sprintf('Unexpected number responses. Expected 1, got %d', sizeof($responses)) );
106
            if (sizeof($responses) == 0) {
107
                $this->logger->warning('Could not determine whether the notification was sent');
108
                return;
109
            }
110
        }
111
        /** @var \Pushok\Response $response */
112
        $response = reset($responses);  // Get first response from the array
113
        $deviceToken=$response->getDeviceToken() ?? '';
114
        // A canonical UUID that is the unique ID for the notification. E.g. 123e4567-e89b-12d3-a456-4266554400a0
115
        $apnsId=$response->getApnsId() ?? '';
116
        // Status code. E.g. 200 (Success), 410 (The device token is no longer active for the topic.)
117
        $statusCode=$response->getStatusCode();
118
        $this->logger->info(sprintf('Got response with ApnsId "%s", status %s for deviceToken "%s"', $apnsId, $statusCode, $deviceToken));
119
        if ( strcasecmp($deviceToken, $this->getAddress()) ) {
120
        $this->logger->warning(sprintf('Unexpected deviceToken in response. Expected: "%s"; got: "%s"', $this->getAddress(), $deviceToken));
121
        }
122
        if ($statusCode == 200) {
123
            $this->logger->notice(sprintf('Successfully sent APNS2 push notification. APNS ID: "%s"; deviceToken: "%s"', $apnsId, $deviceToken));
124
            return;
125
        }
126
127
        $reasonPhrase=$response->getReasonPhrase(); // E.g. The device token is no longer active for the topic.
128
        $errorReason=$response->getErrorReason(); // E.g. Unregistered
129
        $errorDescription=$response->getErrorDescription(); // E.g. The device token is inactive for the specified topic.
130
131
        $this->logger->error(sprintf('Error sending APNS2 push notification. APNS ID: "%s"; deviceToken: "%s"; Error: "%s" "%s" "%s"', $apnsId, $deviceToken, $reasonPhrase, $errorReason, $errorDescription));
132
        throw new RuntimeException(
133
            sprintf('Error sending APNS2 push notification. Status: %s. Error: "%s" "%s" "%s"', $statusCode, $reasonPhrase, $errorReason, $errorDescription)
134
        );
135
    }
136
}
137