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() |
|
|
|
|
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(); |
|
|
|
|
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
|
|
|
|
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.