Completed
Push — master ( 9cc424...58c353 )
by vistart
16:50
created

Organization::getJoinIpAddress()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 9
rs 9.6666
ccs 0
cts 5
cp 0
cc 2
eloc 6
nc 2
nop 0
crap 6
1
<?php
2
3
/**
4
 *  _   __ __ _____ _____ ___  ____  _____
5
 * | | / // // ___//_  _//   ||  __||_   _|
6
 * | |/ // /(__  )  / / / /| || |     | |
7
 * |___//_//____/  /_/ /_/ |_||_|     |_|
8
 * @link https://vistart.me/
9
 * @copyright Copyright (c) 2016 - 2017 vistart
10
 * @license https://vistart.me/license/
11
 */
12
13
namespace rhosocial\organization;
14
15
use rhosocial\base\models\traits\SelfBlameableTrait;
16
use rhosocial\base\models\queries\BaseBlameableQuery;
17
use rhosocial\base\models\queries\BaseUserQuery;
18
use rhosocial\organization\exceptions\DisallowMemberJoinOtherException;
19
use rhosocial\organization\exceptions\ExcludeOtherMembersException;
20
use rhosocial\organization\exceptions\OnlyAcceptCurrentOrgMemberException;
21
use rhosocial\organization\exceptions\OnlyAcceptSuperiorOrgMemberException;
22
use rhosocial\organization\rbac\roles\DepartmentAdmin;
23
use rhosocial\organization\rbac\roles\DepartmentCreator;
24
use rhosocial\organization\rbac\roles\OrganizationAdmin;
25
use rhosocial\organization\rbac\roles\OrganizationCreator;
26
use rhosocial\organization\queries\MemberQuery;
27
use rhosocial\organization\queries\OrganizationQuery;
28
use rhosocial\user\User;
29
use Yii;
30
use yii\base\Event;
31
use yii\base\InvalidParamException;
32
use yii\db\IntegrityException;
33
34
/**
35
 * Organization.
36
 * This class is used to describe an organization or department, depending on the type property.
37
 * Organization or department should be created by the user, it is best not to directly implement their own such.
38
 *
39
 * In general, the organization needs to have `setUpOrganization` permission, and the user does not have this permission
40
 * by default. You need to give this permission to the user who created the organization in advance.
41
 * Department, affiliated with the organization or other department, also need the appropriate permission to set up.
42
 *
43
 * While this can work independently, we still strongly recommend that you declare the Organization class yourself and
44
 * inherit this.
45
 * Then you need to specify the Profile and Member class yourself, like following:
46
```php
47
class Organization extends \rhosocial\organization\Organization
48
{
49
    public $profileClass = Profile::class;
50
    public $memberClass = Member::class;
51
}
52
```
53
 * If you need to limit the number of subordinates, the number of members, you need to specify the appropriate class
54
 * name.
55
 * If there is no limit, you need to set it to false manually.
56
 *
57
 * @method Member createMember(array $config) Create member who is subordinate to this.
58
 * @property int $type Whether indicate this instance is an organization or a department.
59
 *
60
 * @property bool $isExcludeOtherMembers Determine whether the other organization and its subordinate departments
61
 * members could join in the current organization and its subordinate departments. (Only fit for Organization)
62
 * @property bool $isDisallowMemberJoinOther Determine whether the current organization and its subordinate
63
 * departments members could join in the other organization and its subordinate departments. (Only fit for Organization)
64
 * @property bool $isOnlyAcceptCurrentOrgMember Determine whether the current department only accept the member of
65
 * the top level organization. (Only fit for Department)
66
 * @property bool $isOnlyAcceptSuperiorOrgMember Determine whether the current department only accept the member of
67
 * the superior organization or department. (Only fit for Department)
68
 * @property string $joinPassword
69
 * @property string $joinIpAddress
70
 * @property string $joinEntranceUrl
71
 * @property bool $exitAllowWithdrawActively
72
 *
73
 * @property-read Member[] $members Get all member models of this organization/department.
74
 * @property-read User[] $memberUsers Get all members of this organization/department.
75
 * @property-read User $creator Get creator of this organization/department.
76
 * @property-read User[] $administrators Get administrators of this organization/department.
77
 * @property-read SubordinateLimit subordinateLimit
78
 * @property-read MemberLimit memberLimit
79
 * @property-read static|null $topOrganization The top level organization of current organization or departments.
80
 * @property-read Profile $profile Get profile model. Friendly to IDE.
81
 * @property-read OrganizationSetting[] $settings Get all settings.
82
 *
83
 * @version 1.0
84
 * @author vistart <[email protected]>
85
 */
86
class Organization extends User
87
{
88
    use SelfBlameableTrait;
89
90
    const TYPE_ORGANIZATION = 1;
91
    const TYPE_DEPARTMENT = 2;
92
93
    /**
94
     * @var boolean Organization does not need password and corresponding features.
95
     */
96
    public $passwordHashAttribute = false;
97
98
    /**
99
     * @var boolean Organization does not need password and corresponding features.
100
     */
101
    public $passwordResetTokenAttribute = false;
102
103
    /**
104
     * @var boolean Organization does not need password and corresponding features.
105
     */
106
    public $passwordHistoryClass = false;
107
108
    /**
109
     * @var boolean Organization does not need source.
110
     */
111
    public $sourceAttribute = false;
112
113
    /**
114
     * @var boolean Organization does not need auth key.
115
     */
116
    public $authKeyAttribute = false;
117
118
    /**
119
     * @var boolean Organization does not need access token.
120
     */
121
    public $accessTokenAttribute = false;
122
123
    /**
124
     * @var boolean Organization does not need login log.
125
     */
126
    public $loginLogClass = false;
127
128
    /**
129
     * @var string The Organization Profile Class
130
     */
131
    public $profileClass = Profile::class;
132
133
    /**
134
     * @var string The Member Class.
135
     */
136
    public $memberClass = Member::class;
137
138
    /**
139
     * @var string The Subordinate Limit Class
140
     */
141
    public $subordinateLimitClass = SubordinateLimit::class;
142
143
    /**
144
     * @var string The Member Limit Class
145
     */
146
    public $memberLimitClass = MemberLimit::class;
147
148
    /**
149
     * @var string The Organization Search Class
150
     */
151
    public $searchClass = OrganizationSearch::class;
152
153
    /**
154
     * @var string The Organization Setting Class
155
     */
156
    public $organizationSettingClass = OrganizationSetting::class;
157
158
    /**
159
     * @var Member
160
     */
161
    private $noInitMember;
162
163
    /**
164
     * @var SubordinateLimit
165
     */
166
    private $noInitSubordinateLimit;
167
168
    /**
169
     * @var MemberLimit
170
     */
171
    private $noInitMemberLimit;
172
173
    /**
174
     * @var OrganizationSetting
175
     */
176
    private $noInitOrganizationSetting;
177
178
    /**
179
     * @var User the creator of current Organization or Department.
180
     * This property is only available after registration.
181
     * Please do not access it at other times.
182
     * If you want to get creator model except registration, please
183
     * access [[$creator]] magic-property instead.
184
     */
185
    public $creatorModel;
186
187
    /**
188
     * @var array The configuration array of Organization Profile.
189
     * This property is only available after registration.
190
     * Please do not access it at other times.
191
     * If you want to get profile model except registration, please
192
     * access [[$profile]] magic-property instead.
193
     */
194
    public $profileConfig;
195
196
    const EVENT_BEFORE_ADD_MEMBER = 'eventBeforeAddMember';
197
    const EVENT_AFTER_ADD_MEMBER = 'eventAfterAddMember';
198
    const EVENT_BEFORE_REMOVE_MEMBER = 'eventBeforeRemoveMember';
199
    const EVENT_AFTER_REMOVE_MEMBER = 'eventAfterRemoveMember';
200
201
    public $cacheTagPrefix = 'tag_organization_';
202
203
    /**
204
     * @return Member
205 51
     */
206
    public function getNoInitMember()
207 51
    {
208 51
        if (!$this->noInitMember) {
209 51
            $class = $this->memberClass;
210
            $this->noInitMember = $class::buildNoInitModel();
211 51
        }
212
        return $this->noInitMember;
213
    }
214
215
    /**
216
     * @return SubordinateLimit
217 2
     */
218
    public function getNoInitSubordinateLimit()
219 2
    {
220 2
        if (!$this->noInitSubordinateLimit) {
221 2
            $class = $this->subordinateLimitClass;
222
            $this->noInitSubordinateLimit = $class::buildNoInitModel();
223 2
        }
224
        return $this->noInitSubordinateLimit;
225
    }
226
227
    /**
228
     * @return MemberLimit
229 1
     */
230
    public function getNoInitMemberLimit()
231 1
    {
232 1
        if (!$this->noInitMemberLimit) {
233 1
            $class = $this->memberLimitClass;
234
            $this->noInitMemberLimit = $class::buildNoInitModel();
235 1
        }
236
        return $this->noInitMemberLimit;
237
    }
238
239
    /**
240
     * @return null|OrganizationSetting
241 31
     */
242
    public function getNoInitOrganizationSetting()
243 31
    {
244 31
        if (!$this->noInitOrganizationSetting) {
245 31
            $class = $this->organizationSettingClass;
246
            if (empty($class)) {
247
                return null;
248 31
            }
249
            $this->noInitOrganizationSetting = $class::buildNoInitModel();
250 31
        }
251
        return $this->noInitOrganizationSetting;
252
    }
253
254
    /**
255
     * @return null|OrganizationSearch
256
     */
257
    public function getSearchModel()
258
    {
259
        $class = $this->searchClass;
260
        if (empty($class) || !class_exists($class)) {
261
            return null;
262
        }
263
        return new $class;
264
    }
265
266
    /**
267
     * @inheritdoc
268 52
     */
269
    public function init()
270 52
    {
271 52
        $this->parentAttribute = 'parent_guid';
272 52
        if (class_exists($this->memberClass)) {
273
            $this->addSubsidiaryClass('Member', ['class' => $this->memberClass]);
274 52
        }
275 52
        if ($this->skipInit) {
276
            return;
277 52
        }
278 52
        $this->on(static::$eventAfterRegister, [$this, 'onAddProfile'], $this->profileConfig);
279 52
        $this->on(static::$eventAfterRegister, [$this, 'onAssignCreator'], $this->creatorModel);
280 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeCreator']);
281 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokeAdministrators']);
282 52
        $this->on(static::EVENT_BEFORE_DELETE, [$this, 'onRevokePermissions']);
283 52
        $this->initSelfBlameableEvents();
284 52
        parent::init();
285
    }
286
287
    /**
288
     * @inheritdoc
289 1
     */
290
    public function attributeLabels()
291
    {
292 1
        return [
293 1
            $this->guidAttribute => Yii::t('user', 'GUID'),
294 1
            $this->idAttribute => Yii::t('user', 'ID'),
295 1
            $this->ipAttribute => Yii::t('user', 'IP Address'),
296 1
            $this->ipTypeAttribute => Yii::t('user', 'IP Address Type'),
297 1
            $this->parentAttribute => Yii::t('organization', 'Parent'),
298 1
            $this->createdAtAttribute => Yii::t('user', 'Creation Time'),
299 1
            $this->updatedAtAttribute => Yii::t('user', 'Last Updated Time'),
300 1
            $this->statusAttribute => Yii::t('user', 'Status'),
301 1
            'type' => Yii::t('user', 'Type'),
302 1
            'isExcludeOtherMembers' => Yii::t('organization', 'Exclude Other Members'),
303 1
            'isDisallowMemberJoinOther' => Yii::t('organization', 'Disallow Member to Join in Other Organizations'),
304 1
            'isOnlyAcceptCurrentOrgMember' => Yii::t('organization', 'Only Accept Current Organization Members'),
305
            'isOnlyAcceptSuperiorOrgMember' => Yii::t('organization', 'Only Accept Superior Organization Members'),
306
        ];
307
    }
308
309
    /**
310
     * @inheritdoc
311 52
     */
312
    public static function tableName()
313 52
    {
314
        return '{{%organization}}';
315
    }
316
317
    /**
318
     * Find.
319
     * Friendly to IDE.
320
     * @return OrganizationQuery
321 52
     */
322
    public static function find()
323 52
    {
324
        return parent::find();
325
    }
326
327
    /**
328
     * Get rules associated with type attribute.
329
     * @return array
330 51
     */
331
    protected function getTypeRules()
332
    {
333 51
        return [
334
            ['type', 'default', 'value' => static::TYPE_ORGANIZATION],
335 51
            ['type', 'required'],
336
            ['type', 'in', 'range' => [static::TYPE_ORGANIZATION, static::TYPE_DEPARTMENT]],
337
        ];
338
    }
339
340
    /**
341
     * @inheritdoc
342 51
     */
343
    public function rules()
344 51
    {
345
        return array_merge(parent::rules(), $this->getTypeRules(), $this->getSelfBlameableRules());
346
    }
347
348
    /**
349
     * Get Member Query.
350
     * @return MemberQuery
351 50
     */
352
    public function getMembers()
353 50
    {
354 50
        return $this->hasMany($this->memberClass, [
355 50
            $this->getNoInitMember()->createdByAttribute => $this->guidAttribute
356
        ])->inverseOf('organization');
357
    }
358
359
    /**
360
     * Get organization member users' query.
361
     * @return BaseUserQuery
362 6
     */
363
    public function getMemberUsers()
364 6
    {
365 6
        $noInit = $this->getNoInitMember();
366 6
        $class = $noInit->memberUserClass;
367 6
        $noInitUser = $class::buildNoInitModel();
368 6
        return $this->hasMany($class, [
369 6
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
370
        ])->via('members')->inverseOf('atOrganizations');
371
    }
372
373
    /**
374
     * Get subordinate limit query.
375
     * @return null|BaseBlameableQuery
376 2
     */
377
    public function getSubordinateLimit()
378 2
    {
379
        if (empty($this->subordinateLimitClass)) {
380
            return null;
381 2
        }
382 2
        return $this->hasOne($this->subordinateLimitClass, [
383
            $this->getNoInitSubordinateLimit()->createdByAttribute => $this->guidAttribute
384
        ]);
385
    }
386
387
    /**
388
     * Get member limit query.
389
     * @return null|BaseBlameableQuery
390 1
     */
391
    public function getMemberLimit()
392 1
    {
393
        if (empty($this->memberLimitClass)) {
394
            return null;
395 1
        }
396 1
        return $this->hasOne($this->memberLimitClass, [
397
            $this->getNoInitMemberLimit()->createdByAttribute => $this->guidAttribute
398
        ]);
399
    }
400
401
    /**
402
     * @param string|null $item If you want to get all settings, please set it null.
403
     * @return null
404 31
     */
405
    public function getSettings($item = null)
406 31
    {
407
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
408
            return null;
409 31
        }
410 31
        $query = $this->hasMany($this->organizationSettingClass, [$this->getNoInitOrganizationSetting()->createdByAttribute => $this->guidAttribute]);
411 31
        if (!empty($item)) {
412
            $query = $query->andWhere([$this->getNoInitOrganizationSetting()->idAttribute => $item]);
413 31
        }
414
        return $query;
415
    }
416
417
    /**
418
     * Set organization setting.
419
     * @param string $item
420
     * @param string $value
421
     * @param bool $unique
422
     * @return bool|null Null if organization setting not enabled.
423
     * @throws IntegrityException throw if "item-value" unique broke.
424 31
     */
425
    public function setSetting($item, $value, $unique = false)
426 31
    {
427
        if (empty($this->organizationSettingClass) || !is_string($this->organizationSettingClass)) {
428
            return null;
429 31
        }
430
        $setting = $this->getSettings($item)->one();
431 31
        /* @var $setting OrganizationSetting */
432 31
        if (!$setting) {
433 31
            $setting = $this->create($this->organizationSettingClass, [
434
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
435
            ]);
436 31
        }
437 31
        $setting->value = $value;
438
        if ($unique) {
439
            $class = $this->organizationSettingClass;
440
            if ($class::find()->andWhere([
441
                $this->getNoInitOrganizationSetting()->idAttribute => $item,
442
                $this->getNoInitOrganizationSetting()->contentAttribute => $value
443
            ])->exists()) {
444
                throw new IntegrityException("`$item` : `$value` existed.");
445
            }
446 31
        }
447
        return $setting->save();
448
    }
449
450
    /**
451
     * Get member with specified user.
452
     * @param User|string|integer $user
453
     * @return Member Null if `user` is not in this organization.
454 50
     */
455
    public function getMember($user)
456 50
    {
457
        return $this->getMembers()->user($user)->one();
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->getMembers()->user($user)->one(); of type yii\db\ActiveRecord|array|null adds the type array to the return on line 457 which is incompatible with the return type documented by rhosocial\organization\Organization::getMember of type rhosocial\organization\Member|null.
Loading history...
458
    }
459
460
    /**
461
     * Add member to organization.
462
     * @param Member|User|string|integer $member Member or User model, or User ID or GUID.
463
     * If member is created, it will be re-assigned to this parameter.
464
     * @see createMemberModel
465
     * @see createMemberModelWithUser
466
     * @return boolean
467
     * @throws DisallowMemberJoinOtherException
468
     * @throws ExcludeOtherMembersException
469
     * @throws OnlyAcceptCurrentOrgMemberException
470
     * @throws OnlyAcceptSuperiorOrgMemberException
471 50
     */
472
    public function addMember(&$member)
473 50
    {
474
        if ($this->getIsNewRecord()) {
475
            return false;
476 50
        }
477 1
        if ($this->hasReachedMemberLimit()) {
478
            return false;
479 50
        }
480 50
        $user = null;
481
        if ($member instanceof Member) {
482
            if ($member->getIsNewRecord()) {
483
                return false;
484
            }
485
            $user = $member->memberUser;
486 50
        }
487 50
        if ($member instanceof User) {
488
            $user = $member;
489 50
        }
490
        if (is_string($member) || is_int($member)) {
491
            $class = Yii::$app->user->identityClass;
492
            $user = $class::find()->guidOrId($member)->one();
493 50
        }
494
        if ($this->hasMember($user)) {
495
            return false;
496 50
        }
497
        $orgs = $user->getAtOrganizations()->all();
498 50
        /* @var $orgs Organization[] */
499 31
        foreach ($orgs as $org) {
500 1
            if ($org->topOrganization->isDisallowMemberJoinOther && !$org->topOrganization->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like $this->topOrganization can be null; however, equals() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
501
                throw new DisallowMemberJoinOtherException(Yii::t('organization', "An organization in which the user is located does not allow its members to join other organizations."));
502 31
            }
503 31
            if ($this->topOrganization->isExcludeOtherMembers && !$org->topOrganization->equals($this->topOrganization)) {
504
                throw new ExcludeOtherMembersException(Yii::t('organization', "The organization does not allow users who have joined other organizations to join."));
505
            }
506 50
        }
507 1
        if ($this->isDepartment() && $this->isOnlyAcceptCurrentOrgMember && !$this->topOrganization->hasMember($user)) {
508
            throw new OnlyAcceptCurrentOrgMemberException(Yii::t('organization' ,'This department is only accepted by members of the organization.'));
509 50
        }
510 1
        if ($this->isDepartment() && !$this->parent->equals($this->topOrganization) && $this->isOnlyAcceptSuperiorOrgMember && !$this->parent->hasMember($user)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
Bug introduced by
It seems like hasMember() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
511
            throw new OnlyAcceptSuperiorOrgMemberException(Yii::t('organization', 'This department only accepts members of the parent organization or department.'));
512
        }
513 50
514 50
        $this->trigger(self::EVENT_BEFORE_ADD_MEMBER);
515 50
        $model = null;
516
        if ($member instanceof Member) {
517 50
            $model = $this->createMemberModel($member);
518 50
        } elseif (($member instanceof User) || is_string($member) || is_int($member)) {
519
            $model = $this->createMemberModelWithUser($member);
520 50
        }
521 50
        $member = $model;
522 50
        $result = ($member instanceof Member) ? $member->save() : false;
523 50
        $this->trigger(self::EVENT_AFTER_ADD_MEMBER);
524
        return $result;
525
    }
526
527
    /**
528
     * Create member model, and set organization with this.
529
     * @param Member $member If this parameter is not new record, it's organization
530
     * will be set with this, and return it. Otherwise, it will extract `User`
531
     * model and create new `Member` model.
532
     * @see createMemberModelWithUser
533
     * @return Member
534
     */
535
    public function createMemberModel($member)
536
    {
537
        if (!$member->getIsNewRecord()) {
538
            $member->setOrganization($this);
0 ignored issues
show
Documentation introduced by
$this is of type this<rhosocial\organization\Organization>, but the function expects a object<rhosocial\organization\BaseOrganization>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
539
            return $member;
540
        }
541
        return $this->createMemberModelWithUser($member->memberUser);
542
    }
543
544
    /**
545
     * Create member model with user, and set organization with this.
546
     * @param User|string|integer $user
547
     * @return Member
548 50
     */
549
    public function createMemberModelWithUser($user)
550
    {
551 50
        $config = [
552 50
            'memberUser' => $user,
553 50
            'organization' => $this,
554
            'nickname' => '',
555 50
        ];
556 50
        $member = $this->createMember($config);
557 50
        $member->nickname = $member->memberUser->profile->nickname;
558
        return $member;
559
    }
560
561
    /**
562
     * Remove member.
563
     * Note: the creator cannot be removed.
564
     * @param Member|User $member
565
     * @return boolean
566 4
     */
567
    public function removeMember(&$member)
568 4
    {
569
        if ($this->getIsNewRecord()) {
570
            return false;
571 4
        }
572 4
        $this->trigger(self::EVENT_BEFORE_REMOVE_MEMBER);
573 4
        if ($member instanceof $this->memberClass) {
574
            $member = $member->{$member->memberAttribute};
575 4
        }
576 4
        $member = $this->getMember($member);
577
        if (!$member || $member->isCreator()) {
578
            return false;
579 4
        }
580 4
        $result = $member->delete() > 0;
581 4
        $this->trigger(self::EVENT_AFTER_REMOVE_MEMBER);
582
        return $result;
583
    }
584
585
    /**
586
     * Remove administrator.
587
     * @param Member|User|integer|string $member Member instance, or User instance or its GUID or ID.
588
     * @param boolean $keep Keep member after administrator being revoked.
589
     * @return boolean
590
     * @throws IntegrityException
591
     */
592
    public function removeAdministrator(&$member, $keep = true)
593
    {
594
        if ($this->getIsNewRecord()) {
595
            return false;
596
        }
597
        if ($member instanceof $this->memberClass) {
598
            $member = $member->{$member->memberAttribute};
599
        }
600
        $member = $this->getMember($member);
601
        if ($member && $member->isAdministrator()) {
602
            if ($keep) {
603
                return $member->revokeAdministrator();
604
            }
605
            return $this->removeMember($member);
606
        }
607
        return false;
608
    }
609
610
    /**
611
     * 
612
     * @param Event $event
613
     * @throws IntegrityException
614
     * @return boolean
615 51
     */
616
    public function onAddProfile($event)
617 51
    {
618 51
        $profile = $event->sender->createProfile($event->data);
619
        if (!$profile->save()) {
620
            throw new IntegrityException('Profile Save Failed.');
621 51
        }
622
        return true;
623
    }
624
625
    /**
626
     * 
627
     * @param Event $event
628 51
     */
629
    public function onAssignCreator($event)
630 51
    {
631
        return $event->sender->addCreator($event->data);
632
    }
633
634
    /**
635
     * 
636
     * @param Event $event
637
     * @return boolean
638 20
     */
639
    public function onRevokeCreator($event)
640 20
    {
641
        $sender = $event->sender;
642 20
        /* @var $sender static */
643
        $member = $sender->getMemberCreators()->one();
644 20
        /* @var $member Member */
645 20
        $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
646
        return $member->revokeRole($role);
0 ignored issues
show
Documentation introduced by
$role is of type string, but the function expects a object<rhosocial\user\rbac\Role>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
647
    }
648
649
    /**
650
     * 
651
     * @param Event $event
652
     * @return boolean
653 20
     */
654
    public function onRevokeAdministrators($event)
655 20
    {
656
        $sender = $event->sender;
657 20
        /* @var $sender static */
658
        $members = $sender->getMemberAdministrators()->all();
659 20
        /* @var $members Member[] */
660
        foreach ($members as $member)
661 1
        {
662
            $member->revokeAdministrator();
663 20
        }
664
        return true;
665
    }
666
667
    /**
668
     * 
669
     * @param Event $event
670 20
     */
671
    public function onRevokePermissions($event)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
672
    {
673 20
        
674
    }
675
676
    /**
677
     * Check whether current instance is an organization.
678
     * @return boolean
679 50
     */
680
    public function isOrganization()
681 50
    {
682
        return $this->type == static::TYPE_ORGANIZATION;
683
    }
684
685
    /**
686
     * Check whether current instance if a department.
687
     * @return boolean
688 50
     */
689
    public function isDepartment()
690 50
    {
691
        return $this->type == static::TYPE_DEPARTMENT;
692
    }
693
694
    /**
695
     * Check whether the current organization has a member.
696
     * @param User|string|integer $user User instance, GUID or ID.
697
     * @return boolean
698 50
     */
699
    public function hasMember($user)
700 50
    {
701
        return !empty($this->getMember($user));
702
    }
703
704
    /**
705
     * Get member query which role is specified `Creator`.
706
     * @return MemberQuery
707 24
     */
708
    public function getMemberCreators()
709 24
    {
710
        return $this->getMembers()->andWhere(['role' => [(new DepartmentCreator)->name, (new OrganizationCreator)->name]]);
711
    }
712
713
    /**
714
     * Get member query which role is specified `Administrator`.
715
     * @return MemberQuery
716 22
     */
717
    public function getMemberAdministrators()
718 22
    {
719
        return $this->getMembers()->andWhere(['role' => [(new DepartmentAdmin)->name, (new OrganizationAdmin)->name]]);
720
    }
721
722
    /**
723
     * Get user query which role is specified `Creator`.
724
     * @return BaseUserQuery
725 4
     */
726
    public function getCreator()
727 4
    {
728 4
        $noInit = $this->getNoInitMember();
729 4
        $class = $noInit->memberUserClass;
730 4
        $noInitUser = $class::buildNoInitModel();
731 4
        return $this->hasOne($class, [
732 4
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
733
        ])->via('memberCreators')->inverseOf('creatorsAtOrganizations');
734
    }
735
736
    /**
737
     * Get user query which role is specified `Administrator`.
738
     * @return BaseUserQuery
739 2
     */
740
    public function getAdministrators()
741 2
    {
742 2
        $noInit = $this->getNoInitMember();
743 2
        $class = $noInit->memberUserClass;
744 2
        $noInitUser = $class::buildNoInitModel();
745 2
        return $this->hasMany($class, [
746 2
            $noInitUser->guidAttribute => $this->getNoInitMember()->memberAttribute
747
        ])->via('memberAdministrators')->inverseOf('administratorsAtOrganizations');
748
    }
749
750
    /**
751
     * 
752
     * @param User $user
753
     * @return boolean
754
     * @throws \Exception
755
     * @throws IntegrityException
756 51
     */
757
    protected function addCreator($user)
758 51
    {
759 1
        if (!$user) {
760
            throw new InvalidParamException('Creator Invalid.');
761 50
        }
762 50
        $member = $user;
763
        $transaction = Yii::$app->db->beginTransaction();
764 50
        try {
765
            if (!$this->addMember($member)) {
766
                throw new IntegrityException('Failed to add member.');
767 50
            }
768 50
            $role = $this->isOrganization() ? (new OrganizationCreator)->name : (new DepartmentCreator)->name;
769 50
            $member->assignRole($role);
0 ignored issues
show
Documentation introduced by
$role is of type string, but the function expects a object<rhosocial\user\rbac\Role>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
The method assignRole does only exist in rhosocial\organization\Member, but not in rhosocial\user\User.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
770
            if (!$member->save()) {
771
                throw new IntegrityException('Failed to assign creator.');
772 50
            }
773
            $transaction->commit();
774
        } catch (\Exception $ex) {
775
            $transaction->rollBack();
776
            Yii::error($ex->getMessage(), __METHOD__);
777
            throw $ex;
778 50
        }
779
        return true;
780
    }
781
782
    /**
783
     * Add administrator.
784
     * @param User|integer|string $user User instance, or its GUID or ID.
785
     * @return boolean
786
     * @throws \Exception
787
     * @throws IntegrityException
788 17
     */
789
    public function addAdministrator($user)
790 17
    {
791
        $transaction = Yii::$app->db->beginTransaction();
792 17
        try {
793
            if (!$this->hasMember($user) && !$this->addMember($user)) {
794
                throw new IntegrityException(Yii::t('organization', 'Failed to add member.'));
795 17
            }
796 17
            $member = $this->getMember($user);
797 17
            $member->assignAdministrator();
798 2
            $transaction->commit();
799 2
        } catch (\Exception $ex) {
800 2
            $transaction->rollBack();
801 2
            Yii::error($ex->getMessage(), __METHOD__);
802
            throw $ex;
803 17
        }
804
        return true;
805
    }
806
807
    /**
808
     * Check whether the current organization has administrator.
809
     * @param User|integer|string $user
810
     * @return boolean
811 2
     */
812
    public function hasAdministrator($user)
813 2
    {
814 2
        $member = $this->getMember($user);
815
        if (!$member) {
816
            return false;
817 2
        }
818
        return $member->isAdministrator();
819
    }
820
821
    /**
822
     * Check whether this organization has reached the upper limit of subordinates.
823
     * @return boolean
824 19
     */
825
    public function hasReachedSubordinateLimit()
826 19
    {
827 19
        $remaining = $this->getRemainingSubordinatePlaces();
828
        if ($remaining === false) {
829
            return false;
830 19
        }
831
        return $remaining <= 0;
832
    }
833
834
    /**
835
     * Get the remaining places of subordinates.
836
     * @return bool|int False if no limit
837 19
     */
838
    public function getRemainingSubordinatePlaces()
839 19
    {
840 19
        $class = $this->subordinateLimitClass;
841
        if (empty($class)) {
842
            return false;
843 19
        }
844 19
        $limit = $class::getLimit($this);
845
        if ($limit === false) {
846
            return false;
847 19
        }
848 19
        $count = (int)$this->getChildren()->count();
849
        return $limit - $count;
850
    }
851
852
    /**
853
     * Check whether this organization has reached the upper limit of members.
854
     * @return boolean
855 50
     */
856
    public function hasReachedMemberLimit()
857 50
    {
858 50
        $remaining = $this->getRemainingMemberPlaces();
859
        if ($remaining === false) {
860
            return false;
861 50
        }
862
        return $remaining <= 0;
863
    }
864
865
    /**
866
     * Get the remaining places of members.
867
     * @return bool|int False if no limit.
868 50
     */
869
    public function getRemainingMemberPlaces()
870 50
    {
871 50
        $class = $this->memberLimitClass;
872
        if (empty($class)) {
873
            return false;
874 50
        }
875 50
        $limit = $class::getLimit($this);
876
        if ($limit === false) {
877
            return false;
878 50
        }
879 50
        $count = (int)$this->getMembers()->count();
880
        return $limit - $count;
881
    }
882
883
    const SETTING_ITEM_EXCLUDE_OTHER_MEMBERS = 'exclude_other_members';
884
885
    /**
886
     * @return bool
887 31
     */
888
    public function getIsExcludeOtherMembers()
889 31
    {
890 31
        $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
891 31
        if (!$setting) {
892 31
            $this->setIsExcludeOtherMembers(false);
893
            $setting = $this->getSettings(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS)->one();
894 31
        }
895
        return $setting->value == '1';
896
    }
897
898
    /**
899
     * @param bool $value
900
     * @return bool
901 31
     */
902
    public function setIsExcludeOtherMembers($value = true)
903 31
    {
904
        return $this->setSetting(static::SETTING_ITEM_EXCLUDE_OTHER_MEMBERS, $value ? '1' : '0');
905
    }
906
907
    const SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER = 'disallow_member_join_other';
908
909
    /**
910
     * @return bool
911 31
     */
912
    public function getIsDisallowMemberJoinOther()
913 31
    {
914 31
        $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
915 31
        if (!$setting) {
916 31
            $this->setIsDisallowMemberJoinOther(false);
917
            $setting = $this->getSettings(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER)->one();
918 31
        }
919
        return $setting->value == '1';
920
    }
921
922
    /**
923
     * @param bool $value
924
     * @return bool
925 31
     */
926
    public function setIsDisallowMemberJoinOther($value = true)
927 31
    {
928
        return $this->setSetting(static::SETTING_ITEM_DISALLOW_MEMBER_JOIN_OTHER, $value ? '1' : '0');
929
    }
930
931
    const SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER = 'only_accept_current_org_member';
932
933
    /**
934
     * @return bool
935 18
     */
936
    public function getIsOnlyAcceptCurrentOrgMember()
937 18
    {
938 18
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
939 18
        if (!$setting) {
940 18
            $this->setIsOnlyAcceptCurrentOrgMember(false);
941
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER)->one();
942 18
        }
943
        return $setting->value == '1';
944
    }
945
946
    /**
947
     * @param bool $value
948
     * @return bool
949 18
     */
950
    public function setIsOnlyAcceptCurrentOrgMember($value = true)
951 18
    {
952
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_CURRENT_ORG_MEMBER, $value ? '1' : '0');
953
    }
954
955
    const SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER = 'only_accept_superior_org_member';
956
957
    /**
958
     * @return bool
959 10
     */
960
    public function getIsOnlyAcceptSuperiorOrgMember()
961 10
    {
962
        if ($this->parent && $this->parent->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
963
            return $this->getIsOnlyAcceptCurrentOrgMember();
964 10
        }
965 10
        $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
966 10
        if (!$setting) {
967 10
            $this->setIsOnlyAcceptSuperiorOrgMember(false);
968
            $setting = $this->getSettings(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER)->one();
969 10
        }
970
        return $setting->value == '1';
971
    }
972
973
    /**
974
     * @param bool $value
975
     * @return bool
976 10
     */
977
    public function setIsOnlyAcceptSuperiorOrgMember($value = true)
978 10
    {
979
        if ($this->parent && $this->parent->equals($this->topOrganization)) {
0 ignored issues
show
Bug introduced by
It seems like equals() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
980
            return $this->setIsOnlyAcceptCurrentOrgMember($value);
981 10
        }
982
        return $this->setSetting(static::SETTING_ITEM_ONLY_ACCEPT_SUPERIOR_ORG_MEMBER, $value ? '1' : '0');
983
    }
984
985
    const SETTING_ITEM_JOIN_PASSWORD = 'join_password';
986
987
    /**
988
     * Get join password.
989
     * @return mixed
990
     */
991
    public function getJoinPassword()
992
    {
993
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
994
        if (!$setting) {
995
            $this->setJoinPassword();
996
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_PASSWORD)->one();
997
        }
998
        return $setting->value;
999
    }
1000
1001
    /**
1002
     * Set join password.
1003
     * @param string $value
1004
     * @return bool|null
1005
     */
1006
    public function setJoinPassword($value = '')
1007
    {
1008
        return $this->setSetting(static::SETTING_ITEM_JOIN_PASSWORD, $value);
1009
    }
1010
1011
    const SETTING_ITEM_JOIN_IP_ADDRESS = 'join_ip_address';
1012
1013
    /**
1014
     * Get Join IP address
1015
     * @return mixed
1016
     */
1017
    public function getJoinIpAddress()
1018
    {
1019
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1020
        if (!$setting) {
1021
            $this->setJoinIpAddress();
1022
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_IP_ADDRESS)->one();
1023
        }
1024
        return $setting->value;
1025
    }
1026
1027
    /**
1028
     * Set join IP address.
1029
     * @param $value
1030
     * @return bool|null
1031
     */
1032
    public function setJoinIpAddress($value = '')
1033
    {
1034
        return $this->setSetting(static::SETTING_ITEM_JOIN_IP_ADDRESS, $value);
1035
    }
1036
1037
    const SETTING_ITEM_JOIN_ENTRANCE_URL = 'join_entrance_url';
1038
1039
    /**
1040
     * Get join entrance URL.
1041
     * This setting should be confirmed unique.
1042
     * @return string
1043
     */
1044
    public function getJoinEntranceUrl()
1045
    {
1046
        $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1047
        if (!$setting) {
1048
            $this->setJoinEntranceUrl();
1049
            $setting = $this->getSettings(static::SETTING_ITEM_JOIN_ENTRANCE_URL)->one();
1050
        }
1051
        return $setting->value;
1052
    }
1053
1054
    /**
1055
     * Set join entrance URL.
1056
     * @param string $value
1057
     * @return bool|null
1058
     */
1059
    public function setJoinEntranceUrl($value = '')
1060
    {
1061
        return $this->setSetting(static::SETTING_ITEM_JOIN_ENTRANCE_URL, $value, !empty($value));
1062
    }
1063
1064
    const SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY = 'exit_allow_withdraw_actively';
1065
1066 31
    /**
1067
     * @return bool
1068 31
     */
1069 31
    public function getExitAllowWithdrawActively()
1070
    {
1071 18
        $setting = $this->getSettings(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY)->one();
1072 18
        if (!$setting) {
1073
            $this->setExitAllowWithdrawActively();
1074
            $setting = $this->getSettings(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY)->one();
1075
        }
1076
        return $setting->value == '1';
1077
    }
1078
1079
    /**
1080
     * @param bool $value
1081 2
     * @return bool|null
1082
     */
1083 2
    public function setExitAllowWithdrawActively($value = false)
1084 2
    {
1085 1
        return $this->setSetting(static::SETTING_ITEM_EXIT_ALLOW_WITHDRAW_ACTIVELY, $value ? '1' : '0');
1086
    }
1087 2
1088
    /**
1089 2
     * @return $this|null|static
1090 2
     */
1091 2
    public function getTopOrganization()
1092
    {
1093
        if ($this->isOrganization()) {
1094 2
            return $this;
1095
        }
1096
        $chain = $this->getAncestorChain();
1097
        return static::findOne(end($chain));
1098
    }
1099
1100
    /**
1101
     * Check whether the subordinates have the [[$user]]
1102
     * Note, this operation may consume the quantity of database selection.
1103
     * @param User $user
1104
     * @return bool
1105
     */
1106
    public function hasMemberInSubordinates($user)
1107
    {
1108
        if ($this->getChildren()->joinWith(['memberUsers mu_alias'])
1109
            ->andWhere(['mu_alias.' . $user->guidAttribute => $user->getGUID()])->exists()) {
1110
            return true;
1111
        }
1112
        $children = $this->children;
1113
        /* @var $children static[] */
1114
        foreach ($children as $child) {
1115
            if ($child->hasMemberInSubordinates($user)) {
1116
                return true;
1117
            }
1118
        }
1119
        return false;
1120
    }
1121
}
1122