Completed
Push — master ( 4df29a...a6c87b )
by Sam
02:28
created

Item::performFileSystemOperation()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 18
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 12
nc 7
nop 2
1
<?php
2
3
namespace App;
4
5
use Exception;
6
use Intervention\Image\Constraint;
7
use Intervention\Image\ImageManager;
8
9
class Item
10
{
11
12
    /** The default date granularity is 'exact' (ID 1). */
13
    const DATE_GRANULARITY_DEFAULT = 1;
14
15
    /** @var \App\Db */
16
    private $db;
17
18
    /** @var \StdClass */
19
    private $data;
20
21
    /** @var User */
22
    protected $user;
23
24
    public function __construct($id = null, User $user = null)
25
    {
26
        $this->db = new Db();
27
        if ($id !== null) {
28
            $this->load($id);
29
        }
30
        $this->user = $user;
31
    }
32
33
    /**
34
     * When Items are loaded as results of a PDO::query(), the values are set as public class
35
     * attributes. We want to do more than just set the value, though, so we don't set any column
36
     * names as attributes of this class, and instead handle them here.
37
     * @param string $varName The name of the class attribute.
38
     * @param mixed $value The value to set the class attribute to.
39
     */
40
    public function __set($varName, $value)
41
    {
42
        if ($varName === 'id') {
43
            $this->load($value);
44
        }
45
    }
46
47
    /**
48
     * Set the current user.
49
     * @param User $user
50
     */
51
    public function setUser(User $user)
52
    {
53
        $this->user = $user;
54
    }
55
56
    public function load($id)
57
    {
58
        if (!empty($id) && !is_numeric($id)) {
59
            throw new Exception("Not an Item ID: " . print_r($id, true));
60
        }
61
        $sql = 'SELECT items.id, items.title, items.description, items.date, '
62
            . '    items.date_granularity, dg.php_format AS date_granularity_format, '
63
            . '    items.read_group, items.edit_group '
64
            . ' FROM items JOIN date_granularities dg ON dg.id=items.date_granularity '
65
            . ' WHERE items.id=:id ';
66
        $params = ['id' => $id];
67
        $this->data = $this->db->query($sql, $params)->fetch();
68
    }
69
70
    /**
71
     * Is this item editable by any of the current user's groups?
72
     *
73
     * @return bool
74
     */
75
    public function editable()
76
    {
77
        if (!$this->user || $this->user->getId() === false) {
78
            return false;
79
        }
80
        if ($this->getId() === false) {
81
            return true;
82
        }
83
        $editGroupId = $this->getEditGroup()->id;
84
        foreach ($this->user->getGroups() as $group) {
85
            if ($editGroupId == $group['id']) {
86
                return true;
87
            }
88
        }
89
        return false;
90
    }
91
92
    /**
93
     * Save an item's data.
94
     *
95
     * @param string[] $metadata Array of metadata pairs.
96
     * @param string $tagsString CSV string of tags.
97
     * @param string $filename The full filesystem path to a file to attach to this Item. Don't use with $fileContents.
98
     * @param string $fileContents A string to treat as the contents of a file. Don't use with $filename.
99
     * @return false
100
     * @throws Exception If the item is not editable by the current user.
101
     */
102
    public function save($metadata, $tagsString = null, $filename = null, $fileContents = null)
103
    {
104
        if (isset($metadata['id'])) {
105
            $this->load($metadata['id']);
106
        }
107
        if (!$this->editable()) {
108
            throw new Exception("You are not allowed to edit this item.");
109
        }
110
        if (empty($metadata['title'])) {
111
            $metadata['title'] = 'Untitled';
112
        }
113
        if (empty($metadata['description'])) {
114
            $metadata['description'] = null;
115
        }
116
        if (empty($metadata['date'])) {
117
            $metadata['date'] = null;
118
        }
119
        if (empty($metadata['date_granularity'])) {
120
            $metadata['date_granularity'] = self::DATE_GRANULARITY_DEFAULT;
121
        }
122
        if (empty($metadata['edit_group'])) {
123
            $metadata['edit_group'] = $this->getEditGroup()->id;
124
        }
125
        if (empty($metadata['read_group'])) {
126
            $metadata['read_group'] = $this->getReadGroup()->id;
127
        }
128
        $setClause = 'SET title=:title, description=:description, date=:date, '
129
            . ' date_granularity=:date_granularity, edit_group=:edit_group, read_group=:read_group ';
130
131
        // Start a transaction. End after the key words and files have been written.
132
        $this->db->query('BEGIN');
133
134
        if ($this->isLoaded()) {
135
            // Update?
136
            $metadata['id'] = $this->getId();
137
            $sql = "UPDATE items $setClause WHERE id=:id";
138
            $this->db->query($sql, $metadata);
139
            $id = $metadata['id'];
140
        } else {
141
            // Or insert?
142
            unset($metadata['id']);
143
            $sql = "INSERT INTO items $setClause";
144
            $this->db->query($sql, $metadata);
145
            $id = $this->db->lastInsertId();
146
        }
147
        $this->load($id);
148
149
        // Save tags.
150
        if (!empty($tagsString)) {
151
            $this->db->query("DELETE FROM item_tags WHERE item=:id", ['id' => $id]);
152
            $tags = array_map('trim', array_unique(str_getcsv($tagsString)));
153
            foreach ($tags as $tag) {
154
                $this->db->query("INSERT IGNORE INTO tags SET title=:title", ['title' => $tag]);
155
                $selectTagId = "SELECT id FROM tags WHERE title LIKE :title";
156
                $tagId = $this->db->query($selectTagId, ['title' => $tag])->fetchColumn();
157
                $insertJoin = "INSERT IGNORE INTO item_tags SET item=:item, tag=:tag";
158
                $this->db->query($insertJoin, ['item' => $id, 'tag' => $tagId]);
159
            }
160
        }
161
162
        $newVer = $this->getVersionCount() + 1;
163
        // Save file contents.
164
        if (!empty($fileContents)) {
165
            $filesystem = App::getFilesystem();
166
            $filesystem->put("storage://" . $this->getFilePath($newVer), $fileContents);
167
        }
168
169
        // Save uploaded file.
170
        if (!empty($filename)) {
171
            $filesystem = App::getFilesystem();
172
            $stream = fopen($filename, 'r+');
173
            $filesystem->putStream("storage://" . $this->getFilePath($newVer), $stream);
174
            fclose($stream);
175
        }
176
177
        // End the transaction and reload the data from the DB.
178
        $this->db->query('COMMIT');
179
    }
180
181
    /**
182
     * Whether this file is a text file.
183
     */
184
    public function isText($version = null)
185
    {
186
        return $this->getMimeType($version) === 'text/plain';
187
    }
188
189
    public function isImage($version = null)
190
    {
191
        return 0 === strpos($this->getMimeType($version), 'image');
192
    }
193
194
    /**
195
     * Get the file's mime type, or false if there's no file.
196
     * @param integer $version
197
     * @return integer|false
198
     */
199
    public function getMimeType($version = null)
200
    {
201
        return $this->performFileSystemOperation('getMimetype', $version);
202
    }
203
204
    /**
205
     * Get the contents of the file.
206
     *
207
     * @param integer $version Which file version to get.
208
     * @return false|string
209
     * @throws \Exception
210
     */
211
    public function getFileContents($version = null)
212
    {
213
        return $this->performFileSystemOperation('read', $version);
214
    }
215
216
    /**
217
     * Perform an operation on the filesystem file.
218
     * @param integer|null $version
219
     * @param string $method The method name, from the MountManager class.
220
     * @return bool|false|string
221
     * @throws Exception If the method can't be executed.
222
     */
223
    protected function performFileSystemOperation($method, $version)
224
    {
225
        if (!$this->isLoaded()) {
226
            return false;
227
        }
228
        if (is_null($version)) {
229
            $version = $this->getVersionCount();
230
        }
231
        $filesystem = App::getFilesystem();
232
        $path = "storage://" . $this->getFilePath($version);
233
        if ($filesystem->has($path)) {
234
            if (!is_callable([$filesystem, $method])) {
235
                throw new Exception("Unable to execute $method on the filesystem");
236
            }
237
            return $filesystem->$method($path);
238
        }
239
        return false;
240
    }
241
242
    /**
243
     * Get a local filesystem path to the requested file, creating it if required.
244
     * @param string $format The file format, one of 'o', 'd', or 't'.
245
     * @param int $version The version of the file to fetch.
246
     * @return string The fully-qualified path to the file.
247
     * @throws \Exception
248
     */
249
    public function getCachePath($format = 'o', $version = null)
250
    {
251
        if (is_null($version)) {
252
            $version = $this->getVersionCount();
253
        }
254
        if ($version > $this->getVersionCount()) {
255
            throw new Exception("Version $version does not exist for Item ".$this->getId());
256
        }
257
        $filesystem = App::getFilesystem();
258
        $path = $this->getFilePath($version);
259
260
        // Get local filesystem root.
261
        $config = new Config();
262
        $filesystems = $config->filesystems();
263
        $root = realpath($filesystems['cache']['root']);
264
265
        // First of all copy the original file to the cache.
266
        $filenameOrig = $this->getId() . '_v' . $version . '_o';
267
        if (!$filesystem->has("cache://" . $filenameOrig)) {
268
            $filesystem->copy("storage://$path", "cache://" . $filenameOrig);
269
        }
270
        $pathnameOrig = $root . DIRECTORY_SEPARATOR . $filenameOrig;
271
        if ($format === 'o') {
272
            return $pathnameOrig;
273
        }
274
275
        // Then create smaller version if required.
276
        $filenameDisplay = $this->getId() . '_v' . $version . '_t';
277
        $pathnameDisplay = $root . DIRECTORY_SEPARATOR . $filenameDisplay;
278
        $manager = new ImageManager();
279
        $image = $manager->make($pathnameOrig);
280
        if ($format === 'd') {
281
            // 'Display' size.
282
            $width = $image->getWidth();
283
            $height = $image->getHeight();
284
            $newWidth = ($width > $height) ? 700 : null;
285
            $newHeight = ($width > $height) ? null : 700;
286
            $image->resize($newWidth, $newHeight, function (Constraint $constraint) {
287
                $constraint->aspectRatio();
288
                $constraint->upsize();
289
            });
290
        } else {
291
            // Thumbnail.
292
            $image->fit(200);
293
        }
294
        $image->save($pathnameDisplay);
295
296
        clearstatcache(false, $pathnameDisplay);
297
298
        return $pathnameDisplay;
299
    }
300
301
    public function getFileStream($version = null)
302
    {
303
        if (is_null($version)) {
304
            $version = $this->getVersionCount();
305
        }
306
        $filesystem = App::getFilesystem();
307
        $path = "storage://" . $this->getFilePath($version);
308
        if ($filesystem->has($path)) {
309
            return $filesystem->readStream($path);
310
        }
311
    }
312
313
    public function getVersionCount()
314
    {
315
        $filesystem = App::getFilesystem();
316
        $out = $filesystem->getFilesystem('storage')->listContents($this->getHashedPath());
317
        return count($out);
318
    }
319
320
    public function getHashedPath()
321
    {
322
        $hash = md5($this->getId());
323
        return $hash[0] . $hash[1] . '/' . $hash[2] . $hash[3] . '/' . $this->getId();
324
    }
325
326
    /**
327
     * Get the path to a version of the attached file.
328
     * Never has a leading slash, and the last component is the filename.
329
     * @return string
330
     * @throws \Exception
331
     */
332
    public function getFilePath($version = null)
333
    {
334
        if (is_null($version)) {
335
            $version = $this->getVersionCount();
336
        }
337
        if (!is_int($version)) {
338
            throw new \Exception("Version must be an integer ('$version' was given)");
339
        }
340
        return $this->getHashedPath() . '/v' . $version;
341
    }
342
343
    public function getTags()
344
    {
345
        $tagsSql = 'SELECT t.id, t.title '
346
            . ' FROM item_tags it JOIN tags t ON (it.tag=t.id) '
347
            . ' WHERE it.item=:id '
348
            . ' ORDER BY t.title ASC ';
349
        $params = ['id' => $this->getId()];
350
        return $this->db->query($tagsSql, $params)->fetchAll();
351
    }
352
353
    public function getTagsString()
354
    {
355
        $out = [];
356
        foreach ($this->getTags() as $tag) {
357
            $out[] = $tag->title;
358
        }
359
        return join(', ', $out);
360
    }
361
362
    /**
363
     * Find out whether an Item is loaded.
364
     * @uses \App\Item::getId() If an Item has an ID, it's considered loaded.
365
     * @return boolean
366
     */
367
    public function isLoaded()
368
    {
369
        return ($this->getId() !== false);
370
    }
371
372
    public function getId()
373
    {
374
        return isset($this->data->id) ? (int) $this->data->id : false;
375
    }
376
377
    public function getTitle()
378
    {
379
        return isset($this->data->title) ? $this->data->title : false;
380
    }
381
382
    public function getDescription()
383
    {
384
        return isset($this->data->description) ? $this->data->description : false;
385
    }
386
387
    public function getEditGroup()
388
    {
389
        $defaultGroup = ($this->user instanceof User) ? $this->user->getDefaultGroup()->id : User::GROUP_ADMIN;
390
        $groupId = isset($this->data->edit_group) ? $this->data->edit_group : $defaultGroup;
391
        $editGroup = $this->db->query("SELECT * FROM groups WHERE id=:id", ['id'=>$groupId])->fetch();
392
        return $editGroup;
393
    }
394
395 View Code Duplication
    public function getReadGroup()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
396
    {
397
        $groupId = isset($this->data->read_group) ? $this->data->read_group : User::GROUP_PUBLIC;
398
        $readGroup = $this->db->query("SELECT * FROM groups WHERE id=:id", ['id'=>$groupId])->fetch();
399
        //dump($readGroup);exit();
0 ignored issues
show
Unused Code Comprehensibility introduced by
89% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
400
        return $readGroup;
401
    }
402
403
    /**
404
     * Get the raw unformatted date string, including the time component. This may have lots of
405
     * trailing zeros, depending on the granularity of this item.
406
     * @return string|boolean The date, or false if there isn't one.
407
     */
408
    public function getDate()
409
    {
410
        // For new items, default to the current time.
411
        if (!$this->isLoaded()) {
412
            return date('Y-m-d H:i:s');
413
        }
414
        // Otherwise, use what's in the database or false when there's no date.
415
        return isset($this->data->date) ? $this->data->date : false;
416
    }
417
418
    /**
419
     * @return string
420
     */
421
    public function getDateFormatted()
422
    {
423
        if (empty($this->data->date)) {
424
            return '';
425
        }
426
        $format = $this->data->date_granularity_format;
427
        $date = new \DateTime($this->data->date);
428
        return $date->format($format);
429
    }
430
431
    public function getDateGranularity()
432
    {
433
        return isset($this->data->date_granularity) ? $this->data->date_granularity : self::DATE_GRANULARITY_DEFAULT;
434
    }
435
}
436