Passed
Push — master ( 01bab4...e0f3b3 )
by KwangSeob
03:09
created

JiraClient::closeCURLHandle()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 8
nc 4
nop 4
dl 0
loc 12
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace JiraRestApi;
4
5
use JiraRestApi\Configuration\ConfigurationInterface;
6
use JiraRestApi\Configuration\DotEnvConfiguration;
7
use Monolog\Handler\StreamHandler;
8
use Monolog\Logger as Logger;
9
10
/**
11
 * Interact jira server with REST API.
12
 */
13
class JiraClient
14
{
15
    /**
16
     * Json Mapper.
17
     *
18
     * @var \JsonMapper
19
     */
20
    protected $json_mapper;
21
22
    /**
23
     * HTTP response code.
24
     *
25
     * @var string
26
     */
27
    protected $http_response;
28
29
    /**
30
     * JIRA REST API URI.
31
     *
32
     * @var string
33
     */
34
    private $api_uri = '/rest/api/2';
35
36
    /**
37
     * CURL instance.
38
     *
39
     * @var resource
40
     */
41
    protected $curl;
42
43
    /**
44
     * Monolog instance.
45
     *
46
     * @var \Monolog\Logger
47
     */
48
    protected $log;
49
50
    /**
51
     * Jira Rest API Configuration.
52
     *
53
     * @var ConfigurationInterface
54
     */
55
    protected $configuration;
56
57
    /**
58
     * Constructor.
59
     *
60
     * @param ConfigurationInterface $configuration
61
     * @param Logger                 $logger
62
     * @param string                 $path
63
     *
64
     * @throws JiraException
65
     * @throws \Exception
66
     */
67
    public function __construct(ConfigurationInterface $configuration = null, Logger $logger = null, $path = './')
68
    {
69
        if ($configuration === null) {
70
            if (!file_exists($path.'.env')) {
71
                // If calling the getcwd() on laravel it will returning the 'public' directory.
72
                $path = '../';
73
            }
74
            $configuration = new DotEnvConfiguration($path);
75
        }
76
77
        $this->configuration = $configuration;
78
        $this->json_mapper = new \JsonMapper();
79
80
        // Fix "\JiraRestApi\JsonMapperHelper::class" syntax error, unexpected 'class' (T_CLASS), expecting identifier (T_STRING) or variable (T_VARIABLE) or '{' or '$'
81
        $this->json_mapper->undefinedPropertyHandler = [new \JiraRestApi\JsonMapperHelper(), 'setUndefinedProperty'];
0 ignored issues
show
Documentation Bug introduced by
It seems like array(new JiraRestApi\Js...'setUndefinedProperty') of type array<integer,string|Jir...stApi\JsonMapperHelper> is incompatible with the declared type callable of property $undefinedPropertyHandler.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
82
83
        // create logger
84
        if ($logger) {
85
            $this->log = $logger;
86
        } else {
87
            $this->log = new Logger('JiraClient');
88
            $this->log->pushHandler(new StreamHandler(
89
                $configuration->getJiraLogFile(),
90
                $this->convertLogLevel($configuration->getJiraLogLevel())
91
            ));
92
        }
93
94
        $this->http_response = 200;
95
    }
96
97
    /**
98
     * Convert log level.
99
     *
100
     * @param $log_level
101
     *
102
     * @return int
103
     */
104
    private function convertLogLevel($log_level)
105
    {
106
        $log_level = strtoupper($log_level);
107
108
        switch ($log_level) {
109
            case 'EMERGENCY':
110
                return Logger::EMERGENCY;
111
            case 'ALERT':
112
                return Logger::ALERT;
113
            case 'CRITICAL':
114
                return Logger::CRITICAL;
115
            case 'ERROR':
116
                return Logger::ERROR;
117
            case 'WARNING':
118
                return Logger::WARNING;
119
            case 'NOTICE':
120
                return Logger::NOTICE;
121
            case 'DEBUG':
122
                return Logger::DEBUG;
123
            case 'INFO':
124
                return Logger::INFO;
125
            default:
126
                return Logger::WARNING;
127
        }
128
    }
129
130
    /**
131
     * Serilize only not null field.
132
     *
133
     * @param array $haystack
134
     *
135
     * @return array
136
     */
137
    protected function filterNullVariable($haystack)
138
    {
139
        foreach ($haystack as $key => $value) {
140
            if (is_array($value)) {
141
                $haystack[$key] = $this->filterNullVariable($haystack[$key]);
142
            } elseif (is_object($value)) {
143
                $haystack[$key] = $this->filterNullVariable(get_class_vars(get_class($value)));
144
            }
145
146
            if (is_null($haystack[$key]) || empty($haystack[$key])) {
147
                unset($haystack[$key]);
148
            }
149
        }
150
151
        return $haystack;
152
    }
153
154
    /**
155
     * Execute REST request.
156
     *
157
     * @param string $context        Rest API context (ex.:issue, search, etc..)
158
     * @param string $post_data
159
     * @param string $custom_request [PUT|DELETE]
160
     * @param string $cookieFile cookie file
161
     *
162
     * @throws JiraException
163
     *
164
     * @return string
165
     */
166
    public function exec($context, $post_data = null, $custom_request = null, $cookieFile = null)
167
    {
168
        $url = $this->createUrlByContext($context);
169
170
        $this->log->addInfo("Curl $custom_request: $url JsonData=".$post_data);
171
172
        $ch = curl_init();
173
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
174
        curl_setopt($ch, CURLOPT_URL, $url);
175
176
        // post_data
177
        if (!is_null($post_data)) {
178
            // PUT REQUEST
179
            if (!is_null($custom_request) && $custom_request == 'PUT') {
180
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'PUT');
181
                curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
182
            }
183
            if (!is_null($custom_request) && $custom_request == 'DELETE') {
184
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
185
            } else {
186
                curl_setopt($ch, CURLOPT_POST, true);
187
                curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data);
188
            }
189
        } else {
190
            if (!is_null($custom_request) && $custom_request == 'DELETE') {
191
                curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'DELETE');
192
            }
193
        }
194
195
        $this->authorization($ch, $cookieFile);
196
197
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->getConfiguration()->isCurlOptSslVerifyHost());
198
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->getConfiguration()->isCurlOptSslVerifyPeer());
199
        curl_setopt($ch, CURLOPT_USERAGENT, $this->getConfiguration()->getCurlOptUserAgent());
200
201
        // curl_setopt(): CURLOPT_FOLLOWLOCATION cannot be activated when an open_basedir is set
202
        if (!function_exists('ini_get') || !ini_get('open_basedir')) {
203
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
204
        }
205
206
        curl_setopt($ch, CURLOPT_HTTPHEADER,
207
            ['Accept: */*', 'Content-Type: application/json', 'X-Atlassian-Token: no-check']);
208
209
        curl_setopt($ch, CURLOPT_VERBOSE, $this->getConfiguration()->isCurlOptVerbose());
210
211
        $this->log->addDebug('Curl exec='.$url);
212
        $response = curl_exec($ch);
213
214
        // if request failed.
215
        if (!$response) {
216
            $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
217
            $body = curl_error($ch);
218
            curl_close($ch);
219
220
            /*
221
             * 201: The request has been fulfilled, resulting in the creation of a new resource.
222
             * 204: The server successfully processed the request, but is not returning any content.
223
             */
224
            if ($this->http_response === 204 || $this->http_response === 201) {
225
                return true;
0 ignored issues
show
Bug Best Practice introduced by
The expression return true returns the type true which is incompatible with the documented return type string.
Loading history...
226
            }
227
228
            // HostNotFound, No route to Host, etc Network error
229
            $msg = sprintf('CURL Error: http response=%d, %s', $this->http_response, $body);
230
231
            $this->log->addError($msg);
232
233
            throw new JiraException($msg);
234
        } else {
235
            // if request was ok, parsing http response code.
236
            $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
237
238
            curl_close($ch);
239
240
            // don't check 301, 302 because setting CURLOPT_FOLLOWLOCATION
241
            if ($this->http_response != 200 && $this->http_response != 201) {
242
                throw new JiraException('CURL HTTP Request Failed: Status Code : '
243
                    .$this->http_response.', URL:'.$url
244
                    ."\nError Message : ".$response, $this->http_response);
245
            }
246
        }
247
248
        return $response;
249
    }
250
251
    /**
252
     * Create upload handle.
253
     *
254
     * @param string $url         Request URL
255
     * @param string $upload_file Filename
256
     *
257
     * @return resource
258
     */
259
    private function createUploadHandle($url, $upload_file)
260
    {
261
        $ch = curl_init();
262
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
263
        curl_setopt($ch, CURLOPT_URL, $url);
264
265
        // send file
266
        curl_setopt($ch, CURLOPT_POST, true);
267
268
        if (PHP_MAJOR_VERSION == 5 && PHP_MINOR_VERSION < 5) {
269
            $attachments = realpath($upload_file);
270
            $filename = basename($upload_file);
271
272
            curl_setopt($ch, CURLOPT_POSTFIELDS,
273
                ['file' => '@'.$attachments.';filename='.$filename]);
274
275
            $this->log->addDebug('using legacy file upload');
276
        } else {
277
            // CURLFile require PHP > 5.5
278
            $attachments = new \CURLFile(realpath($upload_file));
279
            $attachments->setPostFilename(basename($upload_file));
280
281
            curl_setopt($ch, CURLOPT_POSTFIELDS,
282
                ['file' => $attachments]);
283
284
            $this->log->addDebug('using CURLFile='.var_export($attachments, true));
285
        }
286
287
        $this->authorization($ch);
288
289
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->getConfiguration()->isCurlOptSslVerifyHost());
290
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->getConfiguration()->isCurlOptSslVerifyPeer());
291
292
        // curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); cannot be activated when an open_basedir is set
293
        if (!function_exists('ini_get') || !ini_get('open_basedir')) {
294
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
295
        }
296
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
297
            'Accept: */*',
298
            'Content-Type: multipart/form-data',
299
            'X-Atlassian-Token: nocheck',
300
        ]);
301
302
        curl_setopt($ch, CURLOPT_VERBOSE, $this->getConfiguration()->isCurlOptVerbose());
303
304
        $this->log->addDebug('Curl exec='.$url);
305
306
        return $ch;
307
    }
308
309
    /**
310
     * File upload.
311
     *
312
     * @param string $context       url context
313
     * @param array  $filePathArray upload file path.
314
     *
315
     * @throws JiraException
316
     *
317
     * @return array
318
     */
319
    public function upload($context, $filePathArray)
320
    {
321
        $url = $this->createUrlByContext($context);
322
323
        // return value
324
        $result_code = 200;
325
326
        $chArr = [];
327
        $results = [];
328
        $mh = curl_multi_init();
329
330
        for ($idx = 0; $idx < count($filePathArray); $idx++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
331
            $file = $filePathArray[$idx];
332
            if (file_exists($file) == false) {
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
333
                $body = "File $file not found";
334
                $result_code = -1;
335
                $this->closeCURLHandle($chArr, $mh, $body, $result_code);
336
337
                return $results;
338
            }
339
            $chArr[$idx] = $this->createUploadHandle($url, $filePathArray[$idx]);
340
341
            curl_multi_add_handle($mh, $chArr[$idx]);
342
        }
343
344
        $running = null;
345
        do {
346
            curl_multi_exec($mh, $running);
347
        } while ($running > 0);
348
349
        // Get content and remove handles.
350
        $body = '';
351
        for ($idx = 0; $idx < count($chArr); $idx++) {
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
352
            $ch = $chArr[$idx];
353
354
            $results[$idx] = curl_multi_getcontent($ch);
355
356
            // if request failed.
357
            if (!$results[$idx]) {
358
                $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
359
                $body = curl_error($ch);
360
361
                //The server successfully processed the request, but is not returning any content.
362
                if ($this->http_response == 204) {
363
                    continue;
364
                }
365
366
                // HostNotFound, No route to Host, etc Network error
367
                $result_code = -1;
368
                $body = 'CURL Error: = '.$body;
369
                $this->log->addError($body);
370
            } else {
371
                // if request was ok, parsing http response code.
372
                $result_code = $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
373
374
                // don't check 301, 302 because setting CURLOPT_FOLLOWLOCATION
375
                if ($this->http_response != 200 && $this->http_response != 201) {
376
                    $body = 'CURL HTTP Request Failed: Status Code : '
377
                        .$this->http_response.', URL:'.$url;
378
379
                    $this->log->addError($body);
380
                }
381
            }
382
        }
383
384
        $this->closeCURLHandle($chArr, $mh, $body, $result_code);
385
386
        return $results;
387
    }
388
389
    /**
390
     * @param array $chArr
391
     * @param $mh
392
     * @param $body
393
     * @param $result_code
394
     *
395
     * @throws \JiraRestApi\JiraException
396
     */
397
    protected function closeCURLHandle(array $chArr, $mh, $body, $result_code)
398
    {
399
        foreach ($chArr as $ch) {
400
            $this->log->addDebug('CURL Close handle..');
401
            curl_multi_remove_handle($mh, $ch);
402
            curl_close($ch);
403
        }
404
        $this->log->addDebug('CURL Multi Close handle..');
405
        curl_multi_close($mh);
406
        if ($result_code != 200) {
407
            // @TODO $body might have not been defined
408
            throw new JiraException('CURL Error: = '.$body, $result_code);
409
        }
410
    }
411
412
    /**
413
     * Get URL by context.
414
     *
415
     * @param string $context
416
     *
417
     * @return string
418
     */
419
    protected function createUrlByContext($context)
420
    {
421
        $host = $this->getConfiguration()->getJiraHost();
422
423
        return $host.$this->api_uri.'/'.preg_replace('/\//', '', $context, 1);
424
    }
425
426
    /**
427
     * Add authorize to curl request.
428
     *
429
     * @param resource $ch
430
     */
431
    protected function authorization($ch, $cookieFile = null)
432
    {
433
        // use cookie
434
        if ($this->getConfiguration()->isCookieAuthorizationEnabled()) {
435
            if ($cookieFile === null){
436
                $cookieFile = $this->getConfiguration()->getCookieFile();
437
            }
438
439
            curl_setopt($ch, CURLOPT_COOKIEJAR, $cookieFile);
440
            curl_setopt($ch, CURLOPT_COOKIEFILE, $cookieFile);
441
442
            $this->log->addDebug('Using cookie..');
443
        }
444
445
        // if cookie file not exist, using id/pwd login
446
        if (!file_exists($cookieFile)) {
447
            $username = $this->getConfiguration()->getJiraUser();
448
            $password = $this->getConfiguration()->getJiraPassword();
449
            curl_setopt($ch, CURLOPT_USERPWD, "$username:$password");
450
        }
451
    }
452
453
    /**
454
     * Jira Rest API Configuration.
455
     *
456
     * @return ConfigurationInterface
457
     */
458
    public function getConfiguration()
459
    {
460
        return $this->configuration;
461
    }
462
463
    /**
464
     * Set a custom Jira API URI for the request.
465
     *
466
     * @param string $api_uri
467
     */
468
    public function setAPIUri($api_uri)
469
    {
470
        $this->api_uri = $api_uri;
471
    }
472
473
    /**
474
     * convert to query array to http query parameter.
475
     *
476
     * @param $paramArray
477
     *
478
     * @return string
479
     */
480
    public function toHttpQueryParameter($paramArray)
481
    {
482
        $queryParam = '?';
483
484
        foreach ($paramArray as $key => $value) {
485
            $v = null;
486
487
            // some param field(Ex: expand) type is array.
488
            if (is_array($value)) {
489
                $v = implode(',', $value);
490
            } else {
491
                $v = $value;
492
            }
493
494
            $queryParam .= $key.'='.$v.'&';
495
        }
496
497
        return $queryParam;
498
    }
499
500
    /**
501
     * download and save into outDir.
502
     *
503
     * @param $url full url
0 ignored issues
show
Bug introduced by
The type JiraRestApi\full was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
504
     * @param $outDir save dir
505
     * @param $file save filename
0 ignored issues
show
Bug introduced by
The type JiraRestApi\save was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
506
     * @param $cookieFile cookie filename
0 ignored issues
show
Bug introduced by
The type JiraRestApi\cookie was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
507
     *
508
     * @throws JiraException
509
     *
510
     * @return bool|mixed
511
     */
512
    public function download($url, $outDir, $file, $cookieFile = null)
513
    {
514
        $file = fopen($outDir.DIRECTORY_SEPARATOR.$file, 'w');
515
516
        $ch = curl_init();
517
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
518
        curl_setopt($ch, CURLOPT_URL, $url);
519
520
        // output to file handle
521
        curl_setopt($ch, CURLOPT_FILE, $file);
522
523
        $this->authorization($ch, $cookieFile);
524
525
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $this->getConfiguration()->isCurlOptSslVerifyHost());
526
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->getConfiguration()->isCurlOptSslVerifyPeer());
527
528
        // curl_setopt(): CURLOPT_FOLLOWLOCATION cannot be activated when an open_basedir is set
529
        if (!function_exists('ini_get') || !ini_get('open_basedir')) {
530
            curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
531
        }
532
533
        curl_setopt($ch, CURLOPT_HTTPHEADER,
534
            ['Accept: */*', 'Content-Type: application/json', 'X-Atlassian-Token: no-check']);
535
536
        curl_setopt($ch, CURLOPT_VERBOSE, $this->getConfiguration()->isCurlOptVerbose());
537
538
        $this->log->addDebug('Curl exec='.$url);
539
        $response = curl_exec($ch);
540
541
        // if request failed.
542
        if (!$response) {
543
            $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
544
            $body = curl_error($ch);
545
            curl_close($ch);
546
            fclose($file);
0 ignored issues
show
Bug introduced by
It seems like $file can also be of type false; however, parameter $handle of fclose() does only seem to accept resource, 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

546
            fclose(/** @scrutinizer ignore-type */ $file);
Loading history...
547
548
            /*
549
             * 201: The request has been fulfilled, resulting in the creation of a new resource.
550
             * 204: The server successfully processed the request, but is not returning any content.
551
             */
552
            if ($this->http_response === 204 || $this->http_response === 201) {
553
                return true;
554
            }
555
556
            // HostNotFound, No route to Host, etc Network error
557
            $msg = sprintf('CURL Error: http response=%d, %s', $this->http_response, $body);
558
559
            $this->log->addError($msg);
560
561
            throw new JiraException($msg);
562
        } else {
563
            // if request was ok, parsing http response code.
564
            $this->http_response = curl_getinfo($ch, CURLINFO_HTTP_CODE);
565
566
            curl_close($ch);
567
            fclose($file);
568
569
            // don't check 301, 302 because setting CURLOPT_FOLLOWLOCATION
570
            if ($this->http_response != 200 && $this->http_response != 201) {
571
                throw new JiraException('CURL HTTP Request Failed: Status Code : '
572
                    .$this->http_response.', URL:'.$url
573
                    ."\nError Message : ".$response, $this->http_response);
574
            }
575
        }
576
577
        return $response;
578
    }
579
580
    /**
581
     * setting cookie file path.
582
     *
583
     * @param $cookieFile
584
     * @return $this
585
     */
586
    public function setCookieFile($cookieFile)
587
    {
588
        $this->cookieFile = $cookieFile;
0 ignored issues
show
Bug Best Practice introduced by
The property cookieFile does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
589
590
        return $this;
591
    }
592
}
593