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

podio-community / podio-php / 5860567517

pending completion
5860567517

push

github

web-flow
Merge pull request #241 from podio-community/240-fix-kint-debugging

fix: kint debugging for web setups + making kint an optional dependency

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

979 of 1830 relevant lines covered (53.5%)

37.18 hits per line

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

32.03
/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
        $original_url = $url;
4✔
196
        $encoded_attributes = null;
4✔
197

198
        if (is_object($attributes) && substr(get_class($attributes), 0, 5) == 'Podio') {
4✔
199
            $attributes = $attributes->as_json(false);
×
200
        }
201

202
        if (!is_array($attributes) && !is_object($attributes)) {
4✔
203
            throw new PodioDataIntegrityError('Attributes must be an array');
×
204
        }
205

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

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

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

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

261
        $response = new PodioResponse();
4✔
262

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

287
        if (!isset($options['oauth_request'])) {
4✔
288
            $this->log_request($method, $url, $encoded_attributes, $response, $transferTime);
4✔
289
        }
290

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

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

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

384
        if (isset($options['silent']) && $options['silent']) {
×
385
            $parameters[] = 'silent=1';
×
386
        }
387

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

392
        if (!empty($options['fields'])) {
×
393
            $parameters[] = 'fields=' . $options['fields'];
×
394
        }
395

396
        return $parameters ? $url . '?' . join('&', $parameters) : $url;
×
397
    }
398

399
    public function rate_limit_remaining(): string
400
    {
401
        if (isset($this->last_http_response)) {
×
402
            return implode($this->last_http_response->getHeader('x-rate-limit-remaining'));
×
403
        }
404
        return '-1';
×
405
    }
406

407
    public function rate_limit(): string
408
    {
409
        if (isset($this->last_http_response)) {
×
410
            return implode($this->last_http_response->getHeader('x-rate-limit-limit'));
×
411
        }
412
        return '-1';
×
413
    }
414

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

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

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

452
            $this->logger->call_log[] = $transferTime;
4✔
453
        }
454
    }
455

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

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

474
            $text = "\n{$timestamp} Performed {$count} request(s) in {$duration} seconds\n";
4✔
475
            if ($this->debug === 'file') {
4✔
476
                $this->logger->log($text);
×
477
            } elseif ($this->debug === 'stdout' && php_sapi_name() === 'cli') {
4✔
478
                print $text;
4✔
479
            }
480
        }
481
    }
482
}
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