Api::getObjectIdPartIdByType()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 2.0625

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 7
ccs 3
cts 4
cp 0.75
rs 9.4285
cc 2
eloc 4
nc 2
nop 1
crap 2.0625
1
<?php
2
3
namespace Narcoticfresh\Shotwell;
4
5
/**
6
 * Simple API for Shotwell sqlite databases
7
 *
8
 * @package Narcoticfresh\Shotwell
9
 * @author  Dario Nuevo
10
 * @license https://opensource.org/licenses/gpl-3.0.html GPL-3.0
11
 * @link    https://github.com/narcoticfresh/shotwell
12
 */
13
class Api
14
{
15
16
    /**
17
     * Constant for photo type
18
     */
19
    const TYPE_PHOTO = "PHOTO";
20
21
    /**
22
     * Constant for video type
23
     */
24
    const TYPE_VIDEO = "VIDEO";
25
26
    /**
27
     * Type map, mapping the ObjectID prefixes to our constants
28
     *
29
     * @var array
30
     */
31
    private $typeMap = array(
32
        self::TYPE_PHOTO => 'thumb',
33
        self::TYPE_VIDEO => 'video-'
34
    );
35
36
    /**
37
     * database
38
     *
39
     * @var \PDO
40
     */
41
    private $db;
42
43
    /**
44
     * Local cache array
45
     *
46
     * @var array
47
     */
48
    private $tagMap = null;
49
50
    /**
51
     * Api constructor.
52
     *
53
     * @param string $dbPath Path to sqlite file
54
     *
55
     * @throws \Exception
56
     */
57 11
    public function __construct($dbPath)
58
    {
59 11
        if (!file_exists($dbPath) || !is_readable($dbPath)) {
60 1
            throw new \Exception("Database with path '" . $dbPath ."' doesn't exist or isn't readable!");
61
        }
62
63 11
        $this->db = new \PDO('sqlite:' . $dbPath);
64 11
    }
65
66
    /**
67
     * Returns an array of everything in the database, videos and photos mixed
68
     *
69
     * @return array array of simple array structures containing the data from the database
70
     */
71 1
    public function getAll()
72
    {
73 1
        return $this->searchAllWithCondition();
74
    }
75
76
    /**
77
     * Returns an array of all photos in the database
78
     *
79
     * @return array array of simple array structures containing the data from the database
80
     */
81 1
    public function getAllPhotos()
82
    {
83 1
        return $this->getItem(self::TYPE_PHOTO);
84
    }
85
86
    /**
87
     * Returns an array of all videos in the database
88
     *
89
     * @return array array of simple array structures containing the data from the database
90
     */
91 1
    public function getAllVideos()
92
    {
93 1
        return $this->getItem(self::TYPE_VIDEO);
94
    }
95
96
    /**
97
     * Returns a given photo by it's ID in PhotoTable
98
     *
99
     * @param string $id ID
100
     *
101
     * @return array
102
     */
103 5
    public function getPhotoById($id)
104
    {
105 5
        return $this->getItem(self::TYPE_PHOTO, $id);
106
    }
107
108
    /**
109
     * Returns a given video by it's ID in VideoTable
110
     *
111
     * @param string $id ID
112
     *
113
     * @return array
114
     */
115 6
    public function getVideoById($id)
116
    {
117 6
        return $this->getItem(self::TYPE_VIDEO, $id);
118
    }
119
120
    /**
121
     * Returns all tags associated with a given Object ID. Make sure to get an Object ID first by
122
     * calling getObjectIdByNumericId().
123
     *
124
     * @param string $objectId Object ID.
125
     *
126
     * @return bool Either an array of tags or false if the object doesn't exist
127
     */
128 7
    public function getTagsByObjectId($objectId)
129
    {
130 7
        if (is_null($this->tagMap)) {
131 7
            $this->tagMap = $this->getItemTagMap();
132
        }
133
134 7
        if (isset($this->tagMap[$objectId])) {
135 7
            $ret = $this->tagMap[$objectId];
136
        } else {
137 2
            return false;
138
        }
139
140 7
        return $ret;
141
    }
142
143
    /**
144
     * Sets tags on a given item using an Oject ID. Make sure to get an Object ID first by
145
     * calling getObjectIdByNumericId(). This sets the tags to the ones you pass.
146
     *
147
     * @param string $objectId Object ID
148
     * @param array  $tags     An array of strings containing your tags
149
     *
150
     * @return bool true if all is good, false otherwise
151
     */
152 3
    public function setItemTags($objectId, array $tags)
153
    {
154 3
        $currentTags = $this->getTagsByObjectId($objectId);
155
156 3
        if ($currentTags === false) {
157 1
            return false;
158
        }
159
160
        // get diff - those need to be added
161 3
        $tagDiffAdd = array_diff($tags, $currentTags);
162
163
        // get diff otherway - those need to be removed..
164 3
        $tagDiffRemove = array_diff($currentTags, $tags);
165
166 3
        foreach ($tagDiffAdd as $tagName) {
167 2
            $this->manipulateItemOnTag($tagName, 'add', $objectId);
168
        }
169
170 3
        foreach ($tagDiffRemove as $tagName) {
171 3
            $this->manipulateItemOnTag($tagName, 'remove', $objectId);
172
        }
173
174 3
        $this->tagMap = null;
0 ignored issues
show
Documentation Bug introduced by
It seems like null of type null is incompatible with the declared type array of property $tagMap.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
175
176 3
        return true;
177
    }
178
179
    /**
180
     * This is a private function for doing common stuff on the tag. So not from item perspective.
181
     *
182
     * @param string $tagName  the tag
183
     * @param string $whatToDo What you want to do (either 'add' or 'remove')
184
     * @param string $objectId Object ID
185
     *
186
     * @throws Exception
187
     *
188
     * @return boolean true if all seems ok, false otherwise
189
     */
190 3
    private function manipulateItemOnTag($tagName, $whatToDo, $objectId)
191
    {
192 3
        $thisTagData = $this->getTag($tagName, true);
193 3
        $ret = false;
194
195 3
        if (is_array($thisTagData)) {
196 3
            $thisTagItems = explode(',', $thisTagData['photo_id_list']);
197 3
            $setKey = array_search($objectId, $thisTagItems);
198
199
            // what to do?
200 3
            if ($whatToDo == 'add') {
201 2
                if ($setKey === false) {
202 2
                    $thisTagItems[] = $objectId;
203
                }
204 3
            } elseif ($whatToDo == 'remove') {
205 3
                if ($setKey !== false) {
206 3
                    unset($thisTagItems[$setKey]);
207
                }
208
            }
209
210 3
            $ret = $this->setTagItems($tagName, $thisTagItems);
211
        }
212
213 3
        return $ret;
214
    }
215
216
    /**
217
     * Sets items on a tag
218
     *
219
     * @param string $tag   the tag
220
     * @param array  $items items
221
     *
222
     * @return bool true if all ok, false otherwise
223
     */
224 3
    private function setTagItems($tag, array $items)
225
    {
226 3
        $thisTagId = $this->getTagId($tag);
227 3
        $ret = false;
228
229 3
        if ($thisTagId !== false) {
230 3
            $q = "UPDATE TagTable SET `photo_id_list` = ? WHERE id = ?";
231
232 3
            $res = $this->db->prepare($q);
233
234 3
            $saveString = implode(',', $items).',';
235
236
            // ensure we don't save ",," somewhere..
237 3
            $saveString = str_replace(',,', ',', $saveString);
238
239 3
            $res->execute([
240 3
                $saveString,
241 3
                $thisTagId
242
            ]);
243
244 3
            $ret = true;
245
        }
246
247 3
        return $ret;
248
    }
249
250
    /**
251
     * Sets the rating on a tag using the Object ID. Make sure to get an Object ID first by
252
     * calling getObjectIdByNumericId().
253
     *
254
     * @param string $objectId Object ID
255
     * @param int    $rating   Rating (1-5)
256
     *
257
     * @return bool true if all ok, false otherwise
258
     * @throws \Exception
259
     */
260 3
    public function setItemRating($objectId, $rating)
261
    {
262 3
        if (!is_numeric($rating)) {
263 1
            throw new \Exception(sprintf("Rating must be numeric, '%s' given", $rating));
264
        }
265
266 2
        $thisTable = null;
267 2
        if ($this->getTypeByObjectId($objectId) == self::TYPE_PHOTO) {
268 1
            $thisTable = 'PhotoTable';
269
        }
270 2
        if ($this->getTypeByObjectId($objectId) == self::TYPE_VIDEO) {
271 2
            $thisTable = 'VideoTable';
272
        }
273
274 2
        if (is_null($thisTable)) {
275 1
            return false;
276
        }
277
278 2
        $item = $this->getItemByObjectId($objectId);
279
280 2
        if (!isset($item['id'])) {
281 1
            return false;
282
        }
283
284 1
        $q = "UPDATE `" . $thisTable . "` SET rating=? WHERE id=?";
285
286 1
        $res = $this->db->prepare($q);
287 1
        $ret = $res->execute(array(
288 1
            $rating,
289 1
            $item['id']
290
        ));
291
292 1
        return $ret;
293
    }
294
295
    /**
296
     * Removes all tags from a given item using Object ID. Make sure to get an Object ID first by
297
     * calling getObjectIdByNumericId().
298
     *
299
     * @param string $objectId Object ID
300
     *
301
     * @return void
302
     */
303 1
    public function removeAllItemTags($objectId)
304
    {
305 1
        return $this->setItemTags($objectId, []);
306
    }
307
308
    /**
309
     * Gets an item
310
     *
311
     * @param string      $itemType What type of item
312
     * @param null|string $objectId The object ID
313
     *
314
     * @return array the item
315
     */
316 7
    private function getItem($itemType, $objectId = null)
317
    {
318 7
        if ($itemType == self::TYPE_PHOTO) {
319 5
            $tableName = 'PhotoTable';
320 6
        } elseif ($itemType == self::TYPE_VIDEO) {
321 6
            $tableName = 'VideoTable';
322
        }
323
324 7
        if (!is_null($objectId)) {
325 7
            $query = 'SELECT * FROM `' . $tableName . '` WHERE id=?';
0 ignored issues
show
Bug introduced by
The variable $tableName does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
326 7
            $params = [$objectId];
327
        } else {
328 1
            $query = 'SELECT * FROM ' . $tableName;
329 1
            $params = [];
330
        }
331
332 7
        $res = $this->db->prepare($query);
333 7
        $res->execute($params);
334
335 7
        $ret = array();
336
337 7
        while (($data = $res->fetch(\PDO::FETCH_ASSOC))) {
338 6
            if (is_array($data) && isset($data['id'])) {
339 6
                $data['object_id'] = $this->getObjectIdByNumericId($itemType, $data['id']);
340 6
                $data['tags'] = $this->getTagsByObjectId($data['object_id']);
341 6
                if ($data['tags'] === false) {
342 1
                    $data['tags'] = [];
343
                }
344 6
                $data['type'] = $itemType;
345 6
                $ret[] = $data;
346
            }
347
        }
348
349 7
        if (!is_null($objectId) && isset($ret[0])) {
350 6
            return $ret[0];
351
        }
352
353 2
        return $ret;
354
    }
355
356
    /**
357
     * Gets an item by Object ID
358
     *
359
     * @param string $objectId Object ID
360
     *
361
     * @return array|bool Either the item data or false
362
     */
363 5
    public function getItemByObjectId($objectId)
364
    {
365 5
        $ret = false;
366 5
        $type = $this->getTypeByObjectId($objectId);
367
368 5
        if ($type !== false) {
369 5
            $numId = $this->getNumericIdByObjectId($objectId);
370 5
            if ($type == self::TYPE_PHOTO) {
371 3
                $ret = $this->getPhotoById($numId);
0 ignored issues
show
Documentation introduced by
$numId is of type integer|double|false, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
372 4
            } elseif ($type == self::TYPE_VIDEO) {
373 4
                $ret = $this->getVideoById($numId);
0 ignored issues
show
Documentation introduced by
$numId is of type integer|double|false, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
374
            }
375
376 5
            if (is_array($ret)) {
377 5
                $ret['type'] = $type;
378 5
                $ret['object_id'] = $objectId;
379
            }
380
        }
381
382 5
        return $ret;
383
    }
384
385
    /**
386
     * Returns all tags in the database, optionally with the items attached to each tag.
387
     *
388
     * @param bool $withItems true to attach items
389
     *
390
     * @return array tags
391
     */
392 7
    public function getAllTags($withItems = false)
393
    {
394 7
        $q = 'SELECT * FROM TagTable';
395 7
        $res = $this->db->prepare($q);
396 7
        $res->execute();
397 7
        $ret = [];
398
399 7
        while (($data = $res->fetch(\PDO::FETCH_ASSOC))) {
400 7
            if ($withItems === true) {
401 1
                $data = $this->appendItemsToTag($data);
402
            }
403 7
            $ret[] = $data;
404
        }
405
406 7
        return $ret;
407
    }
408
409
    /**
410
     * Appends items to a tag
411
     *
412
     * @param string $tag tag
413
     *
414
     * @return array the altered tag
415
     */
416 2
    private function appendItemsToTag($tag)
417
    {
418 2
        if (isset($tag['photo_id_list'])) {
419 2
            $thisItems = array_map('trim', explode(',', $tag['photo_id_list']));
420 2
            foreach ($thisItems as $objectId) {
421 2
                if (!empty($objectId) > 0) {
422 2
                    $tag['items'][] = $this->getItemByObjectId($objectId);
423
                }
424
            }
425
        }
426 2
        return $tag;
427
    }
428
429
    /**
430
     * Gets items that are linked to a given tag
431
     *
432
     * @param string $tagName name of the tag
433
     *
434
     * @return array items
435
     */
436 1
    public function getItemsByTag($tagName)
437
    {
438 1
        $tagData = $this->getTag($tagName);
439 1
        $ret = array();
440
441 1
        if (is_array($tagData) && isset($tagData['photo_id_list'])) {
442 1
            $foundItems = explode(",", $tagData['photo_id_list']);
443
444 1
            foreach ($foundItems as $item) {
445 1
                $item = trim($item);
446 1
                if (!empty($item)) {
447 1
                    $thisItem = $this->getItemByObjectId($item);
448 1
                    if (is_array($thisItem)) {
449 1
                        $ret[] = $thisItem;
450
                    }
451
                }
452
            }
453
        }
454
455 1
        return $ret;
456
    }
457
458
    /**
459
     * Gets a given tag, possibly creating it when not existent and maybe attaching the items to it.
460
     *
461
     * @param string $tagName tag name
462
     * @param bool $autoCreate shall it be auto created if it doesn't exist?
463
     * @param bool $withItems  shall items be attached to it ('items' element)
464
     *
465
     * @return array|mixed|null tag
466
     */
467 4
    public function getTag($tagName, $autoCreate = false, $withItems = false)
468
    {
469 4
        $q = 'SELECT * FROM TagTable WHERE name=?';
470 4
        $res = $this->db->prepare($q);
471 4
        $res->execute(array(
472 4
                $tagName
473
        ));
474
475 4
        $ret = null;
476 4
        $data = $res->fetch(\PDO::FETCH_ASSOC);
477
478 4
        if (is_array($data) && isset($data['id'])) {
479 4
            $ret = $data;
480 4
            if ($withItems === true) {
481 4
                $ret = $this->appendItemsToTag($ret);
0 ignored issues
show
Documentation introduced by
$ret is of type array<string,?,{"id":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
482
            }
483
        } else {
484
            // autocreate?
485 3
            if ($autoCreate === true) {
486 2
                $this->createTag($tagName);
487 2
                return $this->getTag($tagName, $autoCreate, $withItems);
488
            }
489
        }
490
491 4
        return $ret;
492
    }
493
494
    /**
495
     * Gets the ID from a tag name
496
     *
497
     * @param $tagName tag name
498
     *
499
     * @return bool|string either the id or false if it doesn't exist
500
     */
501 3
    public function getTagId($tagName)
502
    {
503 3
        $ret = false;
504 3
        $tagData = $this->getTag($tagName);
505
506 3
        if (isset($tagData['id'])) {
507 3
            $ret = $tagData['id'];
508
        }
509
510 3
        return $ret;
511
    }
512
513
    /**
514
     * Create a new tag
515
     *
516
     * @param string $tagName tag name
517
     *
518
     * @return bool true if all good, false otherwise
519
     */
520 3
    public function createTag($tagName)
521
    {
522 3
        $ret = false;
523
524 3
        if (is_null($this->getTag($tagName))) {
525
            $insQ = "
526
                 INSERT INTO TagTable
527
                    (`name`, `photo_id_list`, `time_created`)
528
                    VALUES (?, ?, ?)
529 3
                    ";
530 3
            $res = $this->db->prepare($insQ);
531 3
            $res->execute([$tagName, '', time()]);
532 3
            $ret = true;
533
        }
534
535 3
        return $ret;
536
    }
537
538
    /**
539
     * Gets items whose filename (whole path) matches a given string
540
     *
541
     * @param string $path any part of the filename
542
     *
543
     * @return array matching items
544
     */
545 1
    public function getItemsByPath($path)
546
    {
547 1
        return $this->searchAllWithCondition('filename', $path);
548
    }
549
550
    /**
551
     * Flexible search on the database
552
     *
553
     * @param string $field Which field
554
     * @param string $value Which value
555
     *
556
     * @return array matching items
557
     */
558 2
    private function searchAllWithCondition($field = null, $value = null)
559
    {
560 2
        if ($field != null && $field != null) {
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $field of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison !== instead.
Loading history...
561
            $q = "
562
            SELECT id, 'video' as type FROM VideoTable
563 1
            WHERE `" . $field . "`  LIKE ?
564
            UNION ALL
565
            SELECT id, 'photo' FROM PhotoTable
566 1
            WHERE `" . $field . "` LIKE ?";
567
568 1
            $res = $this->db->prepare($q);
569 1
            $res->execute(['%' . $value . '%', '%' . $value . '%']);
570
        } else {
571
            $q = "
572
            SELECT id, 'video' as type FROM VideoTable
573
            UNION ALL
574 1
            SELECT id, 'photo' FROM PhotoTable";
575
576 1
            $res = $this->db->prepare($q);
577 1
            $res->execute([]);
578
        }
579
580 2
        $ret = array();
581
582 2
        while (($data = $res->fetch(\PDO::FETCH_ASSOC))) {
583 2
            if ($data['type'] == 'video') {
584 2
                $item = $this->getVideoById($data['id']);
585 2
                $ret[] = $item;
586
            }
587 2
            if ($data['type'] == 'photo') {
588 2
                $item = $this->getPhotoById($data['id']);
589 2
                $ret[] = $item;
590
            }
591
        }
592
593 2
        return $ret;
594
    }
595
596
    /**
597
     * Returns an array that has every item as key (Object IDs) and the value
598
     * is an array of tags associated to that item.
599
     *
600
     * @return array
601
     */
602 7
    public function getItemTagMap()
603
    {
604 7
        $tagData = $this->getAllTags();
605 7
        $ret = array();
606
607 7
        foreach ($tagData as $tag) {
608 7
            $thisItems = explode(",", $tag['photo_id_list']);
609 7
            foreach ($thisItems as $objectId) {
610 7
                if (strlen($objectId) > 0) {
611 7
                    $ret[$objectId][] = $tag['name'];
612
                }
613
            }
614
        }
615
616 7
        return $ret;
617
    }
618
619
    /**
620
     * Gets the object type (video or photo, compare with the constants) of a given Object ID
621
     *
622
     * @param string $objectId Object ID
623
     *
624
     * @return bool|int|string type
625
     */
626 5
    public function getTypeByObjectId($objectId)
627
    {
628 5
        $foundType = false;
629 5
        foreach ($this->typeMap as $intName => $idPart) {
630 5
            if (substr($objectId, 0, strlen($idPart)) == $idPart) {
631 5
                $foundType = $intName;
632 5
                break;
633
            }
634
        }
635
636 5
        return $foundType;
637
    }
638
639
    /**
640
     * Returns the Object ID with a type and a numeric ID
641
     *
642
     * @param string $type      Either photo or video, use the class constants
643
     * @param string $numericId Numeric ID
644
     *
645
     * @return string Object ID
646
     */
647 7
    public function getObjectIdByNumericId($type, $numericId)
648
    {
649 7
        $ret = $this->typeMap[$type];
650 7
        $ret .= str_pad(dechex($numericId), 16, "0", STR_PAD_LEFT);
651 7
        return $ret;
652
    }
653
654
    /**
655
     * Gets the type prefix for a given type
656
     *
657
     * @param string $type type
658
     *
659
     * @return bool|string prefix or false
660
     */
661 5
    public function getObjectIdPartIdByType($type)
662
    {
663 5
        if (isset($this->typeMap[$type])) {
664 5
            return $this->typeMap[$type];
665
        }
666
        return false;
667
    }
668
669
    /**
670
     * Returns the numeric ID of a given Object ID
671
     *
672
     * @param string $objectId Object ID
673
     *
674
     * @return bool|number|string numeric id
675
     */
676 5
    public function getNumericIdByObjectId($objectId)
677
    {
678 5
        $ret = false;
679 5
        $thisType = $this->getTypeByObjectId($objectId);
680 5
        if ($thisType !== false) {
681 5
            $ret = ltrim(
682
                substr(
683
                    $objectId,
684 5
                    strlen($this->getObjectIdPartIdByType($thisType))
685
                ),
686 5
                ' 0'
687
            );
688 5
            $ret = hexdec($ret);
689
        }
690
691 5
        return $ret;
692
    }
693
}
694