• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

podio-community / podio-php / 5801311767

pending completion
5801311767

Pull #241

github

web-flow
Merge a97fe4248 into 5369410d4
Pull Request #241: fix: kint debugging for web setups + making kint an optional dependency

5 of 5 new or added lines in 1 file covered. (100.0%)

993 of 2098 relevant lines covered (47.33%)

32.41 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

32.19
/lib/PodioClient.php
1
<?php
2

3
use GuzzleHttp\Client;
4
use GuzzleHttp\Exception\GuzzleException;
5
use GuzzleHttp\Exception\RequestException;
6
use GuzzleHttp\Psr7\MultipartStream;
7
use GuzzleHttp\Psr7\Request;
8
use GuzzleHttp\Psr7\Uri;
9
use GuzzleHttp\RequestOptions;
10
use GuzzleHttp\TransferStats;
11

12
class PodioClient
13
{
14
    public $oauth;
15
    /** @var bool|string */
16
    protected $debug = false;
17
    /**
18
     * Only created/used if debug is enabled and set to 'file'.
19
     *
20
     * @var ?PodioLogger
21
     */
22
    public $logger;
23
    public $session_manager;
24
    /** @var ?PodioResponse */
25
    public $last_response;
26
    public $auth_type;
27
    /** @var \GuzzleHttp\Client */
28
    public $http_client;
29
    protected $url;
30
    protected $client_id;
31
    protected $client_secret;
32
    /** @var \Psr\Http\Message\ResponseInterface */
33
    private $last_http_response;
34

35
    public const VERSION = '7.0.0';
36

37
    public const GET = 'GET';
38
    public const POST = 'POST';
39
    public const PUT = 'PUT';
40
    public const DELETE = 'DELETE';
41

42
    public function __construct($client_id, $client_secret, $options = array('session_manager' => null, 'curl_options' => []))
43
    {
44
        // Setup client info
45
        $this->client_id = $client_id;
4✔
46
        $this->client_secret = $client_secret;
4✔
47

48
        $this->url = empty($options['api_url']) ? 'https://api.podio.com:443' : $options['api_url'];
4✔
49
        $client_config = [
4✔
50
            'base_uri' => $this->url,
4✔
51

52
            RequestOptions::HEADERS => [
4✔
53
                'Accept' => 'application/json',
4✔
54
                'User-Agent' => 'Podio PHP Client/' . self::VERSION . '-guzzle'
4✔
55
            ]
4✔
56
        ];
4✔
57
        if ($options && !empty($options['curl_options'])) {
4✔
58
            $client_config['curl'] = $options['curl_options'];
×
59
        }
60
        if (class_exists('\\Composer\\CaBundle\\CaBundle')) {
4✔
61
            /** @noinspection PhpFullyQualifiedNameUsageInspection */
62
            $client_config[RequestOptions::VERIFY] = \Composer\CaBundle\CaBundle::getSystemCaRootBundlePath();
×
63
        }
64
        $this->http_client = new Client($client_config);
4✔
65

66
        $this->session_manager = null;
4✔
67
        if ($options && !empty($options['session_manager'])) {
4✔
68
            if (is_string($options['session_manager']) && class_exists($options['session_manager'])) {
×
69
                $this->session_manager = new $options['session_manager']();
×
70
            } elseif (is_object($options['session_manager']) && method_exists($options['session_manager'], 'get') && method_exists($options['session_manager'], 'set')) {
×
71
                $this->session_manager = $options['session_manager'];
×
72
            }
73
            if ($this->session_manager) {
×
74
                $this->oauth = $this->session_manager->get();
×
75
            }
76
        }
77
    }
78

79
    public function __destruct()
80
    {
81
        $this->shutdown();
4✔
82
    }
83

84
    public function authenticate_with_app($app_id, $app_token): bool
85
    {
86
        return $this->authenticate('app', ['app_id' => $app_id, 'app_token' => $app_token]);
×
87
    }
88

89
    public function authenticate_with_password($username, $password): bool
90
    {
91
        return $this->authenticate('password', ['username' => $username, 'password' => $password]);
×
92
    }
93

94
    public function authenticate_with_authorization_code($authorization_code, $redirect_uri): bool
95
    {
96
        return $this->authenticate('authorization_code', ['code' => $authorization_code, 'redirect_uri' => $redirect_uri]);
×
97
    }
98

99
    public function refresh_access_token(): bool
100
    {
101
        return $this->authenticate('refresh_token', ['refresh_token' => $this->oauth->refresh_token]);
×
102
    }
103

104
    public function authenticate($grant_type, $attributes): bool
105
    {
106
        $data = [];
×
107
        $auth_type = ['type' => $grant_type];
×
108

109
        switch ($grant_type) {
110
            case 'password':
×
111
                $data['grant_type'] = 'password';
×
112
                $data['username'] = $attributes['username'];
×
113
                $data['password'] = $attributes['password'];
×
114

115
                $auth_type['identifier'] = $attributes['username'];
×
116
                break;
×
117
            case 'refresh_token':
×
118
                $data['grant_type'] = 'refresh_token';
×
119
                $data['refresh_token'] = $attributes['refresh_token'];
×
120
                break;
×
121
            case 'authorization_code':
×
122
                $data['grant_type'] = 'authorization_code';
×
123
                $data['code'] = $attributes['code'];
×
124
                $data['redirect_uri'] = $attributes['redirect_uri'];
×
125
                break;
×
126
            case 'app':
×
127
                $data['grant_type'] = 'app';
×
128
                $data['app_id'] = $attributes['app_id'];
×
129
                $data['app_token'] = $attributes['app_token'];
×
130

131
                $auth_type['identifier'] = $attributes['app_id'];
×
132
                break;
×
133
            default:
134
                break;
×
135
        }
136

137
        $request_data = array_merge($data, ['client_id' => $this->client_id, 'client_secret' => $this->client_secret]);
×
138
        if ($response = $this->request(self::POST, '/oauth/token', $request_data, ['oauth_request' => true])) {
×
139
            $body = $response->json_body();
×
140
            $this->oauth = new PodioOAuth($body['access_token'], $body['refresh_token'], $body['expires_in'], $body['ref'], $body['scope']);
×
141

142
            // Don't touch auth_type if we are refreshing automatically as it'll be reset to null
143
            if ($grant_type !== 'refresh_token') {
×
144
                $this->auth_type = $auth_type;
×
145
            }
146

147
            if ($this->session_manager) {
×
148
                $this->session_manager->set($this->oauth, $this->auth_type);
×
149
            }
150

151
            return true;
×
152
        }
153
        return false;
×
154
    }
155

156
    public function clear_authentication()
157
    {
158
        $this->oauth = new PodioOAuth();
×
159

160
        if ($this->session_manager) {
×
161
            $this->session_manager->set($this->oauth, $this->auth_type);
×
162
        }
163
    }
164

165
    public function authorize_url($redirect_uri, $scope): string
166
    {
167
        $parsed_url = parse_url($this->url);
×
168
        $host = str_replace('api.', '', $parsed_url['host']);
×
169
        return 'https://' . $host . '/oauth/authorize?response_type=code&client_id=' . $this->client_id . '&redirect_uri=' . rawurlencode($redirect_uri) . '&scope=' . rawurlencode($scope);
×
170
    }
171

172
    public function is_authenticated(): bool
173
    {
174
        return $this->oauth && $this->oauth->access_token;
×
175
    }
176

177
    /**
178
     * @throws PodioBadRequestError
179
     * @throws PodioConflictError
180
     * @throws PodioRateLimitError
181
     * @throws PodioUnavailableError
182
     * @throws PodioGoneError
183
     * @throws PodioDataIntegrityError
184
     * @throws PodioForbiddenError
185
     * @throws PodioNotFoundError
186
     * @throws PodioError
187
     * @throws PodioInvalidGrantError
188
     * @throws PodioAuthorizationError
189
     * @throws PodioConnectionError
190
     * @throws PodioServerError
191
     * @throws Exception when client is not setup
192
     */
193
    public function request($method, $url, $attributes = [], $options = [])
194
    {
195
        if (!$this->http_client) {
4✔
196
            throw new Exception('Client has not been setup with client id and client secret.');
×
197
        }
198

199
        $original_url = $url;
4✔
200
        $encoded_attributes = null;
4✔
201

202
        if (is_object($attributes) && substr(get_class($attributes), 0, 5) == 'Podio') {
4✔
203
            $attributes = $attributes->as_json(false);
×
204
        }
205

206
        if (!is_array($attributes) && !is_object($attributes)) {
4✔
207
            throw new PodioDataIntegrityError('Attributes must be an array');
×
208
        }
209

210
        $request = new Request($method, $url);
4✔
211
        switch ($method) {
212
            case self::DELETE:
213
            case self::GET:
214
                $request = $request->withHeader('Content-type', 'application/x-www-form-urlencoded');
4✔
215
                $request = $request->withHeader('Content-length', '0');
4✔
216

217
                $separator = strpos($url, '?') ? '&' : '?';
4✔
218
                if ($attributes) {
4✔
219
                    $query = $this->encode_attributes($attributes);
×
220
                    $request = $request->withUri(new Uri($url . $separator . $query));
×
221
                }
222
                break;
4✔
223
            case self::POST:
224
                if (!empty($options['upload'])) {
×
225
                    $request = $request->withBody(new MultipartStream([
×
226
                        [
×
227
                            'name' => 'source',
×
228
                            'contents' => fopen($attributes['filepath'], 'r'),
×
229
                            'filename' => $attributes['filename']
×
230
                        ], [
×
231
                            'name' => 'filename',
×
232
                            'contents' => $attributes['filename']
×
233
                        ]
×
234
                    ]));
×
235
                } elseif (empty($options['oauth_request'])) {
×
236
                    // application/json
237
                    $encoded_attributes = json_encode($attributes);
×
238
                    $request = $request->withBody(GuzzleHttp\Psr7\Utils::streamFor($encoded_attributes));
×
239
                    $request = $request->withHeader('Content-type', 'application/json');
×
240
                } else {
241
                    // x-www-form-urlencoded
242
                    $encoded_attributes = $this->encode_attributes($attributes);
×
243
                    $request = $request->withBody(GuzzleHttp\Psr7\Utils::streamFor($encoded_attributes));
×
244
                    $request = $request->withHeader('Content-type', 'application/x-www-form-urlencoded');
×
245
                }
246
                break;
×
247
            case self::PUT:
248
                $encoded_attributes = json_encode($attributes);
×
249
                $request = $request->withBody(GuzzleHttp\Psr7\Utils::streamFor($encoded_attributes));
×
250
                $request = $request->withHeader('Content-type', 'application/json');
×
251
                break;
×
252
        }
253

254
        // Add access token to request
255
        if (isset($this->oauth) && !empty($this->oauth->access_token) && !(isset($options['oauth_request']) && $options['oauth_request'] == true)) {
4✔
256
            $token = $this->oauth->access_token;
×
257
            $request = $request->withHeader('Authorization', "OAuth2 {$token}");
×
258
        }
259

260
        // File downloads can be of any type
261
        if (!empty($options['file_download'])) {
4✔
262
            $request = $request->withHeader('Accept', '*/*');
×
263
        }
264

265
        $response = new PodioResponse();
4✔
266

267
        try {
268
            $transferTime = 0;
4✔
269
            /** \Psr\Http\Message\ResponseInterface */
270
            $http_response = $this->http_client->send($request, [
4✔
271
                RequestOptions::HTTP_ERRORS => false,
4✔
272
                RequestOptions::ON_STATS => function (TransferStats $stats) use (&$transferTime) {
4✔
273
                    $transferTime = $stats->getTransferTime();
×
274
                }
4✔
275
            ]);
4✔
276
            $response->status = $http_response->getStatusCode();
4✔
277
            $response->headers = array_map(function ($values) {
4✔
278
                return implode(', ', $values);
×
279
            }, $http_response->getHeaders());
4✔
280
            $this->last_http_response = $http_response;
4✔
281
            if (!isset($options['return_raw_as_resource_only']) || $options['return_raw_as_resource_only'] != true) {
4✔
282
                $response->body = $http_response->getBody()->getContents();
4✔
283
            }
284
            $this->last_response = $response;
4✔
285
        } catch (RequestException $requestException) {
×
286
            throw new PodioConnectionError('Connection to Podio API failed: [' . get_class($requestException) . '] ' . $requestException->getMessage(), $requestException->getCode());
×
287
        } catch (GuzzleException $e) { // this generally should not happen as RequestOptions::HTTP_ERRORS is set to `false`
×
288
            throw new PodioConnectionError('Connection to Podio API failed: [' . get_class($e) . '] ' . $e->getMessage(), $e->getCode());
×
289
        }
290

291
        if (!isset($options['oauth_request'])) {
4✔
292
            $this->log_request($method, $url, $encoded_attributes, $response, $transferTime);
4✔
293
        }
294

295
        switch ($response->status) {
4✔
296
            case 200:
4✔
297
            case 201:
×
298
            case 204:
×
299
                if (isset($options['return_raw_as_resource_only']) && $options['return_raw_as_resource_only'] === true) {
4✔
300
                    return $http_response->getBody();
×
301
                }
302
                return $response;
4✔
303
            case 400:
×
304
                // invalid_grant_error or bad_request_error
305
                $body = $response->json_body();
×
306
                if (strstr($body['error'], 'invalid_grant')) {
×
307
                    // Reset access token & refresh_token
308
                    $this->clear_authentication();
×
309
                    throw new PodioInvalidGrantError($response->body, $response->status, $url);
×
310
                } else {
311
                    throw new PodioBadRequestError($response->body, $response->status, $url);
×
312
                }
313
            // no break
314
            case 401:
×
315
                $body = $response->json_body();
×
316
                if (strstr($body['error_description'], 'expired_token') || strstr($body['error'], 'invalid_token')) {
×
317
                    if ($this->oauth->refresh_token) {
×
318
                        // Access token is expired. Try to refresh it.
319
                        if ($this->refresh_access_token()) {
×
320
                            // Try the original request again.
321
                            return $this->request($method, $original_url, $attributes);
×
322
                        } else {
323
                            $this->clear_authentication();
×
324
                            throw new PodioAuthorizationError($response->body, $response->status, $url);
×
325
                        }
326
                    } else {
327
                        // We have tried in vain to get a new access token. Log the user out.
328
                        $this->clear_authentication();
×
329
                        throw new PodioAuthorizationError($response->body, $response->status, $url);
×
330
                    }
331
                } elseif (strstr($body['error'], 'invalid_request') || strstr($body['error'], 'unauthorized')) {
×
332
                    // Access token is invalid.
333
                    $this->clear_authentication();
×
334
                    throw new PodioAuthorizationError($response->body, $response->status, $url);
×
335
                }
336
                break;
×
337
            case 403:
×
338
                throw new PodioForbiddenError($response->body, $response->status, $url);
×
339
            case 404:
×
340
                throw new PodioNotFoundError($response->body, $response->status, $url);
×
341
            case 409:
×
342
                throw new PodioConflictError($response->body, $response->status, $url);
×
343
            case 410:
×
344
                throw new PodioGoneError($response->body, $response->status, $url);
×
345
            case 420:
×
346
                throw new PodioRateLimitError($response->body, $response->status, $url);
×
347
            case 500:
×
348
                throw new PodioServerError($response->body, $response->status, $url);
×
349
            case 502:
×
350
            case 503:
×
351
            case 504:
×
352
                throw new PodioUnavailableError($response->body, $response->status, $url);
×
353
            default:
354
                throw new PodioError($response->body, $response->status, $url);
×
355
        }
356
        return false;
×
357
    }
358

359
    public function get($url, $attributes = [], $options = [])
360
    {
361
        return $this->request(PodioClient::GET, $url, $attributes, $options);
4✔
362
    }
363
    public function post($url, $attributes = [], $options = [])
364
    {
365
        return $this->request(PodioClient::POST, $url, $attributes, $options);
×
366
    }
367
    public function put($url, $attributes = [])
368
    {
369
        return $this->request(PodioClient::PUT, $url, $attributes);
×
370
    }
371
    public function delete($url, $attributes = [])
372
    {
373
        return $this->request(PodioClient::DELETE, $url, $attributes);
×
374
    }
375

376
    public function encode_attributes($attributes): string
377
    {
378
        $return = [];
×
379
        foreach ($attributes as $key => $value) {
×
380
            $return[] = urlencode($key) . '=' . urlencode($value);
×
381
        }
382
        return join('&', $return);
×
383
    }
384
    public function url_with_options($url, $options): string
385
    {
386
        $parameters = [];
×
387

388
        if (isset($options['silent']) && $options['silent']) {
×
389
            $parameters[] = 'silent=1';
×
390
        }
391

392
        if (isset($options['hook']) && !$options['hook']) {
×
393
            $parameters[] = 'hook=false';
×
394
        }
395

396
        if (!empty($options['fields'])) {
×
397
            $parameters[] = 'fields=' . $options['fields'];
×
398
        }
399

400
        return $parameters ? $url . '?' . join('&', $parameters) : $url;
×
401
    }
402

403
    public function rate_limit_remaining(): string
404
    {
405
        if (isset($this->last_http_response)) {
×
406
            return implode($this->last_http_response->getHeader('x-rate-limit-remaining'));
×
407
        }
408
        return '-1';
×
409
    }
410

411
    public function rate_limit(): string
412
    {
413
        if (isset($this->last_http_response)) {
×
414
            return implode($this->last_http_response->getHeader('x-rate-limit-limit'));
×
415
        }
416
        return '-1';
×
417
    }
418

419
    /**
420
     * Set debug config
421
     *
422
     * @param $toggle boolean True to enable debugging. False to disable
423
     * @param $output string Output mode. Can be "stdout" or "file". Default is "stdout"
424
     */
425
    public function set_debug(bool $toggle, string $output = "stdout")
426
    {
427
        if ($toggle) {
4✔
428
            $this->debug = $output;
4✔
429
        } else {
430
            $this->debug = false;
×
431
        }
432
    }
433

434
    protected function log_request($method, $url, $encoded_attributes, $response, $transferTime): void
435
    {
436
        if ($this->debug) {
4✔
437
            if (!$this->logger) {
4✔
438
                $this->logger = new PodioLogger();
4✔
439
            }
440
            $timestamp = gmdate('Y-m-d H:i:s');
4✔
441
            $text = "{$timestamp} {$response->status} {$method} {$url}\n";
4✔
442
            if (!empty($encoded_attributes)) {
4✔
443
                $text .= "{$timestamp} Request body: " . $encoded_attributes . "\n";
×
444
            }
445
            $text .= "{$timestamp} Response: {$response->body}\n\n";
4✔
446

447
            if ($this->debug === 'file') {
4✔
448
                $this->logger->log($text);
×
449
            } elseif ($this->debug === 'stdout' && php_sapi_name() !== 'cli' && class_exists('\\Kint\\Kint')) {
4✔
450
                /** @noinspection PhpFullyQualifiedNameUsageInspection */
451
                \Kint\Kint::dump("{$method} {$url}", $encoded_attributes, $response);
×
452
            } else {
453
                print $text;
4✔
454
            }
455

456
            $this->logger->call_log[] = $transferTime;
4✔
457
        }
458
    }
459

460
    public function shutdown()
461
    {
462
        // Write any new access and refresh tokens to session.
463
        if ($this->session_manager) {
4✔
464
            $this->session_manager->set($this->oauth, $this->auth_type);
×
465
        }
466

467
        // Log api call times if debugging
468
        if ($this->debug && $this->logger) {
4✔
469
            $timestamp = gmdate('Y-m-d H:i:s');
4✔
470
            $count = sizeof($this->logger->call_log);
4✔
471
            $duration = 0;
4✔
472
            if ($this->logger->call_log) {
4✔
473
                foreach ($this->logger->call_log as $val) {
4✔
474
                    $duration += $val;
4✔
475
                }
476
            }
477

478
            $text = "\n{$timestamp} Performed {$count} request(s) in {$duration} seconds\n";
4✔
479
            if ($this->debug === 'file') {
4✔
480
                $this->logger->log($text);
×
481
            } elseif ($this->debug === 'stdout' && php_sapi_name() === 'cli') {
4✔
482
                print $text;
4✔
483
            }
484
        }
485
    }
486
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc