Completed
Push — dev-master ( cdb56c...dc1664 )
by Vijay
12:32
created

APIMapper::__construct()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

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