1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace App\Models\Forum;
7
8use App\Casts\TimestampOrZero;
9use App\Models\User;
10use Carbon\Carbon;
11use Illuminate\Database\Eloquent\Builder;
12
13/**
14 * @property bool $allow_topic_covers
15 * @property ForumCover $cover
16 * @property int $display_on_index
17 * @property int $enable_icons
18 * @property bool $enable_indexing
19 * @property int $enable_prune
20 * @property bool $enable_sigs
21 * @property string $forum_desc
22 * @property string $forum_desc_bitfield
23 * @property int $forum_desc_options
24 * @property string $forum_desc_uid
25 * @property int $forum_flags
26 * @property int $forum_id
27 * @property string $forum_image
28 * @property int $forum_last_post_id
29 * @property string $forum_last_post_subject
30 * @property \Carbon\Carbon|null $forum_last_post_time
31 * @property string $forum_last_poster_colour
32 * @property int $forum_last_poster_id
33 * @property string $forum_last_poster_name
34 * @property string $forum_link
35 * @property string $forum_name
36 * @property mixed $forum_parents
37 * @property string $forum_password
38 * @property int $forum_posts
39 * @property string $forum_rules
40 * @property string $forum_rules_bitfield
41 * @property string $forum_rules_link
42 * @property int $forum_rules_options
43 * @property string $forum_rules_uid
44 * @property int $forum_status
45 * @property int $forum_style
46 * @property int $forum_topics
47 * @property int $forum_topics_per_page
48 * @property int $forum_topics_real
49 * @property int $forum_type
50 * @property Post $lastPost
51 * @property int $left_id
52 * @property array|null $moderator_groups
53 * @property static $parentForum
54 * @property int $parent_id
55 * @property int $prune_days
56 * @property int $prune_freq
57 * @property int $prune_next
58 * @property int $prune_viewed
59 * @property int $right_id
60 * @property \Illuminate\Database\Eloquent\Collection $subforums static
61 * @property \Illuminate\Database\Eloquent\Collection $topics Topic
62 */
63class Forum extends Model
64{
65 public $timestamps = false;
66
67 protected $casts = [
68 'allow_topic_covers' => 'boolean',
69 'enable_indexing' => 'boolean',
70 'enable_sigs' => 'boolean',
71 'forum_last_post_time' => TimestampOrZero::class,
72 'moderator_groups' => 'array',
73 ];
74 protected $primaryKey = 'forum_id';
75 protected $table = 'phpbb_forums';
76
77 public static function lastTopics($forum = null)
78 {
79 $forumForLastTopic = static
80 ::select('forum_id', 'parent_id', 'forum_parents', 'forum_last_post_id')
81 ->with('lastPost.topic');
82
83 if ($forum !== null) {
84 $forumForLastTopic->whereIn('forum_id', $forum->allSubforums());
85 }
86
87 foreach ($forumForLastTopic->get() as $forum) {
88 if ($forum->lastPost === null) {
89 continue;
90 }
91
92 $topic = $forum->lastPost->topic;
93
94 if ($topic === null) {
95 continue;
96 }
97
98 $forumIds = array_keys($forum->forum_parents);
99 $forumIds[] = $forum->getKey();
100
101 foreach ($forumIds as $forumId) {
102 if (!isset($lastTopics[$forumId]) || $topic->topic_last_post_time > $lastTopics[$forumId]->topic_last_post_time) {
103 $lastTopics[$forumId] = $topic;
104 }
105 }
106 }
107
108 return $lastTopics ?? [];
109 }
110
111 public static function markAllAsRead(User $user)
112 {
113 $user->update(['user_lastmark' => Carbon::now()]);
114 ForumTrack::where('user_id', $user->getKey())->delete();
115 TopicTrack::where('user_id', $user->getKey())->delete();
116 }
117
118 public function categorySlug()
119 {
120 return 'category-'.str_slug($this->category());
121 }
122
123 public function allSubforums($forum_ids = null, $new_forum_ids = null)
124 {
125 if ($forum_ids === null) {
126 $forum_ids = $new_forum_ids = [$this->forum_id];
127 }
128 $new_forum_ids = model_pluck(static::whereIn('parent_id', $new_forum_ids), 'forum_id');
129
130 $new_forum_ids = array_map('intval', $new_forum_ids);
131 $forum_ids = array_merge($forum_ids, $new_forum_ids);
132
133 if (count($new_forum_ids) === 0) {
134 return $forum_ids;
135 } else {
136 return $this->allSubforums($forum_ids, $new_forum_ids);
137 }
138 }
139
140 public function categoryId()
141 {
142 if ($this->forum_parents) {
143 return array_keys($this->forum_parents)[0];
144 } else {
145 return $this->forum_id;
146 }
147 }
148
149 public function category()
150 {
151 if ($this->forum_parents) {
152 return array_values($this->forum_parents)[0][0];
153 } else {
154 return $this->forum_name;
155 }
156 }
157
158 public function topics()
159 {
160 return $this->hasMany(Topic::class);
161 }
162
163 public function parentForum()
164 {
165 return $this->belongsTo(static::class, 'parent_id');
166 }
167
168 public function subforums()
169 {
170 return $this->hasMany(static::class, 'parent_id')->orderBy('left_id');
171 }
172
173 public function lastPost()
174 {
175 return $this->belongsTo(Post::class, 'forum_last_post_id', 'post_id');
176 }
177
178 public function cover()
179 {
180 return $this->hasOne(ForumCover::class);
181 }
182
183 public function scopeDisplayList($query)
184 {
185 $query->orderBy('left_id');
186 }
187
188 public function scopeSearchable(Builder $query): Builder
189 {
190 return $query->where('enable_indexing', true);
191 }
192
193 public function setForumParentsAttribute($value)
194 {
195 $this->attributes['forum_parents'] = $value === null || count($value) === 0 ? '' : serialize($value);
196 }
197
198 /**
199 * Returns array which keys are id of this forum's parents and values are
200 * their names and types. Sorted from topmost parent to immediate parent.
201 *
202 * This method isn't intended to be directly called but through Laravel's
203 * attribute accessor method (in this case, `$forum->forum_parents`)
204 *
205 * warning: don't access this attribute (forum_parents) without selecting
206 * parent_id otherwise returned value may be wrong.
207 *
208 * @param string $value
209 * @return array
210 */
211 public function getForumParentsAttribute($value)
212 {
213 if ($this->parent_id === 0) {
214 return [];
215 }
216
217 if (!present($value) && $this->parentForum !== null) {
218 $parentsArray = $this->parentForum->forum_parents;
219 $parentsArray[$this->parentForum->forum_id] = [
220 $this->parentForum->forum_name,
221 $this->parentForum->forum_type,
222 ];
223
224 $this->update(['forum_parents' => $parentsArray]);
225
226 return $parentsArray;
227 } else {
228 return unserialize($value);
229 }
230 }
231
232 public function getForumLastPosterColourAttribute($value)
233 {
234 if (present($value)) {
235 return "#{$value}";
236 }
237 }
238
239 public function setForumLastPosterColourAttribute($value)
240 {
241 // also functions for casting null to string
242 $this->attributes['forum_last_poster_colour'] = ltrim($value, '#');
243 }
244
245 // feature forum shall have extra features like sorting and voting
246 public function isFeatureForum()
247 {
248 $id = $GLOBALS['cfg']['osu']['forum']['feature_forum_id'];
249
250 return $this->forum_id === $id || isset($this->forum_parents[$id]);
251 }
252
253 public function isHelpForum()
254 {
255 return $this->forum_id === $GLOBALS['cfg']['osu']['forum']['help_forum_id'];
256 }
257
258 public function topicsAdded($count)
259 {
260 $this->getConnection()->transaction(function () use ($count) {
261 $this->update([
262 'forum_topics' => db_unsigned_increment('forum_topics', $count),
263 'forum_topics_real' => db_unsigned_increment('forum_topics_real', $count),
264 ]);
265 });
266 }
267
268 public function postsAdded($count)
269 {
270 $this->getConnection()->transaction(function () use ($count) {
271 $this->fill([
272 'forum_posts' => db_unsigned_increment('forum_posts', $count),
273 ]);
274 $this->setLastPostCache();
275
276 $this->save();
277 });
278 }
279
280 public function refreshCache()
281 {
282 $this->getConnection()->transaction(function () {
283 $this->setTopicsCountCache();
284 $this->setPostCountCache();
285 $this->setLastPostCache();
286
287 $this->saveOrExplode();
288 });
289 }
290
291 public function currentDepth()
292 {
293 return count($this->forum_parents);
294 }
295
296 public function setTopicsCountCache()
297 {
298 $this->forum_topics_real = $this->topics()->count();
299 $this->forum_topics = $this->topics()->where('topic_approved', true)->count();
300 }
301
302 public function setPostCountCache()
303 {
304 $postCount = $this->forum_topics;
305 $postCount += $this->topics()->sum('topic_replies');
306
307 $this->forum_posts = $postCount;
308 }
309
310 public function setLastPostCache()
311 {
312 $lastTopic = Topic
313 ::whereIn('forum_id', $this->allSubforums())
314 ->orderBy('topic_last_post_time', 'DESC')
315 ->first();
316
317 if ($lastTopic === null) {
318 $this->forum_last_post_id = 0;
319 $this->forum_last_post_time = null;
320 $this->forum_last_post_subject = '';
321 $this->forum_last_poster_id = 0;
322 $this->forum_last_poster_name = '';
323 $this->forum_last_poster_colour = '';
324 } else {
325 $this->forum_last_post_id = $lastTopic->topic_last_post_id;
326 $this->forum_last_post_time = $lastTopic->topic_last_post_time;
327 $this->forum_last_post_subject = $lastTopic->topic_title;
328 $this->forum_last_poster_id = $lastTopic->topic_last_poster_id;
329 $this->forum_last_poster_name = $lastTopic->topic_last_poster_name;
330 $this->forum_last_poster_colour = $lastTopic->topic_last_poster_colour;
331 }
332 }
333
334 public function isOpen()
335 {
336 return $this->forum_type === 1;
337 }
338
339 public function markAsRead(User $user, bool $recursive = false)
340 {
341 $forumIds = [$this->getKey()];
342
343 if ($recursive) {
344 $forums = static::all();
345
346 foreach ($forums as $forum) {
347 if (isset($forum->forum_parents[$this->getKey()])) {
348 $forumIds[] = $forum->getKey();
349 }
350 }
351 }
352
353 $this->getConnection()->transaction(function () use ($forumIds, $user) {
354 foreach ($forumIds as $forumId) {
355 $forumTrack = ForumTrack::firstOrNew([
356 'user_id' => $user->getKey(),
357 'forum_id' => $forumId,
358 ]);
359 $forumTrack->mark_time = Carbon::now();
360 $forumTrack->save();
361 }
362
363 TopicTrack::where('user_id', $user->getKey())->whereIn('forum_id', $forumIds)->delete();
364 });
365 }
366}