Issues (57)

src/Plugin.php (6 issues)

1
<?php
2
/**
3
 * @link      https://dukt.net/social/
4
 * @copyright Copyright (c) Dukt
5
 * @license   https://github.com/dukt/social/blob/v2/LICENSE.md
6
 */
7
8
namespace dukt\social;
9
10
use Craft;
11
use craft\elements\User;
12
use craft\events\ModelEvent;
13
use craft\events\RegisterElementTableAttributesEvent;
14
use craft\events\RegisterUrlRulesEvent;
15
use craft\events\SetElementTableAttributeHtmlEvent;
16
use craft\helpers\UrlHelper;
17
use craft\services\Plugins;
18
use craft\web\twig\variables\CraftVariable;
19
use craft\web\UrlManager;
20
use dukt\social\base\PluginTrait;
21
use dukt\social\elements\LoginAccount;
22
use dukt\social\models\Settings;
23
use dukt\social\web\assets\login\LoginAsset;
24
use dukt\social\web\twig\variables\SocialVariable;
25
use dukt\social\web\assets\social\SocialAsset;
26
use yii\base\Event;
27
28
/**
29
 * Social plugin class.
30
 *
31
 * @author  Dukt <[email protected]>
32
 * @since   1.0
33
 */
34
class Plugin extends \craft\base\Plugin
35
{
36
    // Traits
37
    // =========================================================================
38
39
    use PluginTrait;
40
41
    // Properties
42
    // =========================================================================
43
44
    /**
45
     * @var bool
46
     */
47
    public $hasCpSettings = true;
48
49
    /**
50
     * @inheritdoc
51
     */
52
    public $minVersionRequired = '1.1.0';
53
54
    // Public Methods
55
    // =========================================================================
56
57
    /**
58
     * @inheritdoc
59
     */
60
    public function init()
61
    {
62
        parent::init();
63
64
        $this->_setPluginComponents();
65
        $this->_registerCpRoutes();
66
        $this->_registerVariable();
67
        $this->_registerEventHandlers();
68
        $this->_registerTableAttributes();
69
        $this->_initLoginAccountsUserPane();
70
    }
71
72
    /**
73
     * @inheritdoc
74
     */
75
    public function getSettingsResponse()
76
    {
77
        $url = UrlHelper::cpUrl('settings/social/loginproviders');
78
79
        Craft::$app->controller->redirect($url);
80
81
        return '';
82
    }
83
84
    /**
85
     * Get OAuth provider config.
86
     *
87
     * @param $handle
88
     * @param bool $parse
89
     * @return array
90
     * @throws \yii\base\InvalidConfigException
91
     */
92
    public function getOauthProviderConfig(string $handle, bool $parse = true): array
93
    {
94
        $config = [
95
            'options' => $this->getOauthConfigItem($handle, 'options', $parse),
96
            'scope' => $this->getOauthConfigItem($handle, 'scope'),
97
            'authorizationOptions' => $this->getOauthConfigItem($handle, 'authorizationOptions'),
98
        ];
99
100
        $provider = $this->getLoginProviders()->getLoginProvider($handle);
101
102
        if ($provider && !isset($config['options']['redirectUri'])) {
103
            $config['options']['redirectUri'] = $provider->getRedirectUri();
0 ignored issues
show
The method getRedirectUri() does not exist on dukt\social\base\LoginProviderInterface. Since it exists in all sub-types, consider adding an abstract or default implementation to dukt\social\base\LoginProviderInterface. ( Ignorable by Annotation )

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

103
            /** @scrutinizer ignore-call */ 
104
            $config['options']['redirectUri'] = $provider->getRedirectUri();
Loading history...
104
        }
105
106
        return $config;
107
    }
108
109
    /**
110
     * Get login provider config.
111
     *
112
     * @param $handle
113
     *
114
     * @return array
115
     */
116
    public function getLoginProviderConfig($handle)
117
    {
118
        $configSettings = Craft::$app->config->getConfigFromFile($this->id);
0 ignored issues
show
The method getConfigFromFile() does not exist on null. ( Ignorable by Annotation )

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

118
        /** @scrutinizer ignore-call */ 
119
        $configSettings = Craft::$app->config->getConfigFromFile($this->id);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
119
120
        if (isset($configSettings['loginProviders'][$handle])) {
121
            return $configSettings['loginProviders'][$handle];
122
        }
123
124
        return [];
125
    }
126
127
    /**
128
     * Save plugin settings.
129
     *
130
     * @param array $settings
131
     *
132
     * @param Plugin|null $plugin
133
     * @return bool
134
     */
135
    public function savePluginSettings(array $settings, Plugin $plugin = null)
136
    {
137
        if ($plugin === null) {
138
            $plugin = Craft::$app->getPlugins()->getPlugin('social');
139
140
            if ($plugin === null) {
141
                throw new NotFoundHttpException('Plugin not found');
0 ignored issues
show
The type dukt\social\NotFoundHttpException 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...
142
            }
143
        }
144
145
        $storedSettings = Craft::$app->plugins->getStoredPluginInfo('social')['settings'];
0 ignored issues
show
The method getStoredPluginInfo() does not exist on null. ( Ignorable by Annotation )

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

145
        $storedSettings = Craft::$app->plugins->/** @scrutinizer ignore-call */ getStoredPluginInfo('social')['settings'];

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
146
147
        $settings['loginProviders'] = [];
148
149
        if (isset($storedSettings['loginProviders'])) {
150
            $settings['loginProviders'] = $storedSettings['loginProviders'];
151
        }
152
153
        return Craft::$app->getPlugins()->savePluginSettings($plugin, $settings);
154
    }
155
156
    /**
157
     * Save login provider settings.
158
     *
159
     * @param $handle
160
     * @param $providerSettings
161
     *
162
     * @return bool
163
     */
164
    public function saveLoginProviderSettings($handle, $providerSettings)
165
    {
166
        $settings = (array)self::getInstance()->getSettings();
167
        $storedSettings = Craft::$app->plugins->getStoredPluginInfo('social')['settings'];
168
169
        $settings['loginProviders'] = [];
170
171
        if (isset($storedSettings['loginProviders'])) {
172
            $settings['loginProviders'] = $storedSettings['loginProviders'];
173
        }
174
175
        $settings['loginProviders'][$handle] = $providerSettings;
176
177
        $plugin = Craft::$app->getPlugins()->getPlugin('social');
178
179
        return Craft::$app->getPlugins()->savePluginSettings($plugin, $settings);
180
    }
181
182
    // Protected Methods
183
    // =========================================================================
184
185
    /**
186
     * @inheritdoc
187
     */
188
    protected function createSettingsModel()
189
    {
190
        return new Settings();
191
    }
192
193
    // Private Methods
194
    // =========================================================================
195
196
    /**
197
     * Social login for the control panel.
198
     *
199
     * @return null
200
     * @throws \craft\errors\MissingComponentException
201
     * @throws \yii\base\InvalidConfigException
202
     */
203
    private function initCpSocialLogin()
204
    {
205
        if (!Craft::$app->getRequest()->getIsConsoleRequest() && $this->getSettings()->enableCpLogin && Craft::$app->getRequest()->getIsCpRequest() && Craft::$app->getRequest()->getSegment(1) === 'login') {
206
207
            $loginProviders = $this->loginProviders->getLoginProviders();
208
            $jsLoginProviders = [];
209
210
            foreach ($loginProviders as $loginProvider) {
211
                $jsLoginProvider = [
212
                    'name' => $loginProvider->getName(),
213
                    'handle' => $loginProvider->getHandle(),
214
                    'url' => $this->getLoginAccounts()->getLoginUrl($loginProvider->getHandle()),
215
                    'iconUrl' => $loginProvider->getIconUrl(),
216
                ];
217
218
                $jsLoginProviders[] = $jsLoginProvider;
219
            }
220
221
            $error = Craft::$app->getSession()->getFlash('error');
222
223
            Craft::$app->getView()->registerAssetBundle(LoginAsset::class);
224
225
            Craft::$app->getView()->registerJs('var socialLoginForm = new Craft.SocialLoginForm(' . json_encode($jsLoginProviders) . ', ' . json_encode($error) . ');');
226
        }
227
    }
228
229
    /**
230
     * Initialize login accounts user pane.
231
     *
232
     * @return null
233
     */
234
    private function _initLoginAccountsUserPane()
235
    {
236
        Craft::$app->getView()->hook('cp.users.edit.details', function(&$context) {
237
            if ($context['user'] && $context['user']->id) {
238
                $context['loginAccounts'] = $this->loginAccounts->getLoginAccountsByUserId($context['user']->id);
239
                $context['loginProviders'] = $this->loginProviders->getLoginProviders();
240
241
                Craft::$app->getView()->registerAssetBundle(SocialAsset::class);
242
243
                return Craft::$app->getView()->renderTemplate('social/_components/users/login-accounts-pane', $context);
244
            }
245
        });
246
    }
247
248
    /**
249
     * Get OAuth config item
250
     *
251
     * @param string $providerHandle
252
     * @param string $key
253
     *
254
     * @return array
255
     */
256
    private function getOauthConfigItem(string $providerHandle, string $key, bool $parse = true): array
257
    {
258
        $configSettings = Craft::$app->config->getConfigFromFile($this->id);
259
260
        if (isset($configSettings['loginProviders'][$providerHandle]['oauth'][$key])) {
261
            return $this->parseOauthConfigItemEnv($key, $configSettings['loginProviders'][$providerHandle]['oauth'][$key], $parse);
262
        }
263
264
        $storedSettings = Craft::$app->plugins->getStoredPluginInfo($this->id)['settings'];
265
266
        if (isset($storedSettings['loginProviders'][$providerHandle]['oauth'][$key])) {
267
            return $this->parseOauthConfigItemEnv($key, $storedSettings['loginProviders'][$providerHandle]['oauth'][$key], $parse);
268
        }
269
270
        return [];
271
    }
272
273
    /**
274
     * Parse OAuth config item environment variables.
275
     *
276
     * @param string $key
277
     * @param array $configItem
278
     * @param bool $parse
279
     * @return array
280
     */
281
    private function parseOauthConfigItemEnv(string $key, array $configItem, bool $parse = true): array
282
    {
283
        // Parse config item options environment variables
284
        if ($parse && $key === 'options') {
285
            return array_map('Craft::parseEnv', $configItem);
286
        }
287
288
        return $configItem;
289
    }
290
291
    /**
292
     * Set plugin components.
293
     */
294
    private function _setPluginComponents()
295
    {
296
        $this->setComponents([
297
            'loginAccounts' => \dukt\social\services\LoginAccounts::class,
298
            'loginProviders' => \dukt\social\services\LoginProviders::class,
299
        ]);
300
    }
301
302
    /**
303
     * Register CP routes.
304
     */
305
    private function _registerCpRoutes()
306
    {
307
        Event::on(UrlManager::class, UrlManager::EVENT_REGISTER_CP_URL_RULES, function(RegisterUrlRulesEvent $event): void {
308
            $rules = [
309
                'social' => 'social/login-accounts/index',
310
311
                'social/loginaccounts' => 'social/loginAccounts/index',
312
                'social/loginaccounts/<userId:\d+>' => 'social/login-accounts/edit',
313
314
                'settings/social' => 'social/login-providers/index',
315
                'settings/social/loginproviders' => 'social/login-providers/index',
316
                'settings/social/loginproviders/<handle:{handle}>' => 'social/login-providers/oauth',
317
                'settings/social/loginproviders/<handle:{handle}>/user-field-mapping' => 'social/login-providers/user-field-mapping',
318
                'settings/social/settings' => 'social/settings/settings',
319
            ];
320
321
            $event->rules = array_merge($event->rules, $rules);
322
        });
323
    }
324
325
    /**
326
     * Register Social template variable.
327
     */
328
    private function _registerVariable()
329
    {
330
        Event::on(CraftVariable::class, CraftVariable::EVENT_INIT, function(Event $event): void {
331
            /** @var CraftVariable $variable */
332
            $variable = $event->sender;
333
            $variable->set('social', SocialVariable::class);
334
        });
335
    }
336
337
    /**
338
     * Register Social user table attributes.
339
     */
340
    private function _registerTableAttributes()
341
    {
342
        Event::on(User::class, User::EVENT_REGISTER_TABLE_ATTRIBUTES, function(RegisterElementTableAttributesEvent $event): void {
343
            $event->tableAttributes['loginAccounts'] = Craft::t('social', 'Login Accounts');
344
        });
345
346
        Event::on(User::class, User::EVENT_SET_TABLE_ATTRIBUTE_HTML, function(SetElementTableAttributeHtmlEvent $event): void {
347
            if ($event->attribute === 'loginAccounts') {
348
                Craft::$app->getView()->registerAssetBundle(SocialAsset::class);
349
350
                $user = $event->sender;
351
352
                $loginAccounts = LoginAccount::find()
353
                    ->userId($user->id)
354
                    ->trashed($user->trashed)
355
                    ->all();
356
357
                if ($loginAccounts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $loginAccounts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
358
                    $event->html = Craft::$app->getView()->renderTemplate('social/_components/users/login-accounts-table-attribute', [
359
                        'loginAccounts' => $loginAccounts,
360
                    ]);
361
                } else {
362
                    $event->html = '';
363
                }
364
            }
365
        });
366
    }
367
368
    /**
369
     * Register event handlers.
370
     */
371
    private function _registerEventHandlers()
372
    {
373
        Event::on(User::class, User::EVENT_AFTER_SAVE, function(ModelEvent $event): void {
374
            $user = $event->sender;
375
            $loginAccounts = Plugin::getInstance()->getLoginAccounts()->getLoginAccountsByUserId($user->id);
376
377
            foreach ($loginAccounts as $loginAccount) {
378
                Craft::$app->elements->saveElement($loginAccount);
0 ignored issues
show
The method saveElement() does not exist on null. ( Ignorable by Annotation )

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

378
                Craft::$app->elements->/** @scrutinizer ignore-call */ 
379
                                       saveElement($loginAccount);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
379
            }
380
        });
381
382
        // Soft delete the related login accounts after deleting a user
383
        Event::on(User::class, User::EVENT_AFTER_DELETE, function(Event $event): void {
384
            $user = $event->sender;
385
386
            $loginAccounts = LoginAccount::find()
387
                ->userId($user->id)
388
                ->all();
389
390
            foreach($loginAccounts as $loginAccount) {
391
                Craft::$app->getElements()->deleteElement($loginAccount);
392
            }
393
        });
394
395
        // Make sure there’s no duplicate login account before restoring the user
396
        Event::on(User::class, User::EVENT_BEFORE_RESTORE, function(ModelEvent $event) {
397
            $user = $event->sender;
398
399
            // Get the login accounts of the user that’s being restored
400
            $loginAccounts = LoginAccount::find()
401
                ->userId($user->id)
402
                ->trashed(true)
403
                ->all();
404
405
            $conflicts = false;
406
407
            // Check that those login accounts don’t conflict with existing login accounts from other users
408
            foreach ($loginAccounts as $loginAccount) {
409
                // Check if there is another user with a login account using the same providerHandle/socialUid combo
410
                $existingAccount = LoginAccount::find()->one();
411
412
                if ($existingAccount) {
413
                    $conflicts = true;
414
                }
415
            }
416
417
            // Mark the event as invalid is there are conflicts
418
            if ($conflicts) {
419
                $event->isValid = false;
420
                return false;
421
            }
422
423
            // Restore login account elements
424
            foreach($loginAccounts as $loginAccount) {
425
                Craft::$app->getElements()->restoreElement($loginAccount);
426
            }
427
        });
428
429
        // Initialize Social Login for CP after loading the plugins
430
        Event::on(Plugins::class, Plugins::EVENT_AFTER_LOAD_PLUGINS, function(): void {
431
            $this->initCpSocialLogin();
432
        });
433
    }
434
}
435