Passed
Push — master ( 35fb8a...2dccb2 )
by Pieter van der
05:32 queued 14s
created

Tiqr_Message_APNS2::send()   C

Complexity

Conditions 11
Paths 43

Size

Total Lines 105
Code Lines 68

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 132

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 68
c 2
b 0
f 0
dl 0
loc 105
ccs 0
cts 85
cp 0
rs 6.5515
cc 11
nc 43
nop 0
crap 132

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