Completed
Push — master ( ec315b...376b23 )
by Nazar
04:18
created

Posts::get_as_json_ld()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 7
rs 9.4285
cc 2
eloc 5
nc 2
nop 1
1
<?php
2
/**
3
 * @package   Blogs
4
 * @category  modules
5
 * @author    Nazar Mokrynskyi <[email protected]>
6
 * @copyright Copyright (c) 2011-2016, Nazar Mokrynskyi
7
 * @license   MIT License, see license.txt
8
 */
9
namespace cs\modules\Blogs;
10
use
11
	cs\Event,
12
	cs\Cache\Prefix as Cache_prefix,
13
	cs\Config,
14
	cs\Language,
15
	cs\Language\Prefix as Language_prefix,
16
	cs\User,
17
	cs\CRUD,
18
	cs\Singleton,
19
	cs\plugins\Json_ld\Json_ld;
20
21
/**
22
 * @method static $this instance($check = false)
23
 *
24
 * @todo: Use joined CRUD tables
25
 */
26
class Posts {
27
	use
28
		CRUD,
29
		Singleton;
30
	protected $data_model                  = [
31
		'id'      => 'int:0',
32
		'user'    => 'int:0',
33
		'date'    => 'int:0',
34
		'title'   => 'ml:text',
35
		'path'    => 'ml:text',
36
		'content' => 'ml:html',
37
		'draft'   => 'int:0..1'
38
	];
39
	protected $table                       = '[prefix]blogs_posts';
40
	protected $data_model_ml_group         = 'Blogs/posts';
41
	protected $data_model_files_tag_prefix = 'Blogs/posts';
42
	protected $table_sections              = '[prefix]blogs_posts_sections';
43
	protected $table_tags                  = '[prefix]blogs_posts_tags';
44
	/**
45
	 * @var Cache_prefix
46
	 */
47
	protected $cache;
48
49
	protected function construct () {
50
		$this->cache = new Cache_prefix('Blogs');
51
		if (Config::instance()->module('Blogs')->allow_iframes_without_content) {
52
			$this->data_model['content'] = 'ml:html_iframe';
53
		}
54
	}
55
	/**
56
	 * Returns database index
57
	 *
58
	 * @return int
59
	 */
60
	protected function cdb () {
61
		return Config::instance()->module('Blogs')->db('posts');
62
	}
63
	/**
64
	 * Get data of specified post
65
	 *
66
	 * @param int|int[] $id
67
	 *
68
	 * @return array|false
69
	 */
70
	function get ($id) {
71
		if (is_array($id)) {
72
			foreach ($id as &$i) {
73
				$i = $this->get($i);
74
			}
75
			return $id;
76
		}
77
		$L        = Language::instance();
78
		$id       = (int)$id;
79
		$data     = $this->cache->get(
80
			"posts/$id/$L->clang",
81
			function () use ($id, $L) {
82
				$data = $this->read($id);
83
				if ($data) {
84
					$data['short_content'] = truncate(explode('<!-- pagebreak -->', $data['content'])[0]);
85
					$data['sections']      = $this->db()->qfas(
86
						"SELECT `section`
87
						FROM `$this->table_sections`
88
						WHERE `id` = $id"
89
					);
90
					$data['tags']          = $this->db()->qfas(
91
						"SELECT DISTINCT `tag`
92
						FROM `$this->table_tags`
93
						WHERE
94
							`id`	= $id AND
95
							`lang`	= '$L->clang'"
96
					);
97
					if (!$data['tags']) {
98
						$l            = $this->db()->qfs(
99
							"SELECT `lang`
100
							FROM `$this->table_tags`
101
							WHERE `id` = $id
102
							LIMIT 1"
103
						);
104
						$data['tags'] = $this->db()->qfas(
105
							"SELECT DISTINCT `tag`
106
							FROM `$this->table_tags`
107
							WHERE
108
								`id`	= $id AND
109
								`lang`	= '$l'"
110
						);
111
					}
112
					$data['tags'] = array_column(
113
						Tags::instance()->get($data['tags']),
114
						'text'
115
					);
116
				}
117
				return $data;
118
			}
119
		);
120
		$Comments = null;
121
		Event::instance()->fire(
122
			'Comments/instance',
123
			[
124
				'Comments' => &$Comments
125
			]
126
		);
127
		/**
128
		 * @var \cs\modules\Comments\Comments $Comments
129
		 */
130
		$data['comments_count'] =
131
			Config::instance()->module('Blogs')->enable_comments && $Comments
132
				? $Comments->count($data['id'])
133
				: 0;
134
		return $data;
135
	}
136
	/**
137
	 * Get data of specified post
138
	 *
139
	 * @param int|int[] $id
140
	 *
141
	 * @return array|false
142
	 */
143
	function get_as_json_ld ($id) {
144
		$post = $this->get($id);
145
		if (!$post) {
146
			return false;
147
		}
148
		return $this->post_to_jsonld($post);
149
	}
150
	/**
151
	 * @param array|array[] $post
152
	 *
153
	 * @return array
1 ignored issue
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
154
	 */
155
	function post_to_jsonld ($post) {
156
		$base_structure = [
157
			'@context' =>
158
				[
159
					'content'        => 'articleBody',
160
					'title'          => 'headline',
161
					'comments_count' => 'commentCount',
162
					'tags'           => 'keywords',
163
					'datetime'       => null,
164
					'sections_paths' => null,
165
					'tags_paths'     => null
166
				] + Json_ld::context_stub(isset($post[0]) ? $post[0] : $post)
167
		];
168
		if (isset($post[0])) {
169
			return
170
				$base_structure +
171
				[
172
					'@graph' => array_map(
173
						[$this, 'post_to_jsonld_single_post'],
174
						$post
175
					)
176
				];
177
		}
178
		return
179
			$base_structure +
180
			$this->post_to_jsonld_single_post($post);
181
	}
182
	protected function post_to_jsonld_single_post ($post) {
183
		if (preg_match_all('/<img[^>]src=["\'](.*)["\']/Uims', $post['content'], $images)) {
184
			$images = $images[1];
185
		}
186
		$Sections = Sections::instance();
187
		$sections = [];
188
		if ($post['sections'] != [0]) {
189
			$sections = array_column(
190
				$Sections->get($post['sections']),
191
				'title'
192
			);
193
		}
194
		$L            = new Language_prefix('blogs_');
195
		$base_url     = Config::instance()->base_url();
196
		$module_path  = path($L->Blogs);
197
		$section_path = "$base_url/$module_path/".path($L->section);
198
		$tag_path     = "$base_url/$module_path/".path($L->tag);
199
		$url          = "$base_url/$module_path/$post[path]:$post[id]";
200
		return
201
			[
202
				'@id'            => $url,
203
				'@type'          => 'BlogPosting',
204
				'articleSection' => $sections,
205
				'author'         => Json_ld::Person($post['user']),
206
				'datePublished'  => Json_ld::Date($post['date']),
207
				'image'          => $images,
208
				'inLanguage'     => $L->clang,
209
				'url'            => $url,
210
				'datetime'       => $L->to_locale(date($L->_datetime_long, $post['date'] ?: TIME)),
211
				'sections_paths' => array_map(
212
					function ($section) use ($section_path, $Sections) {
213
						$section = $Sections->get($section);
214
						return "$section_path/$section[full_path]";
215
					},
216
					$post['sections']
217
				),
218
				'tags_paths'     => array_map(
219
					function ($tag) use ($tag_path) {
220
						return "$tag_path/$tag";
221
					},
222
					$post['tags']
223
				)
224
			] + $post;
225
	}
226
	/**
227
	 * Get latest posts
228
	 *
229
	 * @param int $page
230
	 * @param int $number
231
	 *
232
	 * @return int[]
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
233
	 */
234
	function get_latest_posts ($page, $number) {
235
		$number = (int)$number;
236
		$from   = ($page - 1) * $number;
237
		return $this->db()->qfas(
238
			"SELECT `id`
239
			FROM `$this->table`
240
			WHERE `draft` = 0
241
			ORDER BY `date` DESC
242
			LIMIT $from, $number"
243
		) ?: [];
244
	}
245
	/**
246
	 * Get posts for section
247
	 *
248
	 * @param int $section
249
	 * @param int $page
250
	 * @param int $number
251
	 *
252
	 * @return int[]
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
253
	 */
254
	function get_for_section ($section, $page, $number) {
255
		$section = (int)$section;
256
		$number  = (int)$number;
257
		$from    = ($page - 1) * $number;
258
		return $this->db()->qfas(
259
			"SELECT `s`.`id`
260
			FROM `$this->table_sections` AS `s`
261
				LEFT JOIN `$this->table` AS `p`
262
			ON `s`.`id` = `p`.`id`
263
			WHERE
264
				`s`.`section`	= $section AND
265
				`p`.`draft`		= 0
266
			ORDER BY `p`.`date` DESC
267
			LIMIT $from, $number"
268
		) ?: [];
269
	}
270
	/**
271
	 * Get posts for tag
272
	 *
273
	 * @param int    $tag
274
	 * @param string $lang
275
	 * @param int    $page
276
	 * @param int    $number
277
	 *
278
	 * @return int[]
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
279
	 */
280
	function get_for_tag ($tag, $lang, $page, $number) {
281
		$number = (int)$number;
282
		$from   = ($page - 1) * $number;
283
		return $this->db()->qfas(
284
			[
285
				"SELECT `t`.`id`
286
				FROM `$this->table_tags` AS `t`
287
					LEFT JOIN `$this->table` AS `p`
288
				ON `t`.`id` = `p`.`id`
289
				WHERE
290
					`t`.`tag`	= '%s' AND
291
					`p`.`draft`	= 0 AND
292
					`t`.`lang`	= '%s'
293
				ORDER BY `p`.`date` DESC
294
				LIMIT $from, $number",
295
				$tag,
296
				$lang
297
			]
298
		) ?: [];
299
	}
300
	/**
301
	 * Get number of posts for tag
302
	 *
303
	 * @param int    $tag
304
	 * @param string $lang
305
	 *
306
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be array[]|integer|integer[]|string|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
307
	 */
308
	function get_for_tag_count ($tag, $lang) {
309
		return $this->db()->qfs(
310
			[
311
				"SELECT COUNT(`t`.`id`)
312
				FROM `$this->table_tags` AS `t`
313
					LEFT JOIN `$this->table` AS `p`
314
				ON `t`.`id` = `p`.`id`
315
				WHERE
316
					`t`.`tag`	= '%s' AND
317
					`p`.`draft`	= 0 AND
318
					`t`.`lang`	= '%s'",
319
				$tag,
320
				$lang
321
			]
322
		) ?: 0;
323
	}
324
	/**
325
	 * Get drafts
326
	 *
327
	 * @param int $user
328
	 * @param int $page
329
	 * @param int $number
330
	 *
331
	 * @return int[]
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|string|array? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
332
	 */
333
	function get_drafts ($user, $page, $number) {
334
		$number = (int)$number;
335
		$from   = ($page - 1) * $number;
336
		return $this->db()->qfas(
337
			[
338
				"SELECT `id`
339
				FROM `$this->table`
340
				WHERE
341
					`draft` = 1 AND
342
					`user`	= '%s'
343
				ORDER BY `date` DESC
344
				LIMIT $from, $number",
345
				$user
346
			]
347
		) ?: [];
348
	}
349
	/**
350
	 * Get number of drafts
351
	 *
352
	 * @param int $user
353
	 *
354
	 * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be array[]|integer|integer[]|string|string[]?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
355
	 */
356
	function get_drafts_count ($user) {
357
		return $this->db()->qfs(
358
			[
359
				"SELECT COUNT(`id`)
360
				FROM `$this->table`
361
				WHERE
362
					`draft` = 1 AND
363
					`user`	= '%s'",
364
				$user
365
			]
366
		) ?: 0;
367
	}
368
	/**
369
	 * Add new post
370
	 *
371
	 * @param string   $title
372
	 * @param string   $path
373
	 * @param string   $content
374
	 * @param int[]    $sections
375
	 * @param string[] $tags
376
	 * @param bool     $draft
377
	 *
378
	 * @return false|int Id of created post on success of <b>false</> on failure
1 ignored issue
show
Documentation introduced by
Should the return type not be false|integer|string?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
379
	 */
380
	function add ($title, $path, $content, $sections, $tags, $draft) {
381
		if (!$this->check_arguments($content, $sections, $tags)) {
382
			return false;
383
		}
384
		$id = $this->create(
385
			[
386
				User::instance()->id,
387
				$draft ? 0 : time(),
388
				$title,
389
				path($path ?: $title),
390
				$content,
391
				(int)(bool)$draft
392
			]
393
		);
394
		if ($id) {
395
			$this->final_updates_and_cache_cleanups($id, $sections, $tags);
396
		}
397
		return $id;
398
	}
399
	/**
400
	 * @param string   $content
401
	 * @param int[]    $sections
402
	 * @param string[] $tags
403
	 *
404
	 * @return bool
405
	 */
406
	protected function check_arguments ($content, &$sections, $tags) {
407
		if (empty($tags) || empty($content)) {
408
			return false;
409
		}
410
		$sections = array_intersect(
411
			array_keys(Sections::instance()->get_list()),
412
			$sections
413
		);
414
		return
415
			$sections && count($sections) <= Config::instance()->module('Blogs')->max_sections;
416
	}
417
	/**
418
	 * @param int      $id
419
	 * @param int[]    $sections
420
	 * @param string[] $tags
421
	 */
422
	protected function final_updates_and_cache_cleanups ($id, $sections, $tags) {
423
		$this->update_sections($id, $sections);
424
		$this->update_tags($id, $tags);
425
		$Cache = $this->cache;
426
		unset(
427
			$Cache->{"posts/$id"},
428
			$Cache->sections,
429
			$Cache->total_count
430
		);
431
	}
432
	/**
433
	 * Remove existing sections and set as specified
434
	 *
435
	 * @param int   $id
436
	 * @param int[] $sections Empty array to just remove all existing sections
437
	 */
438
	protected function update_sections ($id, $sections = []) {
439
		$this->db_prime()->q(
440
			"DELETE FROM `$this->table_sections`
441
			WHERE `id` = %d",
442
			$id
443
		);
444
		if (!$sections) {
445
			return;
446
		}
447
		$id = (int)$id;
448
		$this->db_prime()->insert(
449
			"INSERT INTO `$this->table_sections`
450
				(
451
					`id`,
452
					`section`
453
				) VALUES (
454
					$id,
455
					%d
456
				)",
457
			array_unique($sections),
458
			true
459
		);
460
	}
461
	/**
462
	 * Remove existing tags and set as specified
463
	 *
464
	 * @param int      $id
465
	 * @param string[] $tags Empty array to just remove all existing tags
466
	 */
467
	protected function update_tags ($id, $tags = []) {
468
		if (!$tags) {
469
			$this->db_prime()->q(
470
				"DELETE FROM `$this->table_tags`
471
				WHERE
472
					`id` = %d",
473
				$id
474
			);
475
			return;
476
		}
477
		$L = Language::instance();
478
		$this->db_prime()->q(
479
			"DELETE FROM `$this->table_tags`
480
			WHERE
481
				`id`	= %d AND
482
				`lang`	= '%s'",
483
			$id,
484
			$L->clang
485
		);
486
		$id = (int)$id;
487
		$this->db_prime()->insert(
488
			"INSERT INTO `$this->table_tags`
489
				(
490
					`id`,
491
					`tag`,
492
					`lang`
493
				) VALUES (
494
					$id,
495
					%d,
496
					'$L->clang'
497
				)",
498
			Tags::instance()->add($tags),
499
			true
500
		);
501
	}
502
	/**
503
	 * Set data of specified post
504
	 *
505
	 * @param int      $id
506
	 * @param string   $title
507
	 * @param string   $path
508
	 * @param string   $content
509
	 * @param int[]    $sections
510
	 * @param string[] $tags
511
	 * @param bool     $draft
512
	 *
513
	 * @return bool
514
	 */
515
	function set ($id, $title, $path, $content, $sections, $tags, $draft) {
516
		if (!$this->check_arguments($content, $sections, $tags)) {
517
			return false;
518
		}
519
		$old_data = $this->get($id);
520
		$result   = $this->update(
521
			[
522
				$id,
523
				$old_data['user'],
524
				$old_data['draft'] == 1 && $old_data['date'] == 0 && !$draft ? time() : $old_data['date'],
525
				$title,
526
				path($path ?: $title),
527
				$content,
528
				(int)(bool)$draft
529
			]
530
		);
531
		if ($result) {
532
			$this->final_updates_and_cache_cleanups($id, $sections, $tags);
533
		}
534
		return $result;
535
	}
536
	/**
537
	 * Delete specified post
538
	 *
539
	 * @param int $id
540
	 *
541
	 * @return bool
542
	 */
543
	function del ($id) {
544
		$id     = (int)$id;
545
		$result = $this->delete($id);
546
		if ($result) {
547
			$Comments = null;
548
			Event::instance()->fire(
549
				'Comments/instance',
550
				[
551
					'Comments' => &$Comments
552
				]
553
			);
554
			/**
555
			 * @var \cs\modules\Comments\Comments $Comments
556
			 */
557
			if ($Comments) {
558
				$Comments->del_all($id);
559
			}
560
			$this->final_updates_and_cache_cleanups($id, [], []);
561
		}
562
		return $result;
563
	}
564
	/**
565
	 * Get total count of posts
566
	 *
567
	 * @return int
568
	 */
569
	function get_total_count () {
570
		return $this->cache->get(
571
			'total_count',
572
			function () {
573
				return $this->db()->qfs(
574
					"SELECT COUNT(`id`)
575
					FROM `$this->table`
576
					WHERE `draft` = 0"
577
				);
578
			}
579
		);
580
	}
581
}
582