Passed
Push — develop ( 0cf906...cb779b )
by Jens
06:50
created

Repository::initConfigStorage()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 13
nc 1
nop 1
dl 0
loc 15
rs 9.4285
c 0
b 0
f 0
1
<?php
2
/**
3
 * User: Jens
4
 * Date: 30-1-2017
5
 * Time: 20:15
6
 */
7
8
namespace CloudControl\Cms\storage;
9
use CloudControl\Cms\storage\storage\DocumentStorage;
10
11
/**
12
 * Class Repository
13
 * @package CloudControl\Cms\storage
14
 * @property array sitemap
15
 * @property array applicationComponents
16
 * @property array documentTypes
17
 * @property array bricks
18
 * @property array imageSet
19
 * @property array images
20
 * @property array files
21
 * @property array users
22
 * @property array valuelists
23
 * @property array redirects
24
 */
25
class Repository
26
{
27
    protected $storagePath;
28
29
    protected $fileBasedSubsets = array('sitemap', 'applicationComponents', 'documentTypes', 'bricks', 'imageSet', 'images', 'files', 'users', 'valuelists', 'redirects');
30
31
    protected $sitemap;
32
    protected $sitemapChanges = false;
33
34
    protected $applicationComponents;
35
    protected $applicationComponentsChanges = false;
36
37
    protected $documentTypes;
38
    protected $documentTypesChanges = false;
39
40
    protected $bricks;
41
    protected $bricksChanges = false;
42
43
    protected $imageSet;
44
    protected $imageSetChanges = false;
45
46
    protected $images;
47
    protected $imagesChanges = false;
48
49
    protected $files;
50
    protected $filesChanges = false;
51
52
    protected $users;
53
    protected $usersChanges = false;
54
55
    protected $valuelists;
56
    protected $valuelistsChanges = false;
57
58
    protected $redirects;
59
    protected $redirectsChanges = false;
60
61
    protected $contentDbHandle;
62
63
64
    /**
65
     * Repository constructor.
66
     * @param $storagePath
67
     * @throws \Exception
68
     */
69
    public function __construct($storagePath)
70
    {
71
        $storagePath = realpath($storagePath);
72
        if (is_dir($storagePath) && $storagePath !== false) {
73
            $this->storagePath = $storagePath;
74
        } else {
75
            throw new \Exception('Repository not yet initialized.');
76
        }
77
    }
78
79
    /**
80
     * Creates the folder in which to create all storage related files
81
     *
82
     * @param $storagePath
83
     * @return bool
84
     */
85
    public static function create($storagePath)
86
    {
87
        return mkdir($storagePath);
88
    }
89
90
    /**
91
     * Initiates default storage
92
     * @param $baseStorageDefaultPath
93
     * @param $baseStorageSqlPath
94
     */
95
    public function init($baseStorageDefaultPath, $baseStorageSqlPath)
96
    {
97
        $storageDefaultPath = realpath($baseStorageDefaultPath);
98
        $contentSqlPath = realpath($baseStorageSqlPath);
99
100
        $this->initConfigStorage($storageDefaultPath);
101
        $this->initContentDb($contentSqlPath);
102
103
        $this->save();
104
    }
105
106
    /**
107
     * Load filebased subset and return it's contents
108
     *
109
     * @param $name
110
     * @return mixed|string
111
     * @throws \Exception
112
     */
113
    public function __get($name)
114
    {
115
        if (isset($this->$name)) {
116 View Code Duplication
            if (in_array($name, $this->fileBasedSubsets)) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
117
                return $this->$name;
118
            } else {
119
                throw new \Exception('Trying to get undefined property from Repository: ' . $name);
120
            }
121 View Code Duplication
        } else {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
122
            if (in_array($name, $this->fileBasedSubsets)) {
123
                return $this->loadSubset($name);
124
            } else {
125
                throw new \Exception('Trying to get undefined property from Repository: ' . $name);
126
            }
127
        }
128
    }
129
130
    /**
131
     * Set filebased subset contents
132
     * @param $name
133
     * @param $value
134
     * @throws \Exception
135
     */
136
    public function __set($name, $value)
137
    {
138
        if (in_array($name, $this->fileBasedSubsets)) {
139
            $this->$name = $value;
140
            $changes = $name . 'Changes';
141
            $this->$changes = true;
142
        } else {
143
            throw new \Exception('Trying to persist unknown subset in repository: ' . $name . ' <br /><pre>' . print_r($value, true) . '</pre>');
144
        }
145
    }
146
147
    /**
148
     * Persist all subsets
149
     */
150
    public function save()
151
    {
152
        $host = $this;
153
        array_map(function ($value) use ($host) {
154
            $host->saveSubset($value);
155
		}, $this->fileBasedSubsets);
156
    }
157
158
    /**
159
     * Persist subset to disk
160
     * @param $subset
161
     */
162
    public function saveSubset($subset)
163
    {
164
		$changes = $subset . 'Changes';
165
		if ($this->$changes === true) {
166
            if (!defined('JSON_PRETTY_PRINT')) {
167
                $json = json_encode($this->$subset);
168
            } else {
169
                $json = json_encode($this->$subset, JSON_PRETTY_PRINT);
170
            }
171
			$subsetStoragePath = $this->storagePath . DIRECTORY_SEPARATOR . $subset . '.json';
172
			file_put_contents($subsetStoragePath, $json);
173
174
			$this->$changes = false;
175
		}
176
    }
177
178
    /**
179
     * Load subset from disk
180
     * @param $subset
181
     * @return mixed|string
182
     */
183
    protected function loadSubset($subset)
184
    {
185
        $subsetStoragePath = $this->storagePath . DIRECTORY_SEPARATOR . $subset . '.json';
186
        $json = file_get_contents($subsetStoragePath);
187
        $json = json_decode($json);
188
        $this->$subset = $json;
189
        return $json;
190
    }
191
192
    /**
193
     * @param $contentSqlPath
194
     */
195
    protected function initContentDb($contentSqlPath)
196
    {
197
        $db = $this->getContentDbHandle();
198
        $sql = file_get_contents($contentSqlPath);
199
        $db->exec($sql);
200
    }
201
202
    /**
203
     * @param $storageDefaultPath
204
     */
205
    protected function initConfigStorage($storageDefaultPath)
206
    {
207
        $json = file_get_contents($storageDefaultPath);
208
        $json = json_decode($json);
209
        $this->initConfigIfNotExists($json, 'sitemap');
210
        $this->initConfigIfNotExists($json, 'applicationComponents');
211
        $this->initConfigIfNotExists($json, 'documentTypes');
212
        $this->initConfigIfNotExists($json, 'bricks');
213
        $this->initConfigIfNotExists($json, 'imageSet');
214
        $this->initConfigIfNotExists($json, 'images');
215
        $this->initConfigIfNotExists($json, 'files');
216
        $this->initConfigIfNotExists($json, 'users');
217
        $this->initConfigIfNotExists($json, 'valuelists');
218
        $this->initConfigIfNotExists($json, 'redirects');
219
    }
220
221
    /**
222
     * @return \PDO
223
     */
224
    public function getContentDbHandle()
225
    {
226
        if ($this->contentDbHandle === null) {
227
            $this->contentDbHandle = new \PDO('sqlite:' . $this->storagePath . DIRECTORY_SEPARATOR . 'content.db');
228
        }
229
        return $this->contentDbHandle;
230
    }
231
232
	/**
233
	 * Get all documents
234
	 *
235
	 * @param string $state
236
	 *
237
	 * @return array
238
	 * @throws \Exception
239
	 */
240
    public function getDocuments($state = 'published')
241
    {
242
		if (!in_array($state, Document::$DOCUMENT_STATES)) {
243
			throw new \Exception('Unsupported document state: ' . $state);
244
		}
245
        return $this->getDocumentsByPath('/', $state);
246
    }
247
248
	public function getDocumentsWithState($folderPath = '/')
249
	{
250
		$db = $this->getContentDbHandle();
251
		$folderPathWithWildcard = $folderPath . '%';
252
253
		$ifRootIndex = 1;
254
		if ($folderPath == '/') {
255
			$ifRootIndex = 0;
256
		}
257
258
		$sql = '
259
            SELECT documents_unpublished.*,
260
            	   IFNULL(documents_published.state,"unpublished") as state,
261
            	   IFNULL(documents_published.publicationDate,NULL) as publicationDate,
262
            	   (documents_published.lastModificationDate != documents_unpublished.lastModificationDate) as unpublishedChanges 
263
              FROM documents_unpublished
264
		 LEFT JOIN documents_published
265
         		ON documents_published.path = documents_unpublished.path
266
             WHERE documents_unpublished.`path` LIKE ' . $db->quote($folderPathWithWildcard) . '
267
               AND substr(documents_unpublished.`path`, ' . (strlen($folderPath) + $ifRootIndex + 1) . ') NOT LIKE "%/%"
268
               AND length(documents_unpublished.`path`) > ' . (strlen($folderPath) + $ifRootIndex) . '
269
               AND documents_unpublished.path != ' . $db->quote($folderPath) . '
270
          ORDER BY documents_unpublished.`type` DESC, documents_unpublished.`path` ASC
271
        ';
272
		$stmt = $this->getDbStatement($sql);
273
274
275
276
		$documents = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
277
		foreach ($documents as $key => $document) {
278
			$documents = $this->setAssetsToDocumentFolders($document, $db, $documents, $key);
279
		}
280
		//dump($documents);
281
		return $documents;
282
	}
283
284
	/**
285
	 * Get all documents and folders in a certain path
286
	 *
287
	 * @param        $folderPath
288
	 * @param string $state
289
	 *
290
	 * @return array
291
	 * @throws \Exception
292
	 */
293
    public function getDocumentsByPath($folderPath, $state = 'published')
294
    {
295
    	if (!in_array($state, Document::$DOCUMENT_STATES)) {
296
    		throw new \Exception('Unsupported document state: ' . $state);
297
		}
298
        $db = $this->getContentDbHandle();
299
        $folderPathWithWildcard = $folderPath . '%';
300
301
        $sql = 'SELECT *
302
              FROM documents_' . $state . '
303
             WHERE `path` LIKE ' . $db->quote($folderPathWithWildcard) . '
304
               AND substr(`path`, ' . (strlen($folderPath) + 1) . ') NOT LIKE "%/%"
305
               AND path != ' . $db->quote($folderPath) . '
306
          ORDER BY `type` DESC, `path` ASC';
307
        $stmt = $this->getDbStatement($sql);
308
309
        $documents = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
310
        foreach ($documents as $key => $document) {
311
			$documents = $this->setAssetsToDocumentFolders($document, $db, $documents, $key);
312
        }
313
        return $documents;
314
    }
315
316
317
    /**
318
     * @param $path
319
     * @return bool|Document
320
     */
321
    public function getDocumentContainerByPath($path)
322
    {
323
        $document = $this->getDocumentByPath($path, 'unpublished');
324
        if ($document === false) {
325
            return false;
326
        }
327
        $slugLength = strlen($document->slug);
328
        $containerPath = substr($path, 0, -$slugLength);
329
        if ($containerPath === '/') {
330
            return $this->getRootFolder();
331
        }
332
        if (substr($containerPath, -1) === '/'){
333
			$containerPath = substr($containerPath, 0, -1);
334
		}
335
        $containerFolder = $this->getDocumentByPath($containerPath, 'unpublished');
336
        return $containerFolder;
337
    }
338
339
	/**
340
	 * @param        $path
341
	 * @param string $state
342
	 *
343
	 * @return bool|\CloudControl\Cms\storage\Document
344
	 * @throws \Exception
345
	 */
346
    public function getDocumentByPath($path, $state = 'published')
347
    {
348
		if (!in_array($state, Document::$DOCUMENT_STATES)) {
349
			throw new \Exception('Unsupported document state: ' . $state);
350
		}
351
        $db = $this->getContentDbHandle();
352
        $document = $this->fetchDocument('
353
            SELECT *
354
              FROM documents_' .  $state . '
355
             WHERE path = ' . $db->quote($path) . '
356
        ');
357
        if ($document instanceof Document && $document->type === 'folder') {
358
            $document->dbHandle = $db;
359
            $document->documentStorage = new DocumentStorage($this);
360
        }
361
        return $document;
362
    }
363
364
	/**
365
	 * Returns the count of all documents stored in the db
366
	 *
367
	 * @param string $state
368
	 *
369
	 * @return int
370
	 * @throws \Exception
371
	 */
372
	public function getTotalDocumentCount($state = 'published')
373
	{
374
		if (!in_array($state, Document::$DOCUMENT_STATES)) {
375
			throw new \Exception('Unsupported document state: ' . $state);
376
		}
377
		$db = $this->getContentDbHandle();
378
		$stmt = $db->query('
379
			SELECT count(*)
380
			  FROM documents_' . $state . '
381
			 WHERE `type` != "folder"
382
		');
383
		$result = $stmt->fetch(\PDO::FETCH_ASSOC);
384
		if (!is_array($result )) {
385
			return 0;
386
		}
387
		return intval(current($result));
388
	}
389
390
	public function getPublishedDocumentsNoFolders()
391
	{
392
		$db = $this->getContentDbHandle();
393
		$sql = '
394
			SELECT *
395
			  FROM documents_published
396
			 WHERE `type` != "folder"
397
		';
398
		$stmt = $db->query($sql);
399
		$result = $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
400
		if ($stmt === false || !$stmt->execute()) {
401
			$errorInfo = $db->errorInfo();
402
			$errorMsg = $errorInfo[2];
403
			throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
404
		}
405
		return $result;
406
	}
407
408
	private function publishOrUnpublishDocumentByPath($path, $publish = true) {
409
		if ($publish) {
410
			$sql = '
411
				INSERT OR REPLACE INTO documents_published 
412
					  (`id`,`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,`state`,`lastModificationDate`,`creationDate`,`publicationDate`,`lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`)
413
				SELECT `id`,`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,"published" as state,`lastModificationDate`,`creationDate`,' . time() . ' as publicationDate, `lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`
414
				  FROM documents_unpublished
415
				 WHERE `path` = :path
416
			';
417
		} else {
418
			$sql = 'DELETE FROM documents_published
419
					  WHERE `path` = :path';
420
		}
421
		$db = $this->getContentDbHandle();
422
		$stmt = $db->prepare($sql);
423
		if ($stmt === false) {
424
			$errorInfo = $db->errorInfo();
425
			$errorMsg = $errorInfo[2];
426
			throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
427
		}
428
		$stmt->bindValue(':path', $path);
429
		$stmt->execute();
430
	}
431
432
	public function publishDocumentByPath($path)
433
	{
434
		$this->publishOrUnpublishDocumentByPath($path);
435
	}
436
437
	public function unpublishDocumentByPath($path)
438
	{
439
		$this->publishOrUnpublishDocumentByPath($path, false);
440
	}
441
442
	public function cleanPublishedDeletedDocuments()
443
	{
444
		$sql = '   DELETE FROM documents_published
445
						 WHERE documents_published.path IN (
446
						SELECT documents_published.path
447
						  FROM documents_published
448
					 LEFT JOIN documents_unpublished
449
							ON documents_unpublished.path = documents_published.path
450
						 WHERE documents_unpublished.path IS NULL
451
		)';
452
		$stmt = $this->getDbStatement($sql);
453
		$stmt->execute();
454
	}
455
456
	/**
457
     * Return the results of the query as array of Documents
458
     * @param $sql
459
     * @return array
460
     * @throws \Exception
461
     */
462
    protected function fetchAllDocuments($sql)
463
    {
464
        $stmt = $this->getDbStatement($sql);
465
        return $stmt->fetchAll(\PDO::FETCH_CLASS, '\CloudControl\Cms\storage\Document');
466
    }
467
468
    /**
469
     * Return the result of the query as Document
470
     * @param $sql
471
     * @return mixed
472
     * @throws \Exception
473
     */
474
    protected function fetchDocument($sql)
475
    {
476
        $stmt = $this->getDbStatement($sql);
477
        return $stmt->fetchObject('\CloudControl\Cms\storage\Document');
478
    }
479
480
    /**
481
     * Prepare the sql statement
482
     * @param $sql
483
     * @return \PDOStatement
484
     * @throws \Exception
485
     */
486
    protected function getDbStatement($sql)
487
    {
488
        $db = $this->getContentDbHandle();
489
        $stmt = $db->query($sql);
490
        if ($stmt === false) {
491
            $errorInfo = $db->errorInfo();
492
            $errorMsg = $errorInfo[2];
493
            throw new \Exception('SQLite Exception: ' . $errorMsg . ' in SQL: <br /><pre>' . $sql . '</pre>');
494
        }
495
        return $stmt;
496
    }
497
498
    /**
499
     * Create a (non-existent) root folder Document and return it
500
     * @return Document
501
     */
502
    protected function getRootFolder()
503
    {
504
        $rootFolder = new Document();
505
        $rootFolder->path = '/';
506
        $rootFolder->type = 'folder';
507
        return $rootFolder;
508
    }
509
510
	/**
511
	 * Save the document to the database
512
	 *
513
	 * @param Document $documentObject
514
	 * @param string   $state
515
	 *
516
	 * @return bool
517
	 * @throws \Exception
518
	 * @internal param $path
519
	 */
520
    public function saveDocument($documentObject, $state = 'published')
521
    {
522
		if (!in_array($state, Document::$DOCUMENT_STATES)) {
523
			throw new \Exception('Unsupported document state: ' . $state);
524
		}
525
        $db = $this->getContentDbHandle();
526
        $stmt = $this->getDbStatement('
527
            INSERT OR REPLACE INTO documents_' . $state . ' (`path`,`title`,`slug`,`type`,`documentType`,`documentTypeSlug`,`state`,`lastModificationDate`,`creationDate`,`lastModifiedBy`,`fields`,`bricks`,`dynamicBricks`)
528
            VALUES(
529
              ' . $db->quote($documentObject->path) . ',
530
              ' . $db->quote($documentObject->title) . ',
531
              ' . $db->quote($documentObject->slug) . ',
532
              ' . $db->quote($documentObject->type) . ',
533
              ' . $db->quote($documentObject->documentType) . ',
534
              ' . $db->quote($documentObject->documentTypeSlug) . ',
535
              ' . $db->quote($documentObject->state) . ',
536
              ' . $db->quote($documentObject->lastModificationDate) . ',
537
              ' . $db->quote($documentObject->creationDate) . ',
538
              ' . $db->quote($documentObject->lastModifiedBy) . ',
539
              ' . $db->quote(json_encode($documentObject->fields)) . ',
540
              ' . $db->quote(json_encode($documentObject->bricks)) . ',
541
              ' . $db->quote(json_encode($documentObject->dynamicBricks)) . '
542
            )
543
        ');
544
        $result = $stmt->execute();
545
        return $result;
546
    }
547
548
	/**
549
	 * Delete the document from the database
550
	 * If it's a folder, also delete it's contents
551
	 *
552
	 * @param        $path
553
	 *
554
	 * @internal param string $state
555
	 *
556
	 */
557
    public function deleteDocumentByPath($path)
558
    {
559
        $db = $this->getContentDbHandle();
560
        $documentToDelete = $this->getDocumentByPath($path, 'unpublished');
561
        if ($documentToDelete instanceof Document) {
562
            if ($documentToDelete->type == 'document') {
563
                $stmt = $this->getDbStatement('
564
                    DELETE FROM documents_unpublished
565
                          WHERE path = ' . $db->quote($path) . '
566
                ');
567
                $stmt->execute();
568
            } elseif ($documentToDelete->type == 'folder') {
569
                $folderPathWithWildcard = $path . '%';
570
                $stmt = $this->getDbStatement('
571
                    DELETE FROM documents_unpublished
572
                          WHERE (path LIKE ' . $db->quote($folderPathWithWildcard) . '
573
                            AND substr(`path`, ' . (strlen($path) + 1) . ', 1) = "/")
574
                            OR path = ' . $db->quote($path) . '
575
                ');
576
                $stmt->execute();
577
            }
578
        }
579
    }
580
581
	/**
582
	 * @param $document
583
	 * @param $db
584
	 * @param $documents
585
	 * @param $key
586
	 *
587
	 * @return mixed
588
	 */
589
	private function setAssetsToDocumentFolders($document, $db, $documents, $key)
590
	{
591
		if ($document->type === 'folder') {
592
			$document->dbHandle = $db;
593
			$document->documentStorage = new DocumentStorage($this);
594
			$documents[$key] = $document;
595
		}
596
597
		return $documents;
598
	}
599
600
    private function initConfigIfNotExists($json, $subsetName)
601
    {
602
        $subsetFileName = $this->storagePath . DIRECTORY_SEPARATOR . $subsetName . '.json';
603
        if (file_exists($subsetFileName)) {
604
            $this->loadSubset($subsetName);
605
        } else {
606
            $changes = $subsetName . 'Changes';
607
            $this->$subsetName = $json->$subsetName;
608
            $this->$changes = true;
609
        }
610
    }
611
}