Issues (47)

src/base/Gateway.php (9 issues)

1
<?php
2
/**
3
 * @link      https://dukt.net/videos/
4
 * @copyright Copyright (c) Dukt
5
 * @license   https://github.com/dukt/videos/blob/v2/LICENSE.md
6
 */
7
8
namespace dukt\videos\base;
9
10
use Craft;
11
use craft\helpers\Json;
12
use craft\helpers\UrlHelper;
13
use dukt\videos\errors\ApiResponseException;
14
use dukt\videos\errors\GatewayMethodNotFoundException;
15
use dukt\videos\errors\JsonParsingException;
16
use dukt\videos\errors\VideoNotFoundException;
17
use dukt\videos\Plugin as Videos;
18
use dukt\videos\Plugin;
19
use dukt\videos\records\Token as TokenRecord;
20
use GuzzleHttp\Exception\BadResponseException;
21
use Exception;
22
use Psr\Http\Message\ResponseInterface;
23
use yii\web\Response;
24
25
26
/**
27
 * Gateway is the base class for classes representing video gateways.
28
 *
29
 * @author Dukt <[email protected]>
30
 * @since  1.0
31
 */
32
abstract class Gateway implements GatewayInterface
33
{
34
    // Public Methods
35
    // =========================================================================
36
37
    /**
38
     * Return the handle of the gateway based on its class name
39
     *
40
     * @return string
41
     */
42
    public function getHandle(): string
43
    {
44
        $handle = \get_class($this);
45
46
        return strtolower(substr($handle, strrpos($handle, "\\") + 1));
47
    }
48
49
    /**
50
     * Returns the icon URL.
51
     *
52
     * @return string|false|null
53
     */
54
    public function getIconUrl()
55
    {
56
        $iconAlias = $this->getIconAlias();
57
58
        if (file_exists(Craft::getAlias($iconAlias))) {
0 ignored issues
show
It seems like Craft::getAlias($iconAlias) can also be of type false; however, parameter $filename of file_exists() 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

58
        if (file_exists(/** @scrutinizer ignore-type */ Craft::getAlias($iconAlias))) {
Loading history...
59
            return Craft::$app->assetManager->getPublishedUrl($iconAlias, true);
0 ignored issues
show
The call to yii\web\AssetManager::getPublishedUrl() has too many arguments starting with true. ( Ignorable by Annotation )

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

59
            return Craft::$app->assetManager->/** @scrutinizer ignore-call */ getPublishedUrl($iconAlias, true);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
60
        }
61
62
        return null;
63
    }
64
65
    /**
66
     * OAuth Connect.
67
     *
68
     * @return Response
69
     * @throws \craft\errors\MissingComponentException
70
     * @throws \yii\base\InvalidConfigException
71
     */
72
    public function oauthConnect(): Response
73
    {
74
        $provider = $this->getOauthProvider();
75
76
        Craft::$app->getSession()->set('videos.oauthState', $provider->getState());
77
78
        $scope = $this->getOauthScope();
79
        $options = $this->getOauthAuthorizationOptions();
80
81
        if (!\is_array($options)) {
0 ignored issues
show
The condition is_array($options) is always false.
Loading history...
82
            $options = [];
83
        }
84
85
        $options['scope'] = $scope;
86
87
        $authorizationUrl = $provider->getAuthorizationUrl($options);
88
89
        return Craft::$app->getResponse()->redirect($authorizationUrl);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Craft::app->getRe...rect($authorizationUrl) could return the type yii\console\Response which is incompatible with the type-hinted return yii\web\Response. Consider adding an additional type-check to rule them out.
Loading history...
90
    }
91
92
    /**
93
     * Returns the gateway's OAuth provider.
94
     *
95
     * @return mixed
96
     * @throws \yii\base\InvalidConfigException
97
     */
98
    public function getOauthProvider()
99
    {
100
        $options = $this->getOauthProviderOptions();
101
102
        return $this->createOauthProvider($options);
103
    }
104
105
    /**
106
     * Returns the OAuth provider’s name.
107
     *
108
     * @return string
109
     */
110
    public function getOauthProviderName(): string
111
    {
112
        return $this->getName();
113
    }
114
115
    /**
116
     * Returns the redirect URI.
117
     *
118
     * @return string
119
     */
120
    public function getRedirectUri(): string
121
    {
122
        return UrlHelper::actionUrl('videos/oauth/callback');
123
    }
124
125
    /**
126
     * OAuth Scope
127
     *
128
     * @return array|null
129
     */
130
    public function getOauthScope()
131
    {
132
        return null;
133
    }
134
135
    /**
136
     * OAuth Authorization Options
137
     *
138
     * @return array|null
139
     */
140
    public function getOauthAuthorizationOptions()
141
    {
142
        return null;
143
    }
144
145
    /**
146
     * OAuth Callback
147
     *
148
     * @return Response
149
     * @throws \craft\errors\MissingComponentException
150
     * @throws \yii\base\InvalidConfigException
151
     */
152
    public function oauthCallback(): Response
153
    {
154
        $provider = $this->getOauthProvider();
155
156
        $code = Craft::$app->getRequest()->getParam('code');
157
158
        try {
159
            // Try to get an access token (using the authorization code grant)
160
            $token = $provider->getAccessToken('authorization_code', [
161
                'code' => $code
162
            ]);
163
164
            // Save token
165
            Videos::$plugin->getOauth()->saveToken($this->getHandle(), $token);
166
167
            // Reset session variables
168
169
            // Redirect
170
            Craft::$app->getSession()->setNotice(Craft::t('videos', 'Connected to {gateway}.', ['gateway' => $this->getName()]));
171
        } catch (Exception $exception) {
172
            Craft::error('Couldn’t connect to video gateway:' . "\r\n"
173
                . 'Message: ' . "\r\n" . $exception->getMessage() . "\r\n"
174
                . 'Trace: ' . "\r\n" . $exception->getTraceAsString(), __METHOD__);
175
176
            // Failed to get the token credentials or user details.
177
            Craft::$app->getSession()->setError($exception->getMessage());
178
        }
179
180
        $redirectUrl = UrlHelper::cpUrl('videos/settings');
181
182
        return Craft::$app->getResponse()->redirect($redirectUrl);
0 ignored issues
show
Bug Best Practice introduced by
The expression return Craft::app->getRe...>redirect($redirectUrl) could return the type yii\console\Response which is incompatible with the type-hinted return yii\web\Response. Consider adding an additional type-check to rule them out.
Loading history...
183
    }
184
185
    /**
186
     * Has Token
187
     *
188
     * @return bool
189
     * @throws \yii\base\InvalidConfigException
190
     */
191
    public function hasToken(): bool
192
    {
193
        $token = Videos::$plugin->getOauth()->getToken($this->getHandle(), false);
194
        return (bool) $token;
195
    }
196
197
    /**
198
     * Returns the gateway's OAuth token.
199
     *
200
     * @return mixed
201
     * @throws \yii\base\InvalidConfigException
202
     */
203
    public function getOauthToken(): ?\League\OAuth2\Client\Token\AccessToken
204
    {
205
        return Videos::$plugin->getOauth()->getToken($this->getHandle());
206
    }
207
208
    /**
209
     * Whether the OAuth flow should be enable or not for this gateway.
210
     *
211
     * @return bool
212
     */
213
    public function enableOauthFlow(): bool
214
    {
215
        return true;
216
    }
217
218
    /**
219
     * Returns the HTML of the embed from a video ID.
220
     *
221
     * @param       $videoId
222
     * @param array $options
223
     *
224
     * @return string
225
     */
226
    public function getEmbedHtml($videoId, array $options = []): string
227
    {
228
        $embedAttributes = [
229
            'title' => 'External video from ' . $this->getHandle(),
230
            'frameborder' => '0',
231
            'allowfullscreen' => 'true',
232
            'allowscriptaccess' => 'true',
233
            'allow' => 'autoplay; encrypted-media',
234
        ];
235
236
        $disableSize = $options['disable_size'] ?? false;
237
238
        if (!$disableSize) {
239
            $this->parseEmbedAttribute($embedAttributes, $options, 'width', 'width');
240
            $this->parseEmbedAttribute($embedAttributes, $options, 'height', 'height');
241
        }
242
243
        $title = $options['title'] ?? false;
244
245
        if ($title) {
246
            $this->parseEmbedAttribute($embedAttributes, $options, 'title', 'title');
247
        }
248
249
        $this->parseEmbedAttribute($embedAttributes, $options, 'iframeClass', 'class');
250
251
        $embedUrl = $this->getEmbedUrl($videoId, $options);
252
253
        $embedAttributesString = '';
254
255
        foreach ($embedAttributes as $key => $value) {
256
            $embedAttributesString .= ' ' . $key . '="' . $value . '"';
257
        }
258
259
        return '<iframe src="' . $embedUrl . '"' . $embedAttributesString . '></iframe>';
260
    }
261
262
    /**
263
     * Returns the URL of the embed from a video ID.
264
     *
265
     * @param       $videoId
266
     * @param array $options
267
     *
268
     * @return string
269
     */
270
    public function getEmbedUrl($videoId, array $options = []): string
271
    {
272
        $format = $this->getEmbedFormat();
273
274
        if ($options !== []) {
275
            $queryMark = '?';
276
277
            if (strpos($this->getEmbedFormat(), '?') !== false) {
278
                $queryMark = '&';
279
            }
280
281
            $options = http_build_query($options);
282
283
            $format .= $queryMark . $options;
284
        }
285
286
        return sprintf($format, $videoId);
287
    }
288
289
    /**
290
     * Returns the javascript origin URL.
291
     *
292
     * @return string
293
     * @throws \craft\errors\SiteNotFoundException
294
     */
295
    public function getJavascriptOrigin(): string
296
    {
297
        return UrlHelper::baseUrl();
298
    }
299
300
    /**
301
     * Returns the account.
302
     *
303
     * @return mixed
304
     * @throws Exception
305
     */
306
    public function getAccount()
307
    {
308
        $token = $this->getOauthToken();
309
310
        if ($token !== null) {
311
            $account = Videos::$plugin->getCache()->get(['getAccount', $token]);
312
313
            if (!$account) {
314
                $oauthProvider = $this->getOauthProvider();
315
                $account = $oauthProvider->getResourceOwner($token);
316
317
                Videos::$plugin->getCache()->set(['getAccount', $token], $account);
318
            }
319
320
            if ($account) {
321
                return $account;
322
            }
323
        }
324
325
        return null;
326
    }
327
328
    /**
329
     * Returns a video from its public URL.
330
     *
331
     * @param $url
332
     *
333
     * @return mixed
334
     * @throws VideoNotFoundException
335
     */
336
    public function getVideoByUrl($url): \dukt\videos\models\Video
337
    {
338
        $url = $url['url'];
339
340
        $videoId = $this->extractVideoIdFromUrl($url);
341
342
        if (!$videoId) {
343
            throw new VideoNotFoundException('Video not found with url given.');
344
        }
345
346
        return $this->getVideoById($videoId);
0 ignored issues
show
It seems like $videoId can also be of type true; however, parameter $id of dukt\videos\base\GatewayInterface::getVideoById() 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

346
        return $this->getVideoById(/** @scrutinizer ignore-type */ $videoId);
Loading history...
347
    }
348
349
    /**
350
     * Returns a list of videos.
351
     *
352
     * @param $method
353
     * @param $options
354
     *
355
     * @return mixed
356
     * @throws GatewayMethodNotFoundException
357
     */
358
    public function getVideos($method, $options)
359
    {
360
        $realMethod = 'getVideos' . ucwords($method);
361
362
        if (method_exists($this, $realMethod)) {
363
            return $this->{$realMethod}($options);
364
        }
365
366
        throw new GatewayMethodNotFoundException('Gateway method “' . $realMethod . '” not found.');
367
    }
368
369
    /**
370
     * Number of videos per page.
371
     *
372
     * @return mixed
373
     */
374
    public function getVideosPerPage()
375
    {
376
        return Videos::$plugin->getSettings()->videosPerPage;
377
    }
378
379
    /**
380
     * Returns the OAuth provider options.
381
     *
382
     * @param bool $parse
383
     * @throws \yii\base\InvalidConfigException
384
     * @return mixed[]|null
385
     */
386
    public function getOauthProviderOptions(bool $parse = true): array
387
    {
388
        return Plugin::getInstance()->getOauthProviderOptions($this->getHandle(), $parse);
0 ignored issues
show
Bug Best Practice introduced by
The expression return dukt\videos\Plugi...s->getHandle(), $parse) could return the type null which is incompatible with the type-hinted return array. Consider adding an additional type-check to rule them out.
Loading history...
389
    }
390
391
    /**
392
     * Whether the gateway supports search or not.
393
     *
394
     * @return bool
395
     */
396
    public function supportsSearch(): bool
397
    {
398
        return false;
399
    }
400
401
    // Protected Methods
402
    // =========================================================================
403
404
    /**
405
     * Performs a GET request on the API.
406
     *
407
     * @param       $uri
408
     * @param array $options
409
     *
410
     * @return array
411
     * @throws ApiResponseException
412
     */
413
    protected function get($uri, array $options = []): array
414
    {
415
        $client = $this->createClient();
0 ignored issues
show
The method createClient() does not exist on dukt\videos\base\Gateway. Since it exists in all sub-types, consider adding an abstract or default implementation to dukt\videos\base\Gateway. ( Ignorable by Annotation )

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

415
        /** @scrutinizer ignore-call */ 
416
        $client = $this->createClient();
Loading history...
416
417
        try {
418
            $response = $client->request('GET', $uri, $options);
419
420
            if (Videos::$plugin->getVideos()->pluginDevMode && $this->getHandle() === 'vimeo') {
421
                Craft::info('URI: '.Json::encode($uri), __METHOD__);
422
                Craft::info('Options: '.Json::encode($options), __METHOD__);
423
                Craft::info('Vimeo X-RateLimit-Limit: '.Json::encode($response->getHeader('X-RateLimit-Limit')), __METHOD__);
424
                Craft::info('Vimeo X-RateLimit-Remaining: '.Json::encode($response->getHeader('X-RateLimit-Remaining')), __METHOD__);
425
            }
426
427
            $body = (string)$response->getBody();
428
            $data = Json::decode($body);
429
        } catch (BadResponseException $badResponseException) {
430
            $response = $badResponseException->getResponse();
431
            $body = (string)$response->getBody();
432
433
            try {
434
                $data = Json::decode($body);
435
            } catch (JsonParsingException $jsonParsingException) {
436
                throw $badResponseException;
437
            }
438
        }
439
440
        $this->checkResponse($response, $data);
441
442
        return $data;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $data returns the type null which is incompatible with the type-hinted return array.
Loading history...
443
    }
444
445
    /**
446
     * Checks a provider response for errors.
447
     *
448
     * @param ResponseInterface $response
449
     * @param                   $data
450
     *
451
     * @throws ApiResponseException
452
     */
453
    protected function checkResponse(ResponseInterface $response, $data)
454
    {
455
        if (!empty($data['error'])) {
456
            $code = 0;
457
            $error = $data['error'];
458
459
            if (\is_array($error)) {
460
                $code = $error['code'];
461
                $error = $error['message'];
462
            }
463
464
            throw new ApiResponseException($error, $code);
465
        }
466
    }
467
468
    // Private Methods
469
    // =========================================================================
470
471
    /**
472
     * Parse embed attribute.
473
     *
474
     * @param $embedAttributes
475
     * @param $options
476
     * @param $option
477
     * @param $attribute
478
     *
479
     * @return null
480
     */
481
    private function parseEmbedAttribute(&$embedAttributes, &$options, $option, $attribute)
482
    {
483
        if (isset($options[$option])) {
484
            $embedAttributes[$attribute] = $options[$option];
485
            unset($options[$option]);
486
        }
487
488
        return null;
489
    }
490
}
491