PluginProbe
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses / 2.10.0
FluentCommunity – Ultra-Fast High-Performance Social Network, Community, LMS & Online Courses v2.10.0
2.10.0 2.10.01 2.9.1 2.9.0 2.8.1 2.8.0 2.7.7 2.7.5 2.7.0 2.6.01 2.6.0 2.5.0 2.4.01 trunk 1.0.90 1.0.91 1.0.92 1.0.93 1.0.94 1.0.95 1.0.96 1.0.97 1.0.98 1.0.99 1.1.0 All 77 releases
← All changes | app/Models/Feed.php +122 -39 2.4.012.10.0 View file →
@@ -4,15 +4,45 @@
4 4
5 5 use FluentCommunity\App\Functions\Utility;
6 6 use FluentCommunity\App\Services\FeedsHelper;
7 7 use FluentCommunity\App\Services\Helper;
8 +use FluentCommunity\App\Services\ProfileHelper;
8 9 use FluentCommunityPro\App\Models\Follow;
9 10
11 +/**
12 + * @property int $id
13 + * @property int|null $user_id
14 + * @property string|null $title
15 + * @property string|null $slug
16 + * @property string|null $message
17 + * @property string|null $message_rendered
18 + * @property string|null $type
19 + * @property string|null $content_type
20 + * @property int|null $space_id
21 + * @property string|null $privacy
22 + * @property string|null $status
23 + * @property int|null $priority
24 + * @property string|null $featured_image
25 + * @property int|null $is_sticky
26 + * @property string|null $expired_at
27 + * @property string|null $scheduled_at
28 + * @property int|null $comments_count
29 + * @property int|null $reactions_count
30 + * @property array $meta
31 + * @property string|null $created_at
32 + * @property string|null $updated_at
33 + * @property-read User|null $user
34 + * @property-read BaseSpace|null $space
35 + * @property-read \FluentCommunity\Framework\Database\Orm\Collection $comments
36 + * @property bool|null $has_user_react
37 + * @property bool|null $bookmarked
38 + * @property string|null $default_comment_sort_by
39 + */
10 40 class Feed extends Model
11 41 {
12 42 protected $table = 'fcom_posts';
13 43
14 - protected $guarded = ['id'];
44 + protected $guarded = [ 'id' ];
15 45
16 46 protected $casts = [
17 47 'comments_count' => 'int',
18 48 'reactions_count' => 'int',
@@ -39,14 +69,14 @@
39 69 'comments_count',
40 70 'reactions_count',
41 71 'meta',
42 72 'created_at',
43 - 'updated_at'
73 + 'updated_at',
44 74 ];
45 75
46 76 protected $searchable = [
47 77 'message',
48 - 'title'
78 + 'title',
49 79 ];
50 80
51 81 public static $publicColumns = [
52 82 'id',
@@ -66,17 +96,28 @@
66 96 'status',
67 97 'is_sticky',
68 98 'scheduled_at',
69 99 'comments_count',
70 - 'reactions_count'
100 + 'reactions_count',
71 101 ];
72 102
73 103 protected $appends = [
74 - 'permalink'
104 + 'permalink',
75 105 ];
76 106
77 107 public static $scopeType = 'text';
78 108
109 + /**
110 + * The types a /post/{slug} URL can serve.
111 + *
112 + * fcom_posts is shared: it also holds course_lesson, course_section and
113 + * space_page rows, and each of those owns a different route. Any lookup that
114 + * drops the type global scope to reach image and article posts must narrow to
115 + * this list, or a feed URL can answer with a lesson or a page that happens to
116 + * share the slug. Add a new feed shape here when one is introduced.
117 + */
118 + public static $feedViewTypes = ['text', 'image', 'article'];
119 +
79 120 public static function boot()
80 121 {
81 122 parent::boot();
82 123
@@ -104,11 +145,15 @@
104 145
105 146 static::deleting(function ($feed) {
106 147 Media::where('feed_id', $feed->id)
107 148 ->update([
108 - 'is_active' => 0
149 + 'is_active' => 0,
109 150 ]);
110 151 Reaction::where('object_id', $feed->id)
152 + ->where(function ($query) {
153 + $query->where('object_type', 'feed')
154 + ->orWhere('type', 'survey_vote');
155 + })
111 156 ->delete();
112 157 });
113 158
114 159 }
@@ -118,14 +163,15 @@
118 163 if ($newModel->title) {
119 164 // Remove the emojis
120 165 $title = preg_replace('/[\x{10000}-\x{10FFFF}]/u', '', $newModel->title);
121 166 // get the first 40 char from the title
122 - $title = substr($title, 0, 40);
167 + $title = mb_substr($title, 0, 40, 'UTF-8');
123 168 } else {
124 169 // get the first 25 char from the message
125 - $title = substr($newModel->message, 0, 40);
170 + $title = mb_substr((string) $newModel->message, 0, 40, 'UTF-8');
126 171 }
127 172
173 + $title = Helper::normalizeToAscii($title);
128 174 $title = remove_accents($title);
129 175
130 176 $title = strtolower($title);
131 177 // only allow alphanumeric, dash, and underscore
@@ -141,9 +187,9 @@
141 187 if ($count == 5) {
142 188 $count = time();
143 189 }
144 190 $slug = $title . '-' . $count;
145 - $count++;
191 + ++$count;
146 192 }
147 193
148 194 if (strlen($slug) <= 4) {
149 195 $slug = $slug . '-' . time();
@@ -154,9 +200,9 @@
154 200
155 201 protected static function getDefaultMeta()
156 202 {
157 203 return [
158 - 'preview_data' => null
204 + 'preview_data' => null,
159 205 ];
160 206 }
161 207
162 208 public function setMetaAttribute($value)
@@ -171,9 +217,9 @@
171 217 if (!$meta) {
172 218 $meta = [];
173 219 }
174 220
175 - return $meta;
221 + return Helper::sanitizeStoredMediaPreview($meta);
176 222 }
177 223
178 224 public function scopeSearchBy($query, $search, $in = [])
179 225 {
@@ -181,9 +227,9 @@
181 227 return $query;
182 228 }
183 229
184 230 if (!$in || !is_array($in)) {
185 - $in = ['post_content'];
231 + $in = [ 'post_content' ];
186 232 }
187 233
188 234 $fields = $this->searchable;
189 235 $query->where(function ($query) use ($fields, $search, $in) {
@@ -202,9 +248,9 @@
202 248 $query->orWhereHas('comments', function ($q) use ($search) {
203 249 return $q->where('message', 'LIKE', "%$search%");
204 250 });
205 251 }
206 - } else if ($in && in_array('post_comments', $in)) {
252 + } elseif ($in && in_array('post_comments', $in)) {
207 253 $query->whereHas('comments', function ($q) use ($search) {
208 254 return $q->where('message', 'LIKE', "%$search%");
209 255 });
210 256 }
@@ -215,17 +261,19 @@
215 261
216 262 public function scopeByUserAccess($query, $userId)
217 263 {
218 264 if ($userId) {
219 - return $query->where('user_id', $userId)->orWhereNull('space_id')
220 - ->orWhereHas('space', function ($q) use ($userId) {
221 - $spaceIds = get_user_meta($userId, '_fcom_space_ids', true);
222 - if ($spaceIds) {
223 - $q->whereIn('id', $spaceIds);
224 - return $q;
225 - }
226 - return $q->where('privacy', 'public');
227 - });
265 + return $query->where(function ($subQuery) use ($userId) {
266 + $subQuery->where('user_id', $userId)->orWhereNull('space_id')
267 + ->orWhereHas('space', function ($q) use ($userId) {
268 + $spaceIds = get_user_meta($userId, '_fcom_space_ids', true);
269 + if ($spaceIds) {
270 + $q->whereIn('id', $spaceIds);
271 + return $q;
272 + }
273 + return $q->where('privacy', 'public');
274 + });
275 + });
228 276 }
229 277
230 278 return $query->where(function ($q) {
231 279 $q->whereNull('space_id')
@@ -244,16 +292,15 @@
244 292 if (
245 293 $user->hasCommunityModeratorAccess() ||
246 294 ($space && $user->hasSpacePermission('edit_any_feed', $space))
247 295 ) {
248 - return $query->whereIn('status', ['published', 'pending']);
296 + return $query->whereIn('status', [ 'published', 'pending' ]);
249 297 }
250 298
251 - // This is a normal User.
252 - return $query->where(function ($q) use ($user) {
253 - $q->where('status', 'published')
254 - ->orWhere(function ($q) use ($user) {
255 - $q->where('status', 'pending')
299 + return $query->where(function ($statusQuery) use ($user) {
300 + $statusQuery->where('status', 'published')
301 + ->orWhere(function ($subQuery) use ($user) {
302 + $subQuery->where('status', 'pending')
256 303 ->where('user_id', $user->ID);
257 304 });
258 305 });
259 306 }
@@ -412,8 +459,44 @@
412 459 return $this->hasMany(Reaction::class, 'object_id', 'id')
413 460 ->where('object_type', 'feed');
414 461 }
415 462
463 + /**
464 + * Eager-load closures for rendering a feed with its public relations
465 + * (author, moderation-scoped comments, space, top reactions, topics).
466 + */
467 + public static function withPublicRelations($currentUserModel, $space = null)
468 + {
469 + return [
470 + 'xprofile' => function ($q) {
471 + $q->select(ProfileHelper::getXProfilePublicFields());
472 + },
473 + 'comments' => function ($q) use ($currentUserModel, $space) {
474 + $q->byContentModerationAccessStatus($currentUserModel, $space)
475 + ->with(['xprofile' => function ($q) {
476 + $q->select(ProfileHelper::getXProfilePublicFields());
477 + }])
478 + ->whereHas('xprofile', function ($q) {
479 + $q->where('status', 'active');
480 + });
481 + },
482 + 'space' => function ($q) {
483 + $q->select(['id', 'title', 'slug', 'type', 'settings']);
484 + },
485 + 'reactions' => function ($q) {
486 + $q->with(['xprofile' => function ($query) {
487 + $query->select(['user_id', 'avatar', 'display_name']);
488 + }])
489 + ->where('type', 'like')
490 + ->limit(3);
491 + },
492 + 'terms' => function ($q) {
493 + $q->select(['title', 'slug'])
494 + ->where('taxonomy_name', 'post_topic');
495 + }
496 + ];
497 + }
498 +
416 499 // New Relationship: Follow records where this post's user_id is the followed_id
417 500 public function follows()
418 501 {
419 502 return $this->hasMany(Follow::class, 'followed_id', 'user_id');
@@ -435,9 +518,9 @@
435 518 if (!$userId) {
436 519 return false;
437 520 }
438 521
439 - return Reaction::select(['id'])
522 + return Reaction::select([ 'id' ])
440 523 ->where('object_id', $this->id)
441 524 ->where('object_type', 'feed')
442 525 ->where('user_id', $userId)
443 526 ->where('type', $type)
@@ -548,9 +631,9 @@
548 631 Meta::create([
549 632 'object_id' => $this->id,
550 633 'object_type' => 'feed',
551 634 'meta_key' => $key, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
552 - 'value' => $value
635 + 'value' => $value,
553 636 ]);
554 637 }
555 638
556 639 return true;
@@ -601,10 +684,10 @@
601 684 return [
602 685 'name' => 'view_lesson',
603 686 'params' => [
604 687 'course_slug' => $this->space ? $this->space->slug : 'uknown',
605 - 'lesson_slug' => $this->slug
606 - ]
688 + 'lesson_slug' => $this->slug,
689 + ],
607 690 ];
608 691 }
609 692
610 693 if ($this->space_id) {
@@ -611,17 +694,17 @@
611 694 $route = [
612 695 'name' => 'space_feed',
613 696 'params' => [
614 697 'space' => $this->space->slug,
615 - 'feed_slug' => $this->slug
616 - ]
698 + 'feed_slug' => $this->slug,
699 + ],
617 700 ];
618 701 } else {
619 702 $route = [
620 703 'name' => 'single_feed',
621 704 'params' => [
622 - 'feed_slug' => $this->slug
623 - ]
705 + 'feed_slug' => $this->slug,
706 + ],
624 707 ];
625 708 }
626 709
627 710 return $route;
@@ -658,9 +741,9 @@
658 741
659 742 $feedHtml = $this->message_rendered;
660 743 $feedHtml .= FeedsHelper::getMediaHtml($this->meta, $postPermalink);
661 744
662 - $buttonText = $buttonText ?: __('Join the conversation', 'fluent-community');
745 + $buttonText = $buttonText ? $buttonText : __('Join the conversation', 'fluent-community');
663 746
664 747 $emailComposer->addBlock('post_boxed_content', $feedHtml, [
665 748 'user' => $this->user,
666 749 'title' => $this->title,
@@ -665,13 +748,13 @@
665 748 'user' => $this->user,
666 749 'title' => $this->title,
667 750 'permalink' => $postPermalink,
668 751 'space_name' => $this->space ? $this->space->title : __('Community', 'fluent-community'),
669 - 'is_single' => true
752 + 'is_single' => true,
670 753 ]);
671 754
672 755 $emailComposer->addBlock('button', $buttonText, [
673 - 'link' => $postPermalink
756 + 'link' => $postPermalink,
674 757 ]);
675 758
676 759 $emailComposer->setDefaultLogo();
677 760