Completed
Push — master ( a126d8...365252 )
by Henry
02:01
created

RecordsRequestHandler::onBeforeRecordValidated()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 0

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 0
nc 1
nop 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
1
<?php
2
namespace Divergence\Controllers;
3
4
use Exception;
5
6
use Divergence\Helpers\JSON;
7
use Divergence\Helpers\JSONP;
8
use Divergence\Helpers\Util as Util;
9
use Divergence\IO\Database\MySQL as DB;
10
use Divergence\Models\ActiveRecord as ActiveRecord;
11
12
abstract class RecordsRequestHandler extends RequestHandler
13
{
14
    public static $config;
15
16
    // configurables
17
    public static $recordClass;
18
    public static $accountLevelRead = false;
19
    public static $accountLevelBrowse = 'Staff';
20
    public static $accountLevelWrite = 'Staff';
21
    public static $accountLevelAPI = false;
22
    public static $browseOrder = false;
23
    public static $browseConditions = false;
24
    public static $browseLimitDefault = false;
25
    public static $editableFields = false;
26
    public static $searchConditions = false;
27
    
28
    public static $calledClass = __CLASS__;
29
    public static $responseMode = 'dwoo';
30
    
31
    public static function handleRequest()
32
    {
33
        // save static class
34
        static::$calledClass = get_called_class();
35
    
36
        // handle JSON requests
37
        if (static::peekPath() == 'json') {
38
            // check access for API response modes
39
            static::$responseMode = static::shiftPath();
40
            if (in_array(static::$responseMode, ['json','jsonp'])) {
41
                if (!static::checkAPIAccess()) {
42
                    return static::throwAPIUnAuthorizedError();
43
                }
44
            }
45
        }
46
        
47
        return static::handleRecordsRequest();
48
    }
49
50
51
    public static function handleRecordsRequest($action = false)
52
    {
53
        switch ($action ? $action : $action = static::shiftPath()) {
54
            case 'save':
55
            {
56
                return static::handleMultiSaveRequest();
57
            }
58
            
59
            case 'destroy':
60
            {
61
                return static::handleMultiDestroyRequest();
62
            }
63
            
64
            case 'create':
65
            {
66
                return static::handleCreateRequest();
67
            }
68
            
69
            case '':
70
            case false:
71
            {
72
                return static::handleBrowseRequest();
73
            }
74
75
            default:
76
            {
77
                if ($Record = static::getRecordByHandle($action)) {
78
                    if (!static::checkReadAccess($Record)) {
79
                        return static::throwUnauthorizedError();
80
                    }
81
82
                    return static::handleRecordRequest($Record);
83
                } else {
84
                    return static::throwRecordNotFoundError();
85
                }
86
            }
87
        }
88
    }
89
    
90
    public static function getRecordByHandle($handle)
91
    {
92
        $className = static::$recordClass;
93
        
94
        if (method_exists($className, 'getByHandle')) {
95
            return $className::getByHandle($handle);
96
        }
97
    }
98
99
    public static function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = [])
100
    {
101
        if (!static::checkBrowseAccess(func_get_args())) {
102
            return static::throwUnauthorizedError();
103
        }
104
            
105
        if (static::$browseConditions) {
106
            if (!is_array(static::$browseConditions)) {
1 ignored issue
show
introduced by
The condition is_array(static::browseConditions) is always false.
Loading history...
107
                static::$browseConditions = [static::$browseConditions];
108
            }
109
            $conditions = array_merge(static::$browseConditions, $conditions);
110
        }
111
        
112
        if (empty($_REQUEST['offset']) && is_numeric($_REQUEST['start'])) {
113
            $_REQUEST['offset'] = $_REQUEST['start'];
114
        }
115
        
116
        $limit = !empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit']) ? $_REQUEST['limit'] : static::$browseLimitDefault;
117
        $offset = !empty($_REQUEST['offset']) && is_numeric($_REQUEST['offset']) ? $_REQUEST['offset'] : false;
118
        
119
        $options = Util::prepareOptions($options, [
120
            'limit' =>  $limit,
121
            'offset' => $offset,
122
            'order' => static::$browseOrder,
123
        ]);
124
        
125
        // process sorter
126
        if (!empty($_REQUEST['sort'])) {
127
            $sort = json_decode($_REQUEST['sort'], true);
128
            if (!$sort || !is_array($sort)) {
129
                return static::respond('error', [
130
                    'success' => false,
131
                    'failed' => [
132
                        'errors'	=>	'Invalid sorter.',
133
                    ],
134
                ]);
135
            }
136
137
            if (is_array($sort)) {
1 ignored issue
show
introduced by
The condition is_array($sort) is always true.
Loading history...
138
                foreach ($sort as $field) {
139
                    $options['order'][$field['property']] = $field['direction'];
140
                }
141
            }
142
        }
143
        
144
        // process filter
145
        if (!empty($_REQUEST['filter'])) {
146
            $filter = json_decode($_REQUEST['filter'], true);
147
            if (!$filter || !is_array($filter)) {
148
                return static::respond('error', [
149
                    'success' => false,
150
                    'failed' => [
151
                        'errors'	=>	'Invalid filter.',
152
                    ],
153
                ]);
154
            }
155
156
            foreach ($filter as $field) {
157
                $conditions[$field['property']] = $field['value'];
158
            }
159
        }
160
161
        $className = static::$recordClass;
162
163
        return static::respond(
164
            isset($responseID) ? $responseID : static::getTemplateName($className::$pluralNoun),
165
            array_merge($responseData, [
166
                'success' => true,
167
                'data' => $className::getAllByWhere($conditions, $options),
168
                'conditions' => $conditions,
169
                'total' => DB::foundRows(),
170
                'limit' => $options['limit'],
171
                'offset' => $options['offset'],
172
            ])
173
        );
174
    }
175
176
177
    public static function handleRecordRequest(ActiveRecord $Record, $action = false)
178
    {
179
        switch ($action ? $action : $action = static::shiftPath()) {
180
            case '':
181
            case false:
182
            {
183
                $className = static::$recordClass;
184
                
185
                return static::respond(static::getTemplateName($className::$singularNoun), [
186
                    'success' => true,
187
                    'data' => $Record,
188
                ]);
189
            }
190
            
191
            case 'edit':
192
            {
193
                return static::handleEditRequest($Record);
194
            }
195
            
196
            case 'delete':
197
            {
198
                return static::handleDeleteRequest($Record);
199
            }
200
        
201
            default:
202
            {
203
                return static::onRecordRequestNotHandled($Record, $action);
204
            }
205
        }
206
    }
207
208
209
210
    public static function handleMultiSaveRequest()
211
    {
212
        $className = static::$recordClass;
213
    
214
        $PrimaryKey = $className::getPrimaryKey();
215
            
216
        if (static::$responseMode == 'json' && in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT'])) {
217
            $JSONData = JSON::getRequestData();
218
            if (is_array($JSONData)) {
219
                $_REQUEST = $JSONData;
220
            }
221
        }
222
        
223
        if ($className::fieldExists(key($_REQUEST['data']))) {
224
            $_REQUEST['data'] = [$_REQUEST['data']];
225
        }
226
227
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
228
            if (static::$responseMode == 'json') {
229
                return static::respond('error', [
230
                    'success' => false,
231
                    'failed' => [
232
                        'errors'	=>	'Save expects "data" field as array of records.',
233
                    ],
234
                ]);
235
            }
236
        }
237
        
238
        $results = [];
239
        $failed = [];
240
241
        foreach ($_REQUEST['data'] as $datum) {
242
            // get record
243
            if (empty($datum[$PrimaryKey])) {
244
                $Record = new $className::$defaultClass();
245
                static::onRecordCreated($Record, $datum);
246
            } else {
247
                if (!$Record = $className::getByID($datum[$PrimaryKey])) {
248
                    $failed[] = [
249
                        'record' => $datum,
250
                        'errors' => 'Record not found',
251
                    ];
252
                    continue;
253
                }
254
            }
255
            
256
            // check write access
257
            if (!static::checkWriteAccess($Record)) {
258
                $failed[] = [
259
                    'record' => $datum,
260
                    'errors' => 'Write access denied',
261
                ];
262
                continue;
263
            }
264
            
265
            // apply delta
266
            static::applyRecordDelta($Record, $datum);
267
268
            // call template function
269
            static::onBeforeRecordValidated($Record, $datum);
270
271
            // try to save record
272
            try {
273
                // call template function
274
                static::onBeforeRecordSaved($Record, $datum);
275
276
                $Record->save();
277
                $results[] = (!$Record::fieldExists('Class') || get_class($Record) == $Record->Class) ? $Record : $Record->changeClass();
278
                
279
                // call template function
280
                static::onRecordSaved($Record, $datum);
281
            } catch (Exception $e) {
282
                $failed[] = [
283
                    'record' => $Record->data,
284
                    'validationErrors' => $Record->validationErrors,
285
                ];
286
            }
287
        }
288
        
289
        
290
        return static::respond(static::getTemplateName($className::$pluralNoun).'Saved', [
291
            'success' => count($results) || !count($failed),
292
            'data' => $results,
293
            'failed' => $failed,
294
        ]);
295
    }
296
    
297
    
298
    public static function handleMultiDestroyRequest()
299
    {
300
        $className = static::$recordClass;
301
302
        $PrimaryKey = $className::getPrimaryKey();
303
            
304
        if (static::$responseMode == 'json' && in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT','DELETE'])) {
305
            $JSONData = JSON::getRequestData();
306
            if (is_array($JSONData)) {
307
                $_REQUEST = $JSONData;
308
            }
309
        }
310
        
311
        if ($className::fieldExists(key($_REQUEST['data']))) {
312
            $_REQUEST['data'] = [$_REQUEST['data']];
313
        }
314
315
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
316
            if (static::$responseMode == 'json') {
317
                return static::respond('error', [
318
                    'success' => false,
319
                    'failed' => [
320
                        'errors'	=>	'Save expects "data" field as array of records.',
321
                    ],
322
                ]);
323
            }
324
        }
325
326
327
        $results = [];
328
        $failed = [];
329
        
330
        foreach ($_REQUEST['data'] as $datum) {
331
            // get record
332
            if (is_numeric($datum)) {
333
                $recordID = $datum;
334
            } elseif (!empty($datum[$PrimaryKey]) && is_numeric($datum[$PrimaryKey])) {
335
                $recordID = $datum[$PrimaryKey];
336
            } else {
337
                $failed[] = [
338
                    'record' => $datum,
339
                    'errors' => $PrimaryKey.' missing',
340
                ];
341
                continue;
342
            }
343
344
            if (!$Record = $className::getByField($PrimaryKey, $recordID)) {
345
                $failed[] = [
346
                    'record' => $datum,
347
                    'errors' => $PrimaryKey.' not found',
348
                ];
349
                continue;
350
            }
351
            
352
            // check write access
353
            if (!static::checkWriteAccess($Record)) {
354
                $failed[] = [
355
                    'record' => $datum,
356
                    'errors' => 'Write access denied',
357
                ];
358
                continue;
359
            }
360
        
361
            // destroy record
362
            if ($Record->destroy()) {
363
                $results[] = $Record;
364
            }
365
        }
366
        
367
        return static::respond(static::getTemplateName($className::$pluralNoun).'Destroyed', [
368
            'success' => count($results) || !count($failed),
369
            'data' => $results,
370
            'failed' => $failed,
371
        ]);
372
    }
373
374
375
    public static function handleCreateRequest(ActiveRecord $Record = null)
376
    {
377
        // save static class
378
        static::$calledClass = get_called_class();
379
380
        if (!$Record) {
381
            $className = static::$recordClass;
382
            $Record = new $className::$defaultClass();
383
        }
384
        
385
        // call template function
386
        static::onRecordCreated($Record, $_REQUEST);
387
388
        return static::handleEditRequest($Record);
389
    }
390
391
    public static function handleEditRequest(ActiveRecord $Record)
392
    {
393
        $className = static::$recordClass;
394
395
        if (!static::checkWriteAccess($Record)) {
396
            return static::throwUnauthorizedError();
397
        }
398
399
        if (in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT'])) {
400
            if (static::$responseMode == 'json') {
401
                $_REQUEST = JSON::getRequestData();
402
                if (is_array($_REQUEST['data'])) {
403
                    $_REQUEST = $_REQUEST['data'];
404
                }
405
            }
406
            $_REQUEST = $_REQUEST ? $_REQUEST : $_POST;
407
        
408
            // apply delta
409
            static::applyRecordDelta($Record, $_REQUEST);
410
            
411
            // call template function
412
            static::onBeforeRecordValidated($Record, $_REQUEST);
413
414
            // validate
415
            if ($Record->validate()) {
416
                // call template function
417
                static::onBeforeRecordSaved($Record, $_REQUEST);
418
                
419
                try {
420
                    // save session
421
                    $Record->save();
422
                } catch (Exception $e) {
423
                    return static::respond('Error', [
424
                        'success' => false,
425
                        'failed' => [
426
                            'errors'	=>	$e->getMessage(),
427
                        ],
428
                    ]);
429
                }
430
                
431
                // call template function
432
                static::onRecordSaved($Record, $_REQUEST);
433
        
434
                // fire created response
435
                $responseID = static::getTemplateName($className::$singularNoun).'Saved';
436
                $responseData = [
437
                    'success' => true,
438
                    'data' => $Record,
439
                ];
440
                return static::respond($responseID, $responseData);
441
            }
442
            
443
            // fall through back to form if validation failed
444
        }
445
        
446
        $responseID = static::getTemplateName($className::$singularNoun).'Edit';
447
        $responseData = [
448
            'success' => false,
449
            'data' => $Record,
450
        ];
451
    
452
        return static::respond($responseID, $responseData);
453
    }
454
455
456
    public static function handleDeleteRequest(ActiveRecord $Record)
457
    {
458
        $className = static::$recordClass;
459
460
        if (!static::checkWriteAccess($Record)) {
461
            return static::throwUnauthorizedError();
462
        }
463
    
464
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
465
            $Record->destroy();
466
                    
467
            // call cleanup function after delete
468
            static::onRecordDeleted($Record, $data);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $data seems to be never defined.
Loading history...
469
            
470
            // fire created response
471
            return static::respond(static::getTemplateName($className::$singularNoun).'Deleted', [
472
                'success' => true,
473
                'data' => $Record,
474
            ]);
475
        }
476
    
477
        return static::respond('confirm', [
478
            'question' => 'Are you sure you want to delete this '.$className::$singularNoun.'?',
479
            'data' => $Record,
480
        ]);
481
    }
482
    
483
    
484
    public static function respond($responseID, $responseData = [], $responseMode = false)
485
    {
486
        // default to static property
487
        if (!$responseMode) {
488
            $responseMode = static::$responseMode;
489
        }
490
    
491
        return parent::respond($responseID, $responseData, $responseMode);
492
    }
493
    
494
    // access control template functions
495
    public static function checkBrowseAccess($arguments)
496
    {
497
        return true;
498
    }
499
500
    public static function checkReadAccess(ActiveRecord $Record)
501
    {
502
        return true;
503
    }
504
    
505
    public static function checkWriteAccess(ActiveRecord $Record)
506
    {
507
        return true;
508
    }
509
    
510
    public static function checkAPIAccess()
511
    {
512
        return true;
513
    }
514
    
515
    public static function throwUnauthorizedError()
516
    {
517
        return static::respond('Unauthorized', [
518
            'success' => false,
519
            'failed' => [
520
                'errors'	=>	'Login required.',
521
            ],
522
        ]);
523
    }
524
525
    public static function throwAPIUnAuthorizedError()
526
    {
527
        return static::respond('Unauthorized', [
528
            'success' => false,
529
            'failed' => [
530
                'errors'	=>	'API access required.',
531
            ],
532
        ]);
533
    }
534
535
    public static function throwNotFoundError()
536
    {
537
        return static::respond('error', [
538
            'success' => false,
539
            'failed' => [
540
                'errors'	=>	'Record not found.',
541
            ],
542
        ]);
543
    }
544
    
545
    public static function onRecordRequestNotHandled(ActiveRecord $Record, $action)
546
    {
547
        return static::respond('error', [
548
            'success' => false,
549
            'failed' => [
550
                'errors'	=>	'Malformed request.',
551
            ],
552
        ]);
553
    }
554
    
555
556
557
    public static function getTemplateName($noun)
558
    {
559
        return preg_replace_callback('/\s+([a-zA-Z])/', function ($matches) {
560
            return strtoupper($matches[1]);
561
        }, $noun);
562
    }
563
    
564
    public static function applyRecordDelta(ActiveRecord $Record, $data)
565
    {
566
        if (is_array(static::$editableFields)) {
1 ignored issue
show
introduced by
The condition is_array(static::editableFields) is always false.
Loading history...
567
            $Record->setFields(array_intersect_key($data, array_flip(static::$editableFields)));
568
        } else {
569
            $Record->setFields($data);
570
        }
571
    }
572
    
573
    // event template functions
574
    protected static function onRecordCreated(ActiveRecord $Record, $data)
575
    {
576
    }
577
    protected static function onBeforeRecordValidated(ActiveRecord $Record, $data)
578
    {
579
    }
580
    protected static function onBeforeRecordSaved(ActiveRecord $Record, $data)
581
    {
582
    }
583
    protected static function onRecordDeleted(ActiveRecord $Record, $data)
584
    {
585
    }
586
    protected static function onRecordSaved(ActiveRecord $Record, $data)
587
    {
588
    }
589
    
590
    protected static function throwRecordNotFoundError($message = 'Record not found')
591
    {
592
        return static::throwNotFoundError($message);
0 ignored issues
show
Unused Code introduced by
The call to Divergence\Controllers\R...r::throwNotFoundError() has too many arguments starting with $message. ( Ignorable by Annotation )

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

592
        return static::/** @scrutinizer ignore-call */ throwNotFoundError($message);

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...
593
    }
594
}
595