ConfirmationBehavior   A
last analyzed

Complexity

Total Complexity 33

Size/Duplication

Total Lines 273
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Importance

Changes 0
Metric Value
wmc 33
lcom 1
cbo 7
dl 0
loc 273
rs 9.3999
c 0
b 0
f 0

13 Methods

Rating   Name   Duplication   Size   Complexity  
A events() 0 6 1
A beforeSave() 0 3 1
B protectAttributes() 0 18 6
B isAuthorised() 0 26 6
A userIsAuthorised() 0 7 3
A createConfirmationRequest() 0 17 1
A hasChanged() 0 3 1
A getChangedValues() 0 11 3
A resetAttribute() 0 3 1
A createFeedbackMessage() 0 3 1
A displayMessage() 0 3 1
A sendSecondFactorMessage() 0 13 2
B getEmail() 0 28 6
1
<?php
2
/**
3
 * Created by PhpStorm.
4
 * User: joels
5
 * Date: 5/3/17
6
 * Time: 6:12 PM
7
 */
8
9
namespace enigmatix\confirmation;
10
11
12
use yii\base\Behavior;
13
use yii\base\Event;
14
use yii\base\InvalidCallException;
15
use yii\db\BaseActiveRecord;
16
use Yii;
17
use yii\helpers\ArrayHelper;
18
19
20
21
/**
22
 * Class ConfirmationBehavior
23
 *
24
 * @package enigmatix\confirmation
25
 */
26
class ConfirmationBehavior extends Behavior
27
{
28
    /**
29
     * @var bool whether to skip typecasting of `null` values.
30
     * If enabled attribute value which equals to `null` will not be type-casted (e.g. `null` remains `null`),
31
     * otherwise it will be converted according to the type configured at [[attributeTypes]].
32
     */
33
    public $skipOnNull = true;
34
35
    /**
36
     * @var array a list of the attributes to be protected, provided when constructing the behavior and attaching,
37
     *            or in the model's behaviors method.
38
     */
39
    public $protectedAttributes = [];
40
41
    /**
42
     * @var string If a release token has been supplied, provided the corresponding release object is valid, the change
43
     *             will be executed.
44
     */
45
    public $releaseToken;
46
47
    /**
48
     * @var array A list of roles that can bypass the protection and make the change without triggering a confirmation
49
     *            request.
50
     */
51
    public $allow = [];
52
53
    /**
54
     * @var string namespace of the object that stores and executes the ConfirmationRequest
55
     */
56
    public $confirmationRequestClass = 'enigmatix\\confirmation\\ConfirmationRequest';
57
58
    /**
59
     * @var string delivery method for the Confirmation Request.  Currently only email is supported.
60
     */
61
    public $secondFactor = 'email';
62
63
64
    /**
65
     * @var string the name of the variable to use to traverse to the user table from the secured model.
66
     */
67
    public $createdByAttribute = 'createdBy';
68
69
    /**
70
     * @var string
71
     */
72
    public $confirmationViewPath = '@vendor/enigmatix/yii2-confirmation/mail/_confirmationEmail';
73
74
    /**
75
     * @var string The name of the table attribute that tracks when a record is updated.  This value is ignored when
76
     *             determining of a record has changed values in it.
77
     */
78
    public $timestampAttribute = 'updated_at';
79
    /**
80
     * @inheritdoc
81
     */
82
    public function events()
83
    {
84
85
        return [BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave'];
86
87
    }
88
89
    /**
90
     * @param $event Event;
91
     */
92
93
    public function beforeSave($event) {
94
        $this->protectAttributes();
95
    }
96
97
98
    /**
99
     * Business logic around triggering the confirmation request.
100
     */
101
    protected function protectAttributes()
102
    {
103
        $user = Yii::$app->user;
104
105
        $changedValues = $this->getChangedValues();
106
107
        foreach ($changedValues as $attribute => $value) {
108
109
            if ($this->skipOnNull && $value === null || $attribute == $this->timestampAttribute) {
110
                            continue;
111
            }
112
113
            if (!$this->isAuthorised($user, $attribute, $value)) {
114
                $this->createConfirmationRequest();
115
                $this->resetAttribute($attribute);
116
            }
117
        }
118
    }
119
120
    /**
121
     * Checks whether a user is allowed to make the change without triggering a confirmation request.
122
     * @param \yii\web\User $user
123
     * @param string $attribute
124
     * @param string $value
125
     *
126
     * @return bool
127
     */
128
    protected function isAuthorised($user, $attribute, $value) {
129
130
        //Check for pre-defined administration roles
131
        if ($this->userIsAuthorised($user))
132
            return true;
133
134
        //Check for valid release token , eg that the token exists and is for the same record as this
135
        if ($this->releaseToken != null) {
136
            $confirmation = ConfirmationRequest::findOne(['release_token' => $this->releaseToken]);
137
138
            if ($confirmation == null)
139
                return false;
140
141
            $model = $confirmation->constructObject();
142
143
            return $this->owner->getPrimaryKey(true) == $model->getPrimaryKey(true);
144
145
        }
146
147
        //Check to see if any protected attributes have been altered
148
        foreach ($this->protectedAttributes as $attribute)
149
            if ($this->hasChanged($attribute))
150
                return false;
151
152
        return true;
153
    }
154
155
    /**
156
     * Iterates over roles to determine if the user is authorised to complete the action.
157
     * @param \yii\web\User $user
158
     *
159
     * @return bool
160
     */
161
    protected function userIsAuthorised($user) {
162
        foreach ($this->allow as $role)
163
            if ($user->can($role))
164
                return true;
165
166
        return false;
167
    }
168
169
    /**
170
     * Business logic handling the creation of the Confirmation Request, and sending the second factor message.
171
     */
172
    protected function createConfirmationRequest() {
173
174
        $model         = $this->owner;
175
        $changedValues = $this->getChangedValues();
176
177
        /* @var ConfirmationRequest $request */
178
        
179
        $request = new $this->confirmationRequestClass([
180
            'model'  => $model->className(),
181
            'object' => serialize($model),
182
            'values' => serialize($changedValues),
183
        ]);
184
185
        $request->save();
186
187
        $this->sendSecondFactorMessage($request);
188
    }
189
190
191
    /**
192
     * Determines whether an attribute has been changed in the object.
193
     * @param string $attribute
194
     *
195
     * @return bool
196
     */
197
    protected function hasChanged($attribute) {
198
        return $this->owner->oldAttributes[$attribute] != $this->owner->{$attribute};
199
    }
200
201
    /**
202
     * Fetches all values which have changed, expect for the timestamp attribute.
203
     * @return array
204
     */
205
    public function getChangedValues() {
206
        $changedAttributes = [];
207
208
        foreach ($this->owner->attributes() as $attribute)
209
            if ($this->hasChanged($attribute))
210
                $changedAttributes[$attribute] = $this->owner->$attribute;
211
212
        unset($changedAttributes[$this->timestampAttribute]);
213
214
        return $changedAttributes;
215
    }
216
217
    /**
218
     * Sets an attribute back to it's original value when it was fetched.
219
     * @param string $attribute
220
     */
221
    protected function resetAttribute($attribute) {
222
        $this->owner->$attribute = $this->owner->oldAttributes[$attribute];
223
    }
224
225
    /**
226
     * Adds a flash message to the interface stating the change has been held over pending confirmation.
227
     * @param ConfirmationRequest $model
228
     */
229
    public function createFeedbackMessage($model) {
230
        $this->displayMessage($model);
231
    }
232
233
    /**
234
     * Business logic around displaying an appropriate feedback message to the user regbarding the change.
235
     * @param $model
236
     */
237
    protected function displayMessage($model) {
238
        Yii::$app->session->setFlash('warning', 'Your update is pending confirmation.  Please check your email for a confirmation link.');
239
    }
240
241
    /**
242
     * Business logic around transmitting the second factor message.
243
     * @param ConfirmationRequest $model
244
     */
245
    public function sendSecondFactorMessage($model) {
246
        switch ($this->secondFactor) {
247
            case 'email':
248
                Yii::$app->mailer
249
                    ->compose($this->confirmationViewPath, ['model' => $model])
250
                    ->setTo([$this->getEmail($model)])
251
                    ->send();
252
                $this->createFeedbackMessage($model);
253
                break;
254
            default:
255
                break;
256
        }
257
    }
258
259
    /**
260
     * Attempts to retrieve an address from several places within the request.  Firstly, attempts to find an email in the
261
     * changed values, and then looks within the current object for an email, and lastly attempts to traverse to the User
262
     * who created the object if an identifier has been recorded.
263
     *
264
     * @param ConfirmationRequest $model
265
     *
266
     * @return string
267
     * @throws InvalidCallException
268
     */
269
    protected function getEmail($model) {
270
271
        $values = unserialize($model->values);
272
        $email  = ArrayHelper::getValue($values, 'email');
273
        $object = $model->constructObject();
274
275
        if ($email == null) {
276
            $email = ArrayHelper::getValue($values, 'email_address');
277
        }
278
279
        if ($email == null) {
280
            $email = ArrayHelper::getValue($object, 'email');
281
        }
282
283
        if ($email == null) {
284
            $email = ArrayHelper::getValue($object, 'email_address');
285
        }
286
287
        if ($email == null) {
288
            $email = ArrayHelper::getValue($object, $this->createdByAttribute . '.email');
289
        }
290
291
        if ($email == null) {
292
            throw new InvalidCallException('Unable to locate email address via record, changed values, or user account');
293
        }
294
295
        return $email;
296
    }
297
298
}
299