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

podio-community / podio-php / 5254206542

pending completion
5254206542

push

github

web-flow
Merge pull request #230 from podio-community/daniel-sc-patch-1

fix: replace wrong static access on refresh access token

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

917 of 2108 relevant lines covered (43.5%)

24.11 hits per line

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

0.0
/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
    protected $debug = false;
16
    public $logger;
17
    public $session_manager;
18
    public $last_response;
19
    public $auth_type;
20
    /** @var \GuzzleHttp\Client */
21
    public $http_client;
22
    protected $url;
23
    protected $client_id;
24
    protected $client_secret;
25
    protected $secret;
26
    protected $headers;
27
    /** @var \Psr\Http\Message\ResponseInterface */
28
    private $last_http_response;
29

30
    public const VERSION = '6.1.0';
31

32
    public const GET = 'GET';
33
    public const POST = 'POST';
34
    public const PUT = 'PUT';
35
    public const DELETE = 'DELETE';
36

37
    public function __construct($client_id, $client_secret, $options = array('session_manager' => null, 'curl_options' => []))
38
    {
39
        // Setup client info
40
        $this->client_id = $client_id;
×
41
        $this->client_secret = $client_secret;
×
42

43
        $this->url = empty($options['api_url']) ? 'https://api.podio.com:443' : $options['api_url'];
×
44
        $client_config = [
×
45
            'base_uri' => $this->url,
×
46

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

61
        $this->session_manager = null;
×
62
        if ($options && !empty($options['session_manager'])) {
×
63
            if (is_string($options['session_manager']) && class_exists($options['session_manager'])) {
×
64
                $this->session_manager = new $options['session_manager']();
×
65
            } elseif (is_object($options['session_manager']) && method_exists($options['session_manager'], 'get') && method_exists($options['session_manager'], 'set')) {
×
66
                $this->session_manager = $options['session_manager'];
×
67
            }
68
            if ($this->session_manager) {
×
69
                $this->oauth = $this->session_manager->get();
×
70
            }
71
        }
72
    }
73

74
    public function __destruct()
75
    {
76
        $this->shutdown();
×
77
    }
78

79
    public function authenticate_with_app($app_id, $app_token): bool
80
    {
81
        return $this->authenticate('app', ['app_id' => $app_id, 'app_token' => $app_token]);
×
82
    }
83

84
    public function authenticate_with_password($username, $password): bool
85
    {
86
        return $this->authenticate('password', ['username' => $username, 'password' => $password]);
×
87
    }
88

89
    public function authenticate_with_authorization_code($authorization_code, $redirect_uri): bool
90
    {
91
        return $this->authenticate('authorization_code', ['code' => $authorization_code, 'redirect_uri' => $redirect_uri]);
×
92
    }
93

94
    public function refresh_access_token(): bool
95
    {
96
        return $this->authenticate('refresh_token', ['refresh_token' => $this->oauth->refresh_token]);
×
97
    }
98

99
    public function authenticate($grant_type, $attributes): bool
100
    {
101
        $data = [];
×
102
        $auth_type = ['type' => $grant_type];
×
103

104
        switch ($grant_type) {
105
            case 'password':
×
106
                $data['grant_type'] = 'password';
×
107
                $data['username'] = $attributes['username'];
×
108
                $data['password'] = $attributes['password'];
×
109

110
                $auth_type['identifier'] = $attributes['username'];
×
111
                break;
×
112
            case 'refresh_token':
×
113
                $data['grant_type'] = 'refresh_token';
×
114
                $data['refresh_token'] = $attributes['refresh_token'];
×
115
                break;
×
116
            case 'authorization_code':
×
117
                $data['grant_type'] = 'authorization_code';
×
118
                $data['code'] = $attributes['code'];
×
119
                $data['redirect_uri'] = $attributes['redirect_uri'];
×
120
                break;
×
121
            case 'app':
×
122
                $data['grant_type'] = 'app';
×
123
                $data['app_id'] = $attributes['app_id'];
×
124
                $data['app_token'] = $attributes['app_token'];
×
125

126
                $auth_type['identifier'] = $attributes['app_id'];
×
127
                break;
×
128
            default:
129
                break;
×
130
        }
131

132
        $request_data = array_merge($data, ['client_id' => $this->client_id, 'client_secret' => $this->client_secret]);
×
133
        if ($response = $this->request(self::POST, '/oauth/token', $request_data, ['oauth_request' => true])) {
×
134
            $body = $response->json_body();
×
135
            $this->oauth = new PodioOAuth($body['access_token'], $body['refresh_token'], $body['expires_in'], $body['ref'], $body['scope']);
×
136

137
            // Don't touch auth_type if we are refreshing automatically as it'll be reset to null
138
            if ($grant_type !== 'refresh_token') {
×
139
                $this->auth_type = $auth_type;
×
140
            }
141

142
            if ($this->session_manager) {
×
143
                $this->session_manager->set($this->oauth, $this->auth_type);
×
144
            }
145

146
            return true;
×
147
        }
148
        return false;
×
149
    }
150

151
    public function clear_authentication()
152
    {
153
        $this->oauth = new PodioOAuth();
×
154

155
        if ($this->session_manager) {
×
156
            $this->session_manager->set($this->oauth, $this->auth_type);
×
157
        }
158
    }
159

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

167
    public function is_authenticated(): bool
168
    {
169
        return $this->oauth && $this->oauth->access_token;
×
170
    }
171

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

194
        $original_url = $url;
×
195
        $encoded_attributes = null;
×
196

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

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

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

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

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

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

260
        $response = new PodioResponse();
×
261

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

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

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

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

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

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

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

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

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

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

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

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

429
    public function log_request($method, $url, $encoded_attributes, $response, $transferTime): void
430
    {
431
        if ($this->debug) {
×
432
            $timestamp = gmdate('Y-m-d H:i:s');
×
433
            $text = "{$timestamp} {$response->status} {$method} {$url}\n";
×
434
            if (!empty($encoded_attributes)) {
×
435
                $text .= "{$timestamp} Request body: " . $encoded_attributes . "\n";
×
436
            }
437
            $text .= "{$timestamp} Reponse: {$response->body}\n\n";
×
438

439
            if ($this->debug === 'file') {
×
440
                if (!$this->logger) {
×
441
                    $this->logger = new PodioLogger();
×
442
                }
443
                $this->logger->log($text);
×
444
            } elseif ($this->debug === 'stdout' && php_sapi_name() === 'cli') {
×
445
                print $text;
×
446
            } elseif ($this->debug === 'stdout' && php_sapi_name() === 'cli') {
×
447
                require_once 'vendor/kint/Kint.class.php';
×
448
                Kint::dump("{$method} {$url}", $encoded_attributes, $response);
×
449
            }
450

451
            $this->logger->call_log[] = $transferTime;
×
452
        }
453
    }
454

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

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

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