Skip to content

Commit 164f01b

Browse files
committed
Merge branch 'development' into release
2 parents f563a00 + c6d0e69 commit 164f01b

File tree

414 files changed

+6583
-1015
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

414 files changed

+6583
-1015
lines changed

.env.example.complete

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,15 @@ ALLOWED_IFRAME_HOSTS=null
359359
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
360360
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
361361

362+
# A list of the sources/hostnames that can be reached by application SSR calls.
363+
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
364+
# Host-specific functionality (usually controlled via other options) like auth
365+
# or user avatars for example, won't use this list.
366+
# Space seperated if multiple. Can use '*' as a wildcard.
367+
# Values will be compared prefix-matched, case-insensitive, against called SSR urls.
368+
# Defaults to allow all hosts.
369+
ALLOWED_SSR_HOSTS="*"
370+
362371
# The default and maximum item-counts for listing API requests.
363372
API_DEFAULT_ITEM_COUNT=100
364373
API_MAX_ITEM_COUNT=500

.github/translators.txt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,14 @@ hamidreza amini (hamidrezaamini2022) :: Persian
344344
Tomislav Kraljević (tomislav.kraljevic) :: Croatian
345345
Taygun Yıldırım (yildirimtaygun) :: Turkish
346346
robing29 :: German
347+
Bruno Eduardo de Jesus Barroso (brunoejb) :: Portuguese, Brazilian
348+
Igor V Belousov (biv) :: Russian
349+
David Bauer (davbauer) :: German
350+
Guttorm Hveem (guttormhveem) :: Norwegian Bokmal
351+
Minh Giang Truong (minhgiang1204) :: Vietnamese
352+
Ioannis Ioannides (i.ioannides) :: Greek
353+
Vadim (vadrozh) :: Russian
354+
Flip333 :: German Informal; German
355+
Paulo Henrique (paulohsantos114) :: Portuguese, Brazilian
356+
Dženan (Dzenan) :: Swedish
357+
Péter Péli (peter.peli) :: Hungarian

app/Activity/ActivityType.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ class ActivityType
2727
const BOOKSHELF_DELETE = 'bookshelf_delete';
2828

2929
const COMMENTED_ON = 'commented_on';
30+
const COMMENT_CREATE = 'comment_create';
31+
const COMMENT_UPDATE = 'comment_update';
32+
const COMMENT_DELETE = 'comment_delete';
33+
3034
const PERMISSIONS_UPDATE = 'permissions_update';
3135

3236
const REVISION_RESTORE = 'revision_restore';

app/Activity/CommentRepo.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public function create(Entity $entity, string $text, ?int $parent_id): Comment
3333
$comment->parent_id = $parent_id;
3434

3535
$entity->comments()->save($comment);
36+
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
3637
ActivityService::add(ActivityType::COMMENTED_ON, $entity);
3738

3839
return $comment;
@@ -48,6 +49,8 @@ public function update(Comment $comment, string $text): Comment
4849
$comment->html = $this->commentToHtml($text);
4950
$comment->save();
5051

52+
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
53+
5154
return $comment;
5255
}
5356

@@ -57,6 +60,8 @@ public function update(Comment $comment, string $text): Comment
5760
public function delete(Comment $comment): void
5861
{
5962
$comment->delete();
63+
64+
ActivityService::add(ActivityType::COMMENT_DELETE, $comment);
6065
}
6166

6267
/**
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Controllers;
4+
5+
use BookStack\Activity\Tools\UserEntityWatchOptions;
6+
use BookStack\App\Model;
7+
use BookStack\Entities\Models\Entity;
8+
use BookStack\Http\Controller;
9+
use Exception;
10+
use Illuminate\Http\Request;
11+
use Illuminate\Validation\ValidationException;
12+
13+
class WatchController extends Controller
14+
{
15+
public function update(Request $request)
16+
{
17+
$this->checkPermission('receive-notifications');
18+
$this->preventGuestAccess();
19+
20+
$requestData = $this->validate($request, [
21+
'level' => ['required', 'string'],
22+
]);
23+
24+
$watchable = $this->getValidatedModelFromRequest($request);
25+
$watchOptions = new UserEntityWatchOptions(user(), $watchable);
26+
$watchOptions->updateLevelByName($requestData['level']);
27+
28+
$this->showSuccessNotification(trans('activities.watch_update_level_notification'));
29+
30+
return redirect()->back();
31+
}
32+
33+
/**
34+
* @throws ValidationException
35+
* @throws Exception
36+
*/
37+
protected function getValidatedModelFromRequest(Request $request): Entity
38+
{
39+
$modelInfo = $this->validate($request, [
40+
'type' => ['required', 'string'],
41+
'id' => ['required', 'integer'],
42+
]);
43+
44+
if (!class_exists($modelInfo['type'])) {
45+
throw new Exception('Model not found');
46+
}
47+
48+
/** @var Model $model */
49+
$model = new $modelInfo['type']();
50+
if (!$model instanceof Entity) {
51+
throw new Exception('Model not an entity');
52+
}
53+
54+
$modelInstance = $model->newQuery()
55+
->where('id', '=', $modelInfo['id'])
56+
->first(['id', 'name', 'owned_by']);
57+
58+
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
59+
if (is_null($modelInstance) || $inaccessibleEntity) {
60+
throw new Exception('Model instance not found');
61+
}
62+
63+
return $modelInstance;
64+
}
65+
}

app/Activity/DispatchWebhookJob.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use BookStack\Facades\Theme;
99
use BookStack\Theming\ThemeEvents;
1010
use BookStack\Users\Models\User;
11+
use BookStack\Util\SsrUrlValidator;
1112
use Illuminate\Bus\Queueable;
1213
use Illuminate\Contracts\Queue\ShouldQueue;
1314
use Illuminate\Foundation\Bus\Dispatchable;
@@ -53,6 +54,8 @@ public function handle()
5354
$lastError = null;
5455

5556
try {
57+
(new SsrUrlValidator())->ensureAllowed($this->webhook->endpoint);
58+
5659
$response = Http::asJson()
5760
->withOptions(['allow_redirects' => ['strict' => true]])
5861
->timeout($this->webhook->timeout)

app/Activity/Models/Comment.php

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use BookStack\App\Model;
66
use BookStack\Users\Models\HasCreatorAndUpdater;
77
use Illuminate\Database\Eloquent\Factories\HasFactory;
8+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
89
use Illuminate\Database\Eloquent\Relations\MorphTo;
910

1011
/**
@@ -13,8 +14,10 @@
1314
* @property string $html
1415
* @property int|null $parent_id
1516
* @property int $local_id
17+
* @property string $entity_type
18+
* @property int $entity_id
1619
*/
17-
class Comment extends Model
20+
class Comment extends Model implements Loggable
1821
{
1922
use HasFactory;
2023
use HasCreatorAndUpdater;
@@ -30,6 +33,14 @@ public function entity(): MorphTo
3033
return $this->morphTo('entity');
3134
}
3235

36+
/**
37+
* Get the parent comment this is in reply to (if existing).
38+
*/
39+
public function parent(): BelongsTo
40+
{
41+
return $this->belongsTo(Comment::class);
42+
}
43+
3344
/**
3445
* Check if a comment has been updated since creation.
3546
*/
@@ -40,21 +51,22 @@ public function isUpdated(): bool
4051

4152
/**
4253
* Get created date as a relative diff.
43-
*
44-
* @return mixed
4554
*/
46-
public function getCreatedAttribute()
55+
public function getCreatedAttribute(): string
4756
{
4857
return $this->created_at->diffForHumans();
4958
}
5059

5160
/**
5261
* Get updated date as a relative diff.
53-
*
54-
* @return mixed
5562
*/
56-
public function getUpdatedAttribute()
63+
public function getUpdatedAttribute(): string
5764
{
5865
return $this->updated_at->diffForHumans();
5966
}
67+
68+
public function logDescriptor(): string
69+
{
70+
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
71+
}
6072
}

app/Activity/Models/Watch.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Models;
4+
5+
use BookStack\Activity\WatchLevels;
6+
use BookStack\Permissions\Models\JointPermission;
7+
use Carbon\Carbon;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Database\Eloquent\Relations\HasMany;
10+
use Illuminate\Database\Eloquent\Relations\MorphTo;
11+
12+
/**
13+
* @property int $id
14+
* @property int $user_id
15+
* @property int $watchable_id
16+
* @property string $watchable_type
17+
* @property int $level
18+
* @property Carbon $created_at
19+
* @property Carbon $updated_at
20+
*/
21+
class Watch extends Model
22+
{
23+
protected $guarded = [];
24+
25+
public function watchable(): MorphTo
26+
{
27+
return $this->morphTo();
28+
}
29+
30+
public function jointPermissions(): HasMany
31+
{
32+
return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
33+
->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');
34+
}
35+
36+
public function getLevelName(): string
37+
{
38+
return WatchLevels::levelValueToName($this->level);
39+
}
40+
41+
public function ignoring(): bool
42+
{
43+
return $this->level === WatchLevels::IGNORE;
44+
}
45+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Notifications\Handlers;
4+
5+
use BookStack\Activity\Models\Loggable;
6+
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
7+
use BookStack\Entities\Models\Entity;
8+
use BookStack\Permissions\PermissionApplicator;
9+
use BookStack\Users\Models\User;
10+
11+
abstract class BaseNotificationHandler implements NotificationHandler
12+
{
13+
/**
14+
* @param class-string<BaseActivityNotification> $notification
15+
* @param int[] $userIds
16+
*/
17+
protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void
18+
{
19+
$users = User::query()->whereIn('id', array_unique($userIds))->get();
20+
21+
foreach ($users as $user) {
22+
// Prevent sending to the user that initiated the activity
23+
if ($user->id === $initiator->id) {
24+
continue;
25+
}
26+
27+
// Prevent sending of the user does not have notification permissions
28+
if (!$user->can('receive-notifications')) {
29+
continue;
30+
}
31+
32+
// Prevent sending if the user does not have access to the related content
33+
$permissions = new PermissionApplicator($user);
34+
if (!$permissions->checkOwnableUserAccess($relatedModel, 'view')) {
35+
continue;
36+
}
37+
38+
// Send the notification
39+
$user->notify(new $notification($detail, $initiator));
40+
}
41+
}
42+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<?php
2+
3+
namespace BookStack\Activity\Notifications\Handlers;
4+
5+
use BookStack\Activity\Models\Activity;
6+
use BookStack\Activity\Models\Comment;
7+
use BookStack\Activity\Models\Loggable;
8+
use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
9+
use BookStack\Activity\Tools\EntityWatchers;
10+
use BookStack\Activity\WatchLevels;
11+
use BookStack\Entities\Models\Page;
12+
use BookStack\Settings\UserNotificationPreferences;
13+
use BookStack\Users\Models\User;
14+
15+
class CommentCreationNotificationHandler extends BaseNotificationHandler
16+
{
17+
public function handle(Activity $activity, Loggable|string $detail, User $user): void
18+
{
19+
if (!($detail instanceof Comment)) {
20+
throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
21+
}
22+
23+
// Main watchers
24+
/** @var Page $page */
25+
$page = $detail->entity;
26+
$watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
27+
$watcherIds = $watchers->getWatcherUserIds();
28+
29+
// Page owner if user preferences allow
30+
if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
31+
$userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
32+
if ($userNotificationPrefs->notifyOnOwnPageComments()) {
33+
$watcherIds[] = $page->owned_by;
34+
}
35+
}
36+
37+
// Parent comment creator if preferences allow
38+
$parentComment = $detail->parent()->first();
39+
if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
40+
$parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
41+
if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
42+
$watcherIds[] = $parentComment->created_by;
43+
}
44+
}
45+
46+
$this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
47+
}
48+
}

0 commit comments

Comments
 (0)