Mandrill::castError()   A
last analyzed

Complexity

Conditions 4
Paths 3

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 4
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 8
rs 10
1
<?php
2
3
use Composer\CaBundle\CaBundle;
4
5
/**
6
 * This is a customised version of the mandrill api sdk available at
7
 * @https://bitbucket.org/mailchimp/mandrill-api-php/
8
 *
9
 * - Fixes follow location (disable follow location in curl and replace curl_exec)
10
 * - Add IDE helpers
11
 * - Fix ca_cert.pem if not available
12
 */
13
require_once 'Mandrill/Templates.php';
14
require_once 'Mandrill/Exports.php';
15
require_once 'Mandrill/Users.php';
16
require_once 'Mandrill/Rejects.php';
17
require_once 'Mandrill/Inbound.php';
18
require_once 'Mandrill/Tags.php';
19
require_once 'Mandrill/Messages.php';
20
require_once 'Mandrill/Whitelists.php';
21
require_once 'Mandrill/Ips.php';
22
require_once 'Mandrill/Internal.php';
23
require_once 'Mandrill/Subaccounts.php';
24
require_once 'Mandrill/Urls.php';
25
require_once 'Mandrill/Webhooks.php';
26
require_once 'Mandrill/Senders.php';
27
require_once 'Mandrill/Metadata.php';
28
require_once 'Mandrill/Exceptions.php';
29
30
/**
31
 * A mandrill api client
32
 */
33
class Mandrill
34
{
35
    /**
36
     * @var string
37
     */
38
    public $apikey;
39
    /**
40
     * @var resource|CurlHandle
41
     */
42
    public $ch;
43
    /**
44
     * @var string
45
     */
46
    public $root = 'https://mandrillapp.com/api/1.0';
47
    /**
48
     * @var boolean
49
     */
50
    public $debug = false;
51
    /**
52
     * @var Mandrill_Templates
53
     */
54
    public $templates;
55
    /**
56
     * @var Mandrill_Exports
57
     */
58
    public $exports;
59
    /**
60
     * @var Mandrill_Users
61
     */
62
    public $users;
63
    /**
64
     * @var Mandrill_Rejects
65
     */
66
    public $rejects;
67
    /**
68
     * @var Mandrill_Inbound
69
     */
70
    public $inbound;
71
    /**
72
     * @var Mandrill_Tags
73
     */
74
    public $tags;
75
    /**
76
     * @var Mandrill_Messages
77
     */
78
    public $messages;
79
    /**
80
     * @var Mandrill_Whitelists
81
     */
82
    public $whitelists;
83
    /**
84
     * @var Mandrill_Ips
85
     */
86
    public $ips;
87
    /**
88
     * @var Mandrill_Internal
89
     */
90
    public $internal;
91
    /**
92
     * @var Mandrill_Subaccounts
93
     */
94
    public $subaccounts;
95
    /**
96
     * @var Mandrill_Urls
97
     */
98
    public $urls;
99
    /**
100
     * @var Mandrill_Webhooks
101
     */
102
    public $webhooks;
103
    /**
104
     * @var Mandrill_Senders
105
     */
106
    public $senders;
107
    /**
108
     * @var Mandrill_Metadata
109
     */
110
    public $metadata;
111
    /**
112
     * @var array
113
     */
114
    public static $error_map = array(
115
        "ValidationError" => "Mandrill_ValidationError",
116
        "Invalid_Key" => "Mandrill_Invalid_Key",
117
        "PaymentRequired" => "Mandrill_PaymentRequired",
118
        "Unknown_Subaccount" => "Mandrill_Unknown_Subaccount",
119
        "Unknown_Template" => "Mandrill_Unknown_Template",
120
        "ServiceUnavailable" => "Mandrill_ServiceUnavailable",
121
        "Unknown_Message" => "Mandrill_Unknown_Message",
122
        "Invalid_Tag_Name" => "Mandrill_Invalid_Tag_Name",
123
        "Invalid_Reject" => "Mandrill_Invalid_Reject",
124
        "Unknown_Sender" => "Mandrill_Unknown_Sender",
125
        "Unknown_Url" => "Mandrill_Unknown_Url",
126
        "Unknown_TrackingDomain" => "Mandrill_Unknown_TrackingDomain",
127
        "Invalid_Template" => "Mandrill_Invalid_Template",
128
        "Unknown_Webhook" => "Mandrill_Unknown_Webhook",
129
        "Unknown_InboundDomain" => "Mandrill_Unknown_InboundDomain",
130
        "Unknown_InboundRoute" => "Mandrill_Unknown_InboundRoute",
131
        "Unknown_Export" => "Mandrill_Unknown_Export",
132
        "IP_ProvisionLimit" => "Mandrill_IP_ProvisionLimit",
133
        "Unknown_Pool" => "Mandrill_Unknown_Pool",
134
        "NoSendingHistory" => "Mandrill_NoSendingHistory",
135
        "PoorReputation" => "Mandrill_PoorReputation",
136
        "Unknown_IP" => "Mandrill_Unknown_IP",
137
        "Invalid_EmptyDefaultPool" => "Mandrill_Invalid_EmptyDefaultPool",
138
        "Invalid_DeleteDefaultPool" => "Mandrill_Invalid_DeleteDefaultPool",
139
        "Invalid_DeleteNonEmptyPool" => "Mandrill_Invalid_DeleteNonEmptyPool",
140
        "Invalid_CustomDNS" => "Mandrill_Invalid_CustomDNS",
141
        "Invalid_CustomDNSPending" => "Mandrill_Invalid_CustomDNSPending",
142
        "Metadata_FieldLimit" => "Mandrill_Metadata_FieldLimit",
143
        "Unknown_MetadataField" => "Mandrill_Unknown_MetadataField"
144
    );
145
146
    public function __construct($apikey = null)
147
    {
148
        if (!$apikey) {
149
            $apikey = getenv('MANDRILL_APIKEY');
150
        }
151
        if (!$apikey) {
152
            $apikey = $this->readConfigs();
153
        }
154
        if (!$apikey) {
155
            throw new Mandrill_Error('You must provide a Mandrill API key');
156
        }
157
        $this->apikey = $apikey;
158
159
        $this->ch = curl_init();
160
        curl_setopt($this->ch, CURLOPT_USERAGENT, 'Mandrill-PHP/1.0.54');
161
        curl_setopt($this->ch, CURLOPT_POST, true);
162
        // We use our own follow, see function curl_exec_follow
163
        //        curl_setopt($this->ch, CURLOPT_FOLLOWLOCATION, true);
164
        curl_setopt($this->ch, CURLOPT_HEADER, false);
165
        curl_setopt($this->ch, CURLOPT_RETURNTRANSFER, true);
166
        curl_setopt($this->ch, CURLOPT_CONNECTTIMEOUT, 30);
167
        curl_setopt($this->ch, CURLOPT_TIMEOUT, 600);
168
169
        // fix ca cert permissions
170
        if (strlen(ini_get('curl.cainfo')) === 0) {
171
            curl_setopt($this->ch, CURLOPT_CAINFO, CaBundle::getBundledCaBundlePath());
172
        }
173
174
        $this->root = rtrim($this->root, '/') . '/';
175
176
        $this->templates = new Mandrill_Templates($this);
177
        $this->exports = new Mandrill_Exports($this);
178
        $this->users = new Mandrill_Users($this);
179
        $this->rejects = new Mandrill_Rejects($this);
180
        $this->inbound = new Mandrill_Inbound($this);
181
        $this->tags = new Mandrill_Tags($this);
182
        $this->messages = new Mandrill_Messages($this);
183
        $this->whitelists = new Mandrill_Whitelists($this);
184
        $this->ips = new Mandrill_Ips($this);
185
        $this->internal = new Mandrill_Internal($this);
186
        $this->subaccounts = new Mandrill_Subaccounts($this);
187
        $this->urls = new Mandrill_Urls($this);
188
        $this->webhooks = new Mandrill_Webhooks($this);
189
        $this->senders = new Mandrill_Senders($this);
190
        $this->metadata = new Mandrill_Metadata($this);
191
    }
192
193
    /**
194
     * A workaround for cURL: follow locations with safe_mode enabled or open_basedir set
195
     *
196
     * This method is used in the call method in Mandrill.php instead of the original curl_exec
197
     *
198
     * @link http://slopjong.de/2012/03/31/curl-follow-locations-with-safe_mode-enabled-or-open_basedir-set/
199
     * @param resource|CurlHandle $ch
200
     * @param int $maxredirect
201
     * @return boolean
202
     */
203
    public static function curl_exec_follow($ch, &$maxredirect = null)
204
    {
205
        $mr = $maxredirect === null ? 5 : intval($maxredirect);
206
207
        if (ini_get('open_basedir') == '' && ini_get('safe_mode') == 'Off') {
208
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, $mr > 0);
209
            curl_setopt($ch, CURLOPT_MAXREDIRS, $mr);
210
            // curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
211
        } else {
212
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, false);
213
214
            if ($mr > 0) {
215
                $original_url = curl_getinfo($ch, CURLINFO_EFFECTIVE_URL);
216
                $newurl = $original_url;
217
218
                $rch = curl_copy_handle($ch);
219
220
                curl_setopt($rch, CURLOPT_HEADER, true);
221
                curl_setopt($rch, CURLOPT_NOBODY, true);
222
                curl_setopt($rch, CURLOPT_FORBID_REUSE, false);
223
                do {
224
                    curl_setopt($rch, CURLOPT_URL, $newurl);
225
                    $header = curl_exec($rch);
226
                    if (curl_errno($rch)) {
227
                        $code = 0;
228
                    } else {
229
                        $code = curl_getinfo($rch, CURLINFO_HTTP_CODE);
230
                        if ($code == 301 || $code == 302) {
231
                            preg_match('/Location:(.*?)\n/', $header, $matches);
0 ignored issues
show
Bug introduced by
It seems like $header can also be of type true; however, parameter $subject of preg_match() 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

231
                            preg_match('/Location:(.*?)\n/', /** @scrutinizer ignore-type */ $header, $matches);
Loading history...
232
                            $newurl = trim(array_pop($matches));
233
234
                            // if no scheme is present then the new url is a
235
                            // relative path and thus needs some extra care
236
                            if (!preg_match("/^https?:/i", $newurl)) {
237
                                $newurl = $original_url . $newurl;
238
                            }
239
                        } else {
240
                            $code = 0;
241
                        }
242
                    }
243
                } while ($code && --$mr);
244
245
                curl_close($rch);
246
247
                if (!$mr) {
248
                    if ($maxredirect === null) {
249
                        trigger_error('Too many redirects.', E_USER_WARNING);
250
                    } else {
251
                        $maxredirect = 0;
252
                    }
253
254
                    return false;
255
                }
256
                curl_setopt($ch, CURLOPT_URL, $newurl);
257
            }
258
        }
259
        return curl_exec($ch);
0 ignored issues
show
Bug Best Practice introduced by
The expression return curl_exec($ch) also could return the type string which is incompatible with the documented return type boolean.
Loading history...
260
    }
261
262
    public function __destruct()
263
    {
264
        curl_close($this->ch);
265
    }
266
267
    public function call($url, $params)
268
    {
269
        $params['key'] = $this->apikey;
270
        $params = json_encode($params);
271
        $ch = $this->ch;
272
273
        curl_setopt($ch, CURLOPT_URL, $this->root . $url . '.json');
274
        curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: application/json'));
275
        curl_setopt($ch, CURLOPT_POSTFIELDS, $params);
276
        curl_setopt($ch, CURLOPT_VERBOSE, $this->debug);
277
278
        $start = microtime(true);
279
        $this->log('Call to ' . $this->root . $url . '.json: ' . $params);
280
        if ($this->debug) {
281
            $curl_buffer = fopen('php://memory', 'w+');
282
            curl_setopt($ch, CURLOPT_STDERR, $curl_buffer);
283
        }
284
285
        //        $response_body = curl_exec($ch);
286
        $response_body = self::curl_exec_follow($ch);
287
        $info = curl_getinfo($ch);
288
        $time = microtime(true) - $start;
289
        if ($this->debug) {
290
            rewind($curl_buffer);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $curl_buffer does not seem to be defined for all execution paths leading up to this point.
Loading history...
291
            $this->log(stream_get_contents($curl_buffer));
292
            fclose($curl_buffer);
293
        }
294
        $this->log('Completed in ' . number_format($time * 1000, 2) . 'ms');
295
        $this->log('Got response: ' . $response_body);
296
297
        if (curl_error($ch)) {
298
            throw new Mandrill_HttpError("API call to $url failed: " . curl_error($ch));
299
        }
300
        $result = json_decode($response_body, true);
0 ignored issues
show
Bug introduced by
$response_body of type boolean is incompatible with the type string expected by parameter $json of json_decode(). ( Ignorable by Annotation )

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

300
        $result = json_decode(/** @scrutinizer ignore-type */ $response_body, true);
Loading history...
301
        if ($result === null) {
302
            throw new Mandrill_Error('We were unable to decode the JSON response from the Mandrill API: ' . $response_body);
303
        }
304
305
        if (floor($info['http_code'] / 100) >= 4) {
306
            throw $this->castError($result);
307
        }
308
309
        return $result;
310
    }
311
312
    public function readConfigs()
313
    {
314
        $paths = array('~/.mandrill.key', '/etc/mandrill.key');
315
        foreach ($paths as $path) {
316
            if (file_exists($path)) {
317
                $apikey = trim(file_get_contents($path));
318
                if ($apikey) {
319
                    return $apikey;
320
                }
321
            }
322
        }
323
        return false;
324
    }
325
326
    public function castError($result)
327
    {
328
        if ($result['status'] !== 'error' || !$result['name']) {
329
            throw new Mandrill_Error('We received an unexpected error: ' . json_encode($result));
330
        }
331
332
        $class = (isset(self::$error_map[$result['name']])) ? self::$error_map[$result['name']] : 'Mandrill_Error';
333
        return new $class($result['message'], $result['code']);
334
    }
335
336
    public function log($msg)
337
    {
338
        if ($this->debug) {
339
            error_log($msg);
340
        }
341
    }
342
}
343