Passed
Push — master ( a97140...2505f7 )
by Henry
02:02
created

RecordsRequestHandler::throwUnauthorizedError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 0
dl 0
loc 6
rs 9.4285
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
                    return static::handleRecordRequest($Record);
79
                } else {
80
                    return static::throwRecordNotFoundError();
81
                }
82
            }
83
        }
84
    }
85
    
86
    public static function getRecordByHandle($handle)
87
    {
88
        $className = static::$recordClass;
89
        
90
        if (method_exists($className, 'getByHandle')) {
91
            return $className::getByHandle($handle);
92
        }
93
    }
94
95
    public static function prepareBrowseConditions($conditions = [])
96
    {
97
        if (static::$browseConditions) {
98
            if (!is_array(static::$browseConditions)) {
99
                static::$browseConditions = [static::$browseConditions];
100
            }
101
            $conditions = array_merge(static::$browseConditions, $conditions);
102
        }
103
        return $conditions;
104
    }
105
106
    public static function prepareDefaultBrowseOptions()
107
    {
108
        if (empty($_REQUEST['offset']) && is_numeric($_REQUEST['start'])) {
109
            $_REQUEST['offset'] = $_REQUEST['start'];
110
        }
111
112
        $limit = !empty($_REQUEST['limit']) && is_numeric($_REQUEST['limit']) ? $_REQUEST['limit'] : static::$browseLimitDefault;
113
        $offset = !empty($_REQUEST['offset']) && is_numeric($_REQUEST['offset']) ? $_REQUEST['offset'] : false;
114
        
115
        $options = [
116
            'limit' =>  $limit,
117
            'offset' => $offset,
118
            'order' => static::$browseOrder,
119
        ];
120
121
        return $options;
122
    }
123
124
    public static function handleBrowseRequest($options = [], $conditions = [], $responseID = null, $responseData = [])
125
    {
126
        if (!static::checkBrowseAccess(func_get_args())) {
127
            return static::throwUnauthorizedError();
128
        }
129
        
130
        $conditions = static::prepareBrowseConditions($conditions);
131
        
132
        $options = static::prepareDefaultBrowseOptions();
133
134
        // process sorter
135
        if (!empty($_REQUEST['sort'])) {
136
            $sort = json_decode($_REQUEST['sort'], true);
137
            if (!$sort || !is_array($sort)) {
138
                return static::respond('error', [
139
                    'success' => false,
140
                    'failed' => [
141
                        'errors'	=>	'Invalid sorter.',
142
                    ],
143
                ]);
144
            }
145
146
            if (is_array($sort)) {
147
                foreach ($sort as $field) {
148
                    $options['order'][$field['property']] = $field['direction'];
149
                }
150
            }
151
        }
152
        
153
        // process filter
154
        if (!empty($_REQUEST['filter'])) {
155
            $filter = json_decode($_REQUEST['filter'], true);
156
            if (!$filter || !is_array($filter)) {
157
                return static::respond('error', [
158
                    'success' => false,
159
                    'failed' => [
160
                        'errors'	=>	'Invalid filter.',
161
                    ],
162
                ]);
163
            }
164
165
            foreach ($filter as $field) {
166
                $conditions[$field['property']] = $field['value'];
167
            }
168
        }
169
170
        $className = static::$recordClass;
171
172
        return static::respond(
173
            isset($responseID) ? $responseID : static::getTemplateName($className::$pluralNoun),
174
            array_merge($responseData, [
175
                'success' => true,
176
                'data' => $className::getAllByWhere($conditions, $options),
177
                'conditions' => $conditions,
178
                'total' => DB::foundRows(),
179
                'limit' => $options['limit'],
180
                'offset' => $options['offset'],
181
            ])
182
        );
183
    }
184
185
186
    public static function handleRecordRequest(ActiveRecord $Record, $action = false)
187
    {
188
        if (!static::checkReadAccess($Record)) {
189
            return static::throwUnauthorizedError();
190
        }
191
192
        switch ($action ? $action : $action = static::shiftPath()) {
193
            case '':
194
            case false:
195
            {
196
                $className = static::$recordClass;
197
                
198
                return static::respond(static::getTemplateName($className::$singularNoun), [
199
                    'success' => true,
200
                    'data' => $Record,
201
                ]);
202
            }
203
            
204
            case 'edit':
205
            {
206
                return static::handleEditRequest($Record);
207
            }
208
            
209
            case 'delete':
210
            {
211
                return static::handleDeleteRequest($Record);
212
            }
213
        
214
            default:
215
            {
216
                return static::onRecordRequestNotHandled($Record, $action);
217
            }
218
        }
219
    }
220
221
222
    public static function prepareResponseModeJSON($methods = [])
223
    {
224
        if (static::$responseMode == 'json' && in_array($_SERVER['REQUEST_METHOD'], $methods)) {
225
            $JSONData = JSON::getRequestData();
226
            if (is_array($JSONData)) {
227
                $_REQUEST = $JSONData;
228
            }
229
        }
230
    }
231
232
    public static function getDatumRecord($datum)
233
    {
234
        $className = static::$recordClass;
235
        $PrimaryKey = $className::getPrimaryKey();
236
        if (empty($datum[$PrimaryKey])) {
237
            $record = new $className::$defaultClass();
238
            static::onRecordCreated($record, $datum);
239
        } else {
240
            if (!$record = $className::getByID($datum[$PrimaryKey])) {
241
                throw new Exception('Record not found');
242
            }
243
        }
244
        return $record;
245
    }
246
247
    public static function processDatumSave($datum)
248
    {
249
        // get record
250
        $Record = static::getDatumRecord($datum);
251
        
252
        // check write access
253
        if (!static::checkWriteAccess($Record)) {
254
            throw new Exception('Write access denied');
255
        }
256
        
257
        // apply delta
258
        static::applyRecordDelta($Record, $datum);
259
260
        // call template function
261
        static::onBeforeRecordValidated($Record, $datum);
262
263
        // try to save record
264
        try {
265
            // call template function
266
            static::onBeforeRecordSaved($Record, $datum);
267
268
            $Record->save();
269
            return (!$Record::fieldExists('Class') || get_class($Record) == $Record->Class) ? $Record : $Record->changeClass();
270
            
271
            // call template function
272
            static::onRecordSaved($Record, $datum);
0 ignored issues
show
Unused Code introduced by
static::onRecordSaved($Record, $datum) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
273
        } catch (Exception $e) {
274
            throw new Exception($Record->validationErrors);
275
        }
276
    }
277
278
    public static function handleMultiSaveRequest()
279
    {
280
        $className = static::$recordClass;
281
            
282
        static::prepareResponseModeJSON(['POST','PUT']);
283
        
284
        if ($className::fieldExists(key($_REQUEST['data']))) {
285
            $_REQUEST['data'] = [$_REQUEST['data']];
286
        }
287
288
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
289
            return static::respond('error', [
290
                'success' => false,
291
                'failed' => [
292
                    'errors'	=>	'Save expects "data" field as array of records.',
293
                ],
294
            ]);
295
        }
296
        
297
        $results = [];
298
        $failed = [];
299
300
        foreach ($_REQUEST['data'] as $datum) {
301
            try {
302
                $results[] = static::processDatumSave($datum);
303
            } catch (Exception $e) {
304
                $failed[] = [
305
                    'record' => $datum,
306
                    'errors' => $e->getMessage(),
307
                ];
308
                continue;
309
            }
310
        }
311
        
312
        
313
        return static::respond(static::getTemplateName($className::$pluralNoun).'Saved', [
314
            'success' => count($results) || !count($failed),
315
            'data' => $results,
316
            'failed' => $failed,
317
        ]);
318
    }
319
    
320
    public static function processDatumDestroy($datum)
321
    {
322
        $className = static::$recordClass;
323
        $PrimaryKey = $className::getPrimaryKey();
324
325
        // get record
326
        if (is_numeric($datum)) {
327
            $recordID = $datum;
328
        } elseif (!empty($datum[$PrimaryKey]) && is_numeric($datum[$PrimaryKey])) {
329
            $recordID = $datum[$PrimaryKey];
330
        } else {
331
            throw new Exception($PrimaryKey.' missing');
332
        }
333
334
        if (!$Record = $className::getByField($PrimaryKey, $recordID)) {
335
            throw new Exception($PrimaryKey.' not found');
336
        }
337
        
338
        // check write access
339
        if (!static::checkWriteAccess($Record)) {
340
            throw new Exception('Write access denied');
341
        }
342
343
        if ($Record->destroy()) {
344
            return $Record;
345
        } else {
346
            throw new Exception('Destroy failed');
347
        }
348
    }
349
350
    public static function handleMultiDestroyRequest()
351
    {
352
        $className = static::$recordClass;
353
354
        $PrimaryKey = $className::getPrimaryKey();
0 ignored issues
show
Unused Code introduced by
The assignment to $PrimaryKey is dead and can be removed.
Loading history...
355
356
        static::prepareResponseModeJSON(['POST','PUT','DELETE']);
357
        
358
        if ($className::fieldExists(key($_REQUEST['data']))) {
359
            $_REQUEST['data'] = [$_REQUEST['data']];
360
        }
361
362
        if (empty($_REQUEST['data']) || !is_array($_REQUEST['data'])) {
363
            return static::respond('error', [
364
                'success' => false,
365
                'failed' => [
366
                    'errors'	=>	'Save expects "data" field as array of records.',
367
                ],
368
            ]);
369
        }
370
371
        $results = [];
372
        $failed = [];
373
        
374
        foreach ($_REQUEST['data'] as $datum) {
375
            try {
376
                $results[] = static::processDatumDestroy($datum);
377
            }    
378
            catch(Exception $e) {
379
                $failed[] = [
380
                    'record' => $datum,
381
                    'errors' => $e->getMessage(),
382
                ];
383
                continue;
384
            }
385
        }
386
        
387
        return static::respond(static::getTemplateName($className::$pluralNoun).'Destroyed', [
388
            'success' => count($results) || !count($failed),
389
            'data' => $results,
390
            'failed' => $failed,
391
        ]);
392
    }
393
394
395
    public static function handleCreateRequest(ActiveRecord $Record = null)
396
    {
397
        // save static class
398
        static::$calledClass = get_called_class();
399
400
        if (!$Record) {
401
            $className = static::$recordClass;
402
            $Record = new $className::$defaultClass();
403
        }
404
        
405
        // call template function
406
        static::onRecordCreated($Record, $_REQUEST);
407
408
        return static::handleEditRequest($Record);
409
    }
410
411
    public static function handleEditRequest(ActiveRecord $Record)
412
    {
413
        $className = static::$recordClass;
414
415
        if (!static::checkWriteAccess($Record)) {
416
            return static::throwUnauthorizedError();
417
        }
418
419
        if (in_array($_SERVER['REQUEST_METHOD'], ['POST','PUT'])) {
420
            if (static::$responseMode == 'json') {
421
                $_REQUEST = JSON::getRequestData();
422
                if (is_array($_REQUEST['data'])) {
423
                    $_REQUEST = $_REQUEST['data'];
424
                }
425
            }
426
            $_REQUEST = $_REQUEST ? $_REQUEST : $_POST;
427
        
428
            // apply delta
429
            static::applyRecordDelta($Record, $_REQUEST);
430
            
431
            // call template function
432
            static::onBeforeRecordValidated($Record, $_REQUEST);
433
434
            // validate
435
            if ($Record->validate()) {
436
                // call template function
437
                static::onBeforeRecordSaved($Record, $_REQUEST);
438
                
439
                try {
440
                    // save session
441
                    $Record->save();
442
                } catch (Exception $e) {
443
                    return static::respond('Error', [
444
                        'success' => false,
445
                        'failed' => [
446
                            'errors'	=>	$e->getMessage(),
447
                        ],
448
                    ]);
449
                }
450
                
451
                // call template function
452
                static::onRecordSaved($Record, $_REQUEST);
453
        
454
                // fire created response
455
                $responseID = static::getTemplateName($className::$singularNoun).'Saved';
456
                $responseData = [
457
                    'success' => true,
458
                    'data' => $Record,
459
                ];
460
                return static::respond($responseID, $responseData);
461
            }
462
            
463
            // fall through back to form if validation failed
464
        }
465
        
466
        $responseID = static::getTemplateName($className::$singularNoun).'Edit';
467
        $responseData = [
468
            'success' => false,
469
            'data' => $Record,
470
        ];
471
    
472
        return static::respond($responseID, $responseData);
473
    }
474
475
476
    public static function handleDeleteRequest(ActiveRecord $Record)
477
    {
478
        $className = static::$recordClass;
479
480
        if (!static::checkWriteAccess($Record)) {
481
            return static::throwUnauthorizedError();
482
        }
483
    
484
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
485
            $data = $Record->data;
486
            $Record->destroy();
487
                    
488
            // call cleanup function after delete
489
            static::onRecordDeleted($Record, $data);
490
            
491
            // fire created response
492
            return static::respond(static::getTemplateName($className::$singularNoun).'Deleted', [
493
                'success' => true,
494
                'data' => $Record,
495
            ]);
496
        }
497
    
498
        return static::respond('confirm', [
499
            'question' => 'Are you sure you want to delete this '.$className::$singularNoun.'?',
500
            'data' => $Record,
501
        ]);
502
    }
503
    
504
    
505
    public static function respond($responseID, $responseData = [], $responseMode = false)
506
    {
507
        // default to static property
508
        if (!$responseMode) {
509
            $responseMode = static::$responseMode;
510
        }
511
    
512
        return parent::respond($responseID, $responseData, $responseMode);
513
    }
514
    
515
    // access control template functions
516
    public static function checkBrowseAccess($arguments)
517
    {
518
        return true;
519
    }
520
521
    public static function checkReadAccess(ActiveRecord $Record)
522
    {
523
        return true;
524
    }
525
    
526
    public static function checkWriteAccess(ActiveRecord $Record)
527
    {
528
        return true;
529
    }
530
    
531
    public static function checkAPIAccess()
532
    {
533
        return true;
534
    }
535
    
536
    public static function throwUnauthorizedError()
537
    {
538
        return static::respond('Unauthorized', [
539
            'success' => false,
540
            'failed' => [
541
                'errors'	=>	'Login required.',
542
            ],
543
        ]);
544
    }
545
546
    public static function throwAPIUnAuthorizedError()
547
    {
548
        return static::respond('Unauthorized', [
549
            'success' => false,
550
            'failed' => [
551
                'errors'	=>	'API access required.',
552
            ],
553
        ]);
554
    }
555
556
    public static function throwNotFoundError()
557
    {
558
        return static::respond('error', [
559
            'success' => false,
560
            'failed' => [
561
                'errors'	=>	'Record not found.',
562
            ],
563
        ]);
564
    }
565
    
566
    public static function onRecordRequestNotHandled(ActiveRecord $Record, $action)
567
    {
568
        return static::respond('error', [
569
            'success' => false,
570
            'failed' => [
571
                'errors'	=>	'Malformed request.',
572
            ],
573
        ]);
574
    }
575
    
576
577
578
    public static function getTemplateName($noun)
579
    {
580
        return preg_replace_callback('/\s+([a-zA-Z])/', function ($matches) {
581
            return strtoupper($matches[1]);
582
        }, $noun);
583
    }
584
    
585
    public static function applyRecordDelta(ActiveRecord $Record, $data)
586
    {
587
        if (is_array(static::$editableFields)) {
588
            $Record->setFields(array_intersect_key($data, array_flip(static::$editableFields)));
589
        } else {
590
            $Record->setFields($data);
591
        }
592
    }
593
    
594
    // event template functions
595
    protected static function onRecordCreated(ActiveRecord $Record, $data)
596
    {
597
    }
598
    protected static function onBeforeRecordValidated(ActiveRecord $Record, $data)
599
    {
600
    }
601
    protected static function onBeforeRecordSaved(ActiveRecord $Record, $data)
602
    {
603
    }
604
    protected static function onRecordDeleted(ActiveRecord $Record, $data)
605
    {
606
    }
607
    protected static function onRecordSaved(ActiveRecord $Record, $data)
608
    {
609
    }
610
    
611
    protected static function throwRecordNotFoundError()
612
    {
613
        return static::throwNotFoundError();
614
    }
615
}
616