Completed
Push — dev-master ( 7a680d...cdb56c )
by Vijay
33:12
created

APIMapper::delete()   B

Complexity

Conditions 6
Paths 4

Size

Total Lines 20
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
dl 0
loc 20
rs 8.8571
c 0
b 0
f 0
eloc 11
nc 4
nop 2
1
<?php
2
3
namespace FFCMS\Controllers\API;
4
5
use FFMVC\Helpers;
6
use FFCMS\{Traits, Models, Mappers};
7
8
9
/**
10
 * REST API Base Controller Class.
11
 *
12
 * @author Vijay Mahrra <[email protected]>
13
 * @copyright Vijay Mahrra
14
 * @license GPLv3 (http://www.gnu.org/licenses/gpl-3.0.html)
15
 */
16
abstract class APIMapper extends API
0 ignored issues
show
Coding Style introduced by
APIMapper does not seem to conform to the naming convention (^Abstract|Factory$).

This check examines a number of code elements and verifies that they conform to the given naming conventions.

You can set conventions for local variables, abstract classes, utility classes, constant, properties, methods, parameters, interfaces, classes, exceptions and special methods.

Loading history...
17
{
18
    use Traits\Validation;
19
20
    /**
21
     * For admin listing and search results
22
     */
23
    use Traits\ControllerMapper;
24
25
    /**
26
     * @var object database class
27
     */
28
    protected $db;
29
30
    /**
31
     * @var string table in the db
32
     */
33
    protected $table = null;
34
35
    /**
36
     * @var string class name
37
     */
38
    protected $mapperClass;
39
40
    /**
41
     * @var \FFMVC\Models\Mapper mapper for class
42
     */
43
    protected $mapper;
44
45
    /**
46
     * is the user authorised for access?
47
     *
48
     * @var bool
49
     */
50
    protected $isAuthorised = false;
51
52
    /**
53
     * is the user authorised for access to the object type?
54
     *
55
     * @var bool
56
     */
57
    protected $adminOnly = true;
58
59
60
    /**
61
     * initialize
62
     * @param \Base $f3
63
     */
64
    public function __construct(\Base $f3)
65
    {
66
        parent::__construct($f3);
67
68
        $f3 = \Base::instance();
69
70
        // guess the table name from the class name if not specified as a class member
71
        $class = strrchr(get_class($this), '\\');
72
        $class = \UTF::instance()->substr($class,1);
73
        if (empty($this->table)) {
74
            $table = $f3->snakecase($class);
75
        } else {
76
            $table = $this->table;
77
        }
78
        $this->table = $table;
79
        $mapperClass = "\FFCMS\Mappers\\" . $class;
80
81
        if (class_exists($mapperClass) && $this instanceof APIMapper) {
82
            $this->mapper = new $mapperClass;
83
            $this->mapperClass = $mapperClass;
84
        }
85
    }
86
87
    /**
88
     *
89
     * @param \Base $f3
90
     * @return void
91
     */
92
    public function init(\Base $f3)
93
    {
94
        $this->isAuthorised = $this->validateAccess();
95
        if (empty($this->isAuthorised)) {
96
            return $this->setOAuthError('invalid_grant');
97
        }
98
99
        $deny = false;
100
        $isAdmin = $f3->get('isAdmin');
101
        if (!$isAdmin && !empty($this->adminOnly)) {
102
            $deny = true;
103
        }
104
105
        $this->isAuthorised = empty($deny);
106
        if ($deny) {
107
            $this->failure('authentication_error', "User does not have permission.", 401);
108
            return $this->setOAuthError('access_denied');
109
        }
110
    }
111
112
113
    /**
114
     * Get the associated mapper for the table
115
     *
116
     * @return
117
     */
118
    public function &getMapper()
119
    {
120
        return $this->mapper;
121
    }
122
123
124
    /**
125
     * Check permissions and load the mapper with the object in the URL param @id
126
     * for the user
127
     *
128
     * @param \Base $f3
129
     * @param array $params
130
     * @param string $idField the field used for the unique id to load by
131
     * @param string $defaultId defaule value to use if not found
132
     * @return void
133
     */
134
    public function getIdObjectIfUser(\Base $f3, array $params, string $idField = 'uuid', $defaultId = null)
135
    {
136
        // valid user?
137
        if (empty($this->isAuthorised)) {
138
            return;
139
        }
140
141
        // only admin has permission to specify @id param
142
        $isAdmin = $f3->get('isAdmin');
143
        $id = !empty($params['id']) ? $params['id'] : $f3->get('REQUEST.id');
144
145
//        if ((!$isAdmin && !empty($id)) || (!$isAdmin && !empty($this->adminOnly))) {
146
        if ((!$isAdmin && !empty($this->adminOnly))) {
147
            $this->failure('authentication_error', "User does not have permission.", 401);
148
            return $this->setOAuthError('access_denied');
149
        }
150
151
        // use default user id
152
        if (empty($id)) {
153
            $id = $defaultId;
154
        }
155
156
        // load object by correct id
157
        $db = \Registry::get('db');
158
        $m = $this->getMapper();
159
        $m->load([$db->quotekey($idField) . ' = ?', $id]);
160
        if (null == $m->$idField) {
161
            $this->failure('authentication_error', "Object with @id does not exist.", 404);
162
            return $this->setOAuthError('invalid_request');
163
        }
164
165
        $this->mapper =& $m;
166
        return $m;
167
    }
168
169
170
    /**
171
     * Check permissions and load the mapper with the object in the URL param @id
172
     * if the user is an admin
173
     *
174
     * @param \Base $f3
175
     * @param param $params
176
     * @param string $idField the field used for the unique id to load by
177
     * @param string $defaultId defaule value to use if not found
178
     * @return type
179
     */
180
    public function getIdObjectIfAdmin(\Base $f3, array $params, string $idField = 'uuid', $defaultId = null)
181
    {
182
        if (empty($this->isAuthorised)) {
183
            return;
184
        }
185
186
        // only admin has permission to delete @id
187
        $isAdmin = $f3->get('isAdmin');
188
        if (!$isAdmin) {
189
            $this->failure('authentication_error', "User does not have permission.", 401);
190
            return $this->setOAuthError('access_denied');
191
        }
192
193
        // invalid id
194
        $id = !empty($params['id']) ? $params['id'] : $f3->get('REQUEST.id');
195
        if (empty($id)) {
196
            $id = $defaultId;
197
        }
198
199
        if (!empty($id) && ('uuid' == $idField && 36 !== strlen($id))) {
200
            $this->failure('authentication_error', "Invalid @id parameter.", 400);
201
            return $this->setOAuthError('invalid_request');
202
        }
203
204
        // check id exists
205
        $db = \Registry::get('db');
206
        $m = $this->getMapper();
207
        $m->load([$db->quotekey($idField) . ' = ?', $id]);
208
        if (null == $m->$idField) {
209
            $this->failure('authentication_error', "Object with @id does not exist.", 404);
210
            return $this->setOAuthError('invalid_request');
211
        }
212
213
        $this->mapper =& $m;
214
        return $m;
215
    }
216
217
218
    /**
219
     * Display the data item
220
     *
221
     * @param \Base $f3
222
     * @param array $params
223
     * @return void
224
     */
225
    public function get(\Base $f3, array $params)
226
    {
227
        $isAdmin = $f3->get('isAdmin');
228
        $m = $this->getIdObjectIfUser($f3, $params, 'uuid', $params['id']);
229
        if (!is_object($m) || null == $m->uuid) {
230
            return;
231
        } elseif (!$isAdmin && $m->users_uuid !== $f3->get('uuid')) {
232
            $this->failure('authentication_error', "User does not have permission.", 401);
233
            return $this->setOAuthError('access_denied');
234
        }
235
        // return raw data for object?
236
        $adminView = $f3->get('isAdmin') && 'admin' == $f3->get('REQUEST.view');
237
        $this->data = $adminView ? $m->castFields($f3->get('REQUEST.fields')) : $m->exportArray($f3->get('REQUEST.fields'));
238
    }
239
240
241
    /**
242
     * Delete the data object indicated by @id in the request
243
     *
244
     * @param \Base $f3
245
     * @param array $params
246
     * @return void
247
     */
248
    public function delete(\Base $f3, array $params)
249
    {
250
        $m = $this->getIdObjectIfUser($f3, $params);
251
        if (!is_object($m) || null == $m->uuid) {
252
            return;
253
        }
254
255
        // a user can only delete if they own the object
256
        $isAdmin = $f3->get('isAdmin');
257
        if (!$isAdmin) {
258
            if (!empty($m->users_uuid) && $m->users_uuid !== $f3->get('uuid')) {
259
                $this->failure('authentication_error', "User does not have permission.", 401);
260
                return $this->setOAuthError('access_denied');
261
            }
262
        }
263
264
        $this->data = [
265
            'deleted' => $m->erase()
266
        ];
267
    }
268
269
270
    /**
271
     * list objects (list is a reserved keyword)
272
     *
273
     * @param \Base $f3
274
     * @return void
275
     */
276
    public function listingAdmin(\Base $f3)
277
    {
278
        // must be an admin
279
        $isAdmin = $f3->get('isAdmin');
280
        if (!$isAdmin) {
281
            $this->failure('authentication_error', "User does not have permission.", 401);
282
            return $this->setOAuthError('access_denied');
283
        }
284
285
        $this->data = $this->getListingResults($f3, $this->getMapper());
286
    }
287
288
289
    /**
290
     * search objects
291
     *
292
     * @param \Base $f3
293
     * @return void
294
     */
295
    public function searchAdmin(\Base $f3)
296
    {
297
        // must be an admin
298
        $isAdmin = $f3->get('isAdmin');
299
        if (!$isAdmin) {
300
            $this->failure('authentication_error', "User does not have permission.", 401);
301
            return $this->setOAuthError('access_denied');
302
        }
303
304
        $this->data = $this->getSearchResults($f3, $this->getMapper());
305
    }
306
307
308
    /**
309
     * list objects (list is a reserved keyword)
310
     *
311
     * @param \Base $f3
312
     * @param array $params
313
     * @return void
314
     */
315
    public function listing(\Base $f3, array $params)
316
    {
317
        $isAdmin = $f3->get('isAdmin');
318
        $users_uuid = null;
319
        if (!$isAdmin && array_key_exists('id', $params)) {
320
            $this->failure('authentication_error', "User does not have permission.", 401);
321
            return $this->setOAuthError('access_denied');
322
        } elseif ($isAdmin && array_key_exists('id', $params)) {
323
            $users_uuid = $params['id'];
324
        } elseif (!$isAdmin) {
325
            $users_uuid = $f3->get('uuid');
326
        }
327
328
        // return raw data for object?
329
        $adminView = $f3->get('isAdmin') && 'admin' == $f3->get('REQUEST.view');
330
331
        // set up paging limits
332
        $minPerPage = $f3->get('api.paging_min');
333
        $maxPerPage = $f3->get('api.paging_max');
334
        $perPage = (int) $f3->get('REQUEST.per_page');
335
        if ($perPage < $minPerPage) {
336
            $perPage = $minPerPage;
337
        }
338
        if ($perPage > $maxPerPage) {
339
            $perPage = $maxPerPage;
340
        }
341
342
        $page = $f3->get('REQUEST.page');
343
        if ($page < 1) {
344
            $page = 1;
345
        }
346
347
        // fetch data (paging is 0 based)
348
        $m = $this->getMapper();
349
350
        // validate order field
351
        $order = $f3->get('REQUEST.order');
352
        $orderClauses = empty($order) ? [] : preg_split("/[,]/", $order);
353
        $allFields = $m->fields();
354
        foreach ($orderClauses as $k => $field) {
355
            // split into field, asc/desc
356
            $field = preg_split("/[\s]+/", trim($field));
357
            if (!in_array($field[0], $allFields)) {
358
                // invalid field
359
                unset($orderClauses[$k]);
360
                continue;
361
            } elseif (count($field) == 1) {
362
                $field[1] = 'asc';
363
            } elseif (count($field) == 2) {
364
                if (!in_array($field[1], ['asc', 'desc'])) {
365
                    $field[1] = 'asc';
366
                }
367
            }
368
            $orderClauses[$k] = $field[0] . ' ' . $field[1];
369
        }
370
        $order = join(',', $orderClauses);
371
372
        // fields to return - validate
373
        $fields = $f3->get('REQUEST.fields');
374
        $fields = empty($fields) ? [] : preg_split("/[,]/", $fields);
375
        foreach ($fields as $k => $field) {
376
            if (!in_array($field, $allFields)) {
377
                unset($fields[$k]);
378
            }
379
        }
380
        $fields = join(',', $fields);
381
382
        // count rows
383
        if ($isAdmin) {
384
            $rows = $m->count();
385
        } else {
386
            $rows = $m->count(['users_uuid = ?', $users_uuid]);
387
        }
388
        if ($rows < 1) {
389
            $this->failure('sever_error', "No data available for request.", 404);
390
            $this->setOAuthError('server_error');
391
            return;
392
        }
393
394
        // if fewer results than per page, set per_page
395
        if ($page == 1 && $perPage > $rows) {
396
            $perPage = $rows;
397
        }
398
399
        $pagination = [];
400
        $pagination['count'] = ceil($rows / $perPage);
401
402
        // too high page number?
403
        if ($page > $pagination['count']) {
404
            $page = $pagination['count'];
405
        }
406
407
        // set up page URLs
408
        $url = $f3->get('PATH');
409
        $urlParams = [
410
            'per_page' => $perPage,
411
        ];
412
        if (!empty($order)) {
413
            $urlParams['order'] = $order;
414
        }
415
        if (!empty($adminView)) {
416
            $urlParams['view'] = 'admin';
417
        }
418
        if (!empty($fields)) {
419
            $urlParams['fields'] = $fields;
420
        }
421
        ksort($urlParams);
422
423
        // previous page url
424
        $prevPage = (1 > $page - 1 ) ? null : $page - 1;
425
        $nextPage = (1 + $page> $pagination['count']) ? null : $page + 1;
426
427
        $resultsFrom = round($page * ($rows / $pagination['count'])) - $perPage + 1;
428
        $resultsTo = $resultsFrom + $perPage - 1;
429
430
        // return data
431
        $this->data['pagination'] = [
432
            'url_base' => $this->url($url, $urlParams),
433
            'url_current' => $this->url($url, $urlParams + ['page' => $page]),
434
            'url_first' => $this->url($url, $urlParams + ['page' => 1]),
435
            'url_last' => $this->url($url, $urlParams + ['page' => $pagination['count']]),
436
            'url_next' => (null == $nextPage) ? null : $this->url($url, $urlParams + ['page' => $nextPage]),
437
            'url_previous' => (null == $prevPage) ? null : $this->url($url, $urlParams + ['page' => $prevPage]),
438
            'results' => $rows,
439
            'results_from' => $resultsFrom,
440
            'results_to' => $resultsTo,
441
            'per_page' => $perPage,
442
            'pages' => $pagination['count'],
443
            'page' => $page,
444
            'object' => $m->table(),
445
            'fields' => preg_split("/[,]/", $fields)
446
        ];
447
448
        // fetch results
449
        if ($isAdmin && empty($users_uuid)) {
450
            $m->load('', [
451
                'order' => $order,
452
                'offset' => (1 == $page) ? 0 : ($page - 1) * $perPage,
453
                'limit' => $perPage
454
            ]);
455
        } else {
456
            $m->load(['users_uuid = ?', $users_uuid], [
457
                'order' => $order,
458
                'offset' => (1 == $page) ? 0 : ($page - 1) * $perPage,
459
                'limit' => $perPage
460
            ]);
461
        }
462
463
        do {
464
            $this->data['objects'][] = $adminView ? $m->castFields($fields) : $m->exportArray($fields);
465
        }
466
        while ($m->skip());
467
    }
468
469
470
    /**
471
     * search objects
472
     *
473
     * @param \Base $f3
474
     * @param array $params
475
     * @return void
476
     */
477
    public function search(\Base $f3, array $params)
478
    {
479
        $isAdmin = $f3->get('isAdmin');
480
        if (!$isAdmin && array_key_exists('id', $params)) {
481
            $this->failure('authentication_error', "User does not have permission.", 401);
482
            return $this->setOAuthError('access_denied');
483
        } elseif ($isAdmin && array_key_exists('id', $params)) {
484
            $users_uuid = $params['id'];
485
        } elseif (!$isAdmin) {
486
            $users_uuid = $f3->get('uuid');
487
        }
488
489
        // return raw data for object?
490
        $adminView = $f3->get('isAdmin') && 'admin' == $f3->get('REQUEST.view');
491
492
        // set up paging limits
493
        $minPerPage = $f3->get('api.paging_min');
494
        $maxPerPage = $f3->get('api.paging_max');
495
        $perPage = (int) $f3->get('REQUEST.per_page');
496
        if ($perPage < $minPerPage) {
497
            $perPage = $minPerPage;
498
        }
499
        if ($perPage > $maxPerPage) {
500
            $perPage = $maxPerPage;
501
        }
502
503
        $page = $f3->get('REQUEST.page');
504
        if ($page < 1) {
505
            $page = 1;
506
        }
507
508
        // fetch data (paging is 0 based)
509
        $m = $this->getMapper();
510
        $allFields = $m->fields();
511
512
        // validate order field
513
        $order = $f3->get('REQUEST.order');
514
        if (!empty($order)) {
515
            $orderClauses = empty($order) ? [] : preg_split("/[,]/", $order);
516
            foreach ($orderClauses as $k => $field) {
517
                // split into field, asc/desc
518
                $field = preg_split("/[\s]+/", trim($field));
519
                if (!in_array($field[0], $allFields)) {
520
                    // invalid field
521
                    unset($orderClauses[$k]);
522
                    continue;
523
                } elseif (count($field) == 1) {
524
                    $field[1] = 'asc';
525
                } elseif (count($field) == 2) {
526
                    if (!in_array($field[1], ['asc', 'desc'])) {
527
                        $field[1] = 'asc';
528
                    }
529
                }
530
                $orderClauses[$k] = $field[0] . ' ' . $field[1];
531
            }
532
            $order = join(',', $orderClauses);
533
        }
534
535
        // fields to return and fields to search - validate
536
        $validFields = [];
537
        foreach (['fields', 'search_fields'] as $fieldsList) {
538
            $fields = $f3->get('REQUEST.' . $fieldsList);
539
            if (empty($fields)) {
540
                continue;
541
            }
542
            $fields = empty($fields) ? [] : preg_split("/[,]/", $fields);
543
            foreach ($fields as $k => $field) {
544
                if (!in_array($field, $allFields)) {
545
                    unset($fields[$k]);
546
                }
547
            }
548
            $validFields[$fieldsList] = join(',', $fields);
549
        }
550
551
        // validated fields to return
552
        $fields = empty($validFields['fields']) ? join(',', $allFields) : $validFields['fields'];
553
554
        // validated fields to search in, use all if empty
555
        $searchFields = empty($fields) ? join(',', $allFields) : $validFields['searchFields'];
556
557
        // get search type
558
        $search = $f3->get('REQUEST.search');
559
        if (!empty($search)) {
560
            $search = trim(strtolower($search));
561
        }
562
        $search_type = $f3->get('REQUEST.search_type');
563
        if (empty($search_type)) {
564
            $search_type = 'exact';
565
        } elseif ($search_type !== 'exact') {
566
            $search_type = 'fuzzy';
567
        }
568
569
        // construct search query
570
        $db = \Registry::get('db');
571
        $sqlClauses = [];
572
        $searchFieldsArray = preg_split("/[,]/", $searchFields);
573
        foreach ($searchFieldsArray as $field) {
574
            $sqlClauses[] = 'LOWER(' . $db->quotekey($field) . ') = ' . $db->quote($search);
575
            if ($search_type == 'fuzzy') {
576
                $sqlClauses[] = 'LOWER(' . $db->quotekey($field) . ') LIKE ' . $db->quote('%' . $search . '%');
577
            }
578
        }
579
580
        // get total results
581
        $query = 'SELECT COUNT(*) AS results FROM ' . $db->quotekey($m->table()) . ' WHERE ';
582
        if (empty($users_uuid)) {
583
             $query .= join(' OR ', $sqlClauses);
584
        } else {
585
             $query .= ' users_uuid = ' . $db->quote($users_uuid)  . ' AND ('.  join(' OR ', $sqlClauses) . ')';
586
        }
587
        $rows = $db->exec($query);
588
        $rows = (int) $rows[0]['results'];
589
        if ($rows < 1) {
590
            $this->failure('sever_error', "No data available for request.", 404);
591
            $this->setOAuthError('server_error');
592
            return;
593
        }
594
595
        // if fewer results than per page, set per_page
596
        if ($page == 1 && $perPage > $rows) {
597
            $perPage = $rows;
598
        }
599
600
        $pagination['count'] = ceil($rows / $perPage);
0 ignored issues
show
Coding Style Comprehensibility introduced by
$pagination was never initialized. Although not strictly required by PHP, it is generally a good practice to add $pagination = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
601
602
        // too high page number?
603
        if ($page > $pagination['count']) {
604
            $page = $pagination['count'];
605
        }
606
607
        // set up page URLs
608
        $url = $f3->get('PATH');
609
        $urlParams = [
610
            'per_page' => $perPage,
611
            'search' => $search,
612
            'search_type' => $search_type,
613
        ];
614
        if (!empty($order)) {
615
            $urlParams['order'] = $order;
616
        }
617
        if (!empty($adminView)) {
618
            $urlParams['view'] = 'admin';
619
        }
620
        if (!empty($fields)) {
621
            $urlParams['fields'] = $fields;
622
        }
623
        ksort($urlParams);
624
625
        // previous page url
626
        $prevPage = (1 > $page - 1 ) ? null : $page - 1;
627
        $nextPage = (1 + $page> $pagination['count']) ? null : $page + 1;
628
629
        $resultsFrom = 1 + ($page * $perPage) - $perPage;
630
        $resultsTo = $resultsFrom + $perPage - 1;
631
        if ($resultsTo > $rows) {
632
            $resultsTo = $rows;
633
        }
634
635
        // return data
636
        $this->data['pagination'] = [
637
            'url_base' => $this->url($url, $urlParams),
638
            'url_current' => $this->url($url, $urlParams + ['page' => $page]),
639
            'url_first' => $this->url($url, $urlParams + ['page' => 1]),
640
            'url_last' => $this->url($url, $urlParams + ['page' => $pagination['count']]),
641
            'url_next' => (null == $nextPage) ? null : $this->url($url, $urlParams + ['page' => $nextPage]),
642
            'url_previous' => (null == $prevPage) ? null : $this->url($url, $urlParams + ['page' => $prevPage]),
643
            'results' => $rows,
644
            'results_from' => $resultsFrom,
645
            'results_to' => $resultsTo,
646
            'per_page' => $perPage,
647
            'pages' => $pagination['count'],
648
            'page' => $page,
649
            'object' => $m->table(),
650
            'fields' => preg_split("/[,]/", $fields)
651
        ];
652
653
        // retrieve results
654
        $query = 'SELECT * FROM ' . $db->quotekey($m->table()) . ' WHERE ';
655
        if (empty($users_uuid)) {
656
             $query .= join(' OR ', $sqlClauses);
657
        } else {
658
             $query .= ' users_uuid = ' . $db->quote($users_uuid)  . ' AND ('.  join(' OR ', $sqlClauses) . ')';
659
        }
660
        $query .= sprintf(' LIMIT %d,%d', (1 == $page) ? 0 : ($page - 1) * $perPage, $perPage);
661
        $results = $db->exec($query);
662
        foreach ($results as $row) {
663
            $this->data['objects'][] = $adminView ? $m->castFields($fields, $row) : $m->exportArray($fields, $row);
664
        }
665
    }
666
667
}
668