Skip to content

Commit

Permalink
Merge branch 'release' into vatger
Browse files Browse the repository at this point in the history
  • Loading branch information
paulhollmann committed Sep 7, 2023
2 parents 0530f90 + 3f47352 commit d9ffc33
Show file tree
Hide file tree
Showing 435 changed files with 6,701 additions and 1,020 deletions.
9 changes: 9 additions & 0 deletions .env.example.complete
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,15 @@ ALLOWED_IFRAME_HOSTS=null
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"

# A list of the sources/hostnames that can be reached by application SSR calls.
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
# Host-specific functionality (usually controlled via other options) like auth
# or user avatars for example, won't use this list.
# Space seperated if multiple. Can use '*' as a wildcard.
# Values will be compared prefix-matched, case-insensitive, against called SSR urls.
# Defaults to allow all hosts.
ALLOWED_SSR_HOSTS="*"

# The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
Expand Down
13 changes: 13 additions & 0 deletions .github/translators.txt
Original file line number Diff line number Diff line change
Expand Up @@ -344,3 +344,16 @@ hamidreza amini (hamidrezaamini2022) :: Persian
Tomislav Kraljević (tomislav.kraljevic) :: Croatian
Taygun Yıldırım (yildirimtaygun) :: Turkish
robing29 :: German
Bruno Eduardo de Jesus Barroso (brunoejb) :: Portuguese, Brazilian
Igor V Belousov (biv) :: Russian
David Bauer (davbauer) :: German
Guttorm Hveem (guttormhveem) :: Norwegian Bokmal
Minh Giang Truong (minhgiang1204) :: Vietnamese
Ioannis Ioannides (i.ioannides) :: Greek
Vadim (vadrozh) :: Russian
Flip333 :: German Informal; German
Paulo Henrique (paulohsantos114) :: Portuguese, Brazilian
Dženan (Dzenan) :: Swedish
Péter Péli (peter.peli) :: Hungarian
TWME :: Chinese Traditional
Sascha (Man-in-Black) :: German
4 changes: 4 additions & 0 deletions app/Activity/ActivityType.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ class ActivityType
const BOOKSHELF_DELETE = 'bookshelf_delete';

const COMMENTED_ON = 'commented_on';
const COMMENT_CREATE = 'comment_create';
const COMMENT_UPDATE = 'comment_update';
const COMMENT_DELETE = 'comment_delete';

const PERMISSIONS_UPDATE = 'permissions_update';

const REVISION_RESTORE = 'revision_restore';
Expand Down
5 changes: 5 additions & 0 deletions app/Activity/CommentRepo.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ public function create(Entity $entity, string $text, ?int $parent_id): Comment
$comment->parent_id = $parent_id;

$entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
ActivityService::add(ActivityType::COMMENTED_ON, $entity);

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

ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);

return $comment;
}

Expand All @@ -57,6 +60,8 @@ public function update(Comment $comment, string $text): Comment
public function delete(Comment $comment): void
{
$comment->delete();

ActivityService::add(ActivityType::COMMENT_DELETE, $comment);
}

/**
Expand Down
65 changes: 65 additions & 0 deletions app/Activity/Controllers/WatchController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<?php

namespace BookStack\Activity\Controllers;

use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Http\Controller;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;

class WatchController extends Controller
{
public function update(Request $request)
{
$this->checkPermission('receive-notifications');
$this->preventGuestAccess();

$requestData = $this->validate($request, [
'level' => ['required', 'string'],
]);

$watchable = $this->getValidatedModelFromRequest($request);
$watchOptions = new UserEntityWatchOptions(user(), $watchable);
$watchOptions->updateLevelByName($requestData['level']);

$this->showSuccessNotification(trans('activities.watch_update_level_notification'));

return redirect()->back();
}

/**
* @throws ValidationException
* @throws Exception
*/
protected function getValidatedModelFromRequest(Request $request): Entity
{
$modelInfo = $this->validate($request, [
'type' => ['required', 'string'],
'id' => ['required', 'integer'],
]);

if (!class_exists($modelInfo['type'])) {
throw new Exception('Model not found');
}

/** @var Model $model */
$model = new $modelInfo['type']();
if (!$model instanceof Entity) {
throw new Exception('Model not an entity');
}

$modelInstance = $model->newQuery()
->where('id', '=', $modelInfo['id'])
->first(['id', 'name', 'owned_by']);

$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
if (is_null($modelInstance) || $inaccessibleEntity) {
throw new Exception('Model instance not found');
}

return $modelInstance;
}
}
3 changes: 3 additions & 0 deletions app/Activity/DispatchWebhookJob.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use BookStack\Util\SsrUrlValidator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
Expand Down Expand Up @@ -53,6 +54,8 @@ public function handle()
$lastError = null;

try {
(new SsrUrlValidator())->ensureAllowed($this->webhook->endpoint);

$response = Http::asJson()
->withOptions(['allow_redirects' => ['strict' => true]])
->timeout($this->webhook->timeout)
Expand Down
26 changes: 19 additions & 7 deletions app/Activity/Models/Comment.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use BookStack\App\Model;
use BookStack\Users\Models\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;

/**
Expand All @@ -13,8 +14,10 @@
* @property string $html
* @property int|null $parent_id
* @property int $local_id
* @property string $entity_type
* @property int $entity_id
*/
class Comment extends Model
class Comment extends Model implements Loggable
{
use HasFactory;
use HasCreatorAndUpdater;
Expand All @@ -30,6 +33,14 @@ public function entity(): MorphTo
return $this->morphTo('entity');
}

/**
* Get the parent comment this is in reply to (if existing).
*/
public function parent(): BelongsTo
{
return $this->belongsTo(Comment::class);
}

/**
* Check if a comment has been updated since creation.
*/
Expand All @@ -40,21 +51,22 @@ public function isUpdated(): bool

/**
* Get created date as a relative diff.
*
* @return mixed
*/
public function getCreatedAttribute()
public function getCreatedAttribute(): string
{
return $this->created_at->diffForHumans();
}

/**
* Get updated date as a relative diff.
*
* @return mixed
*/
public function getUpdatedAttribute()
public function getUpdatedAttribute(): string
{
return $this->updated_at->diffForHumans();
}

public function logDescriptor(): string
{
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
}
}
45 changes: 45 additions & 0 deletions app/Activity/Models/Watch.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

namespace BookStack\Activity\Models;

use BookStack\Activity\WatchLevels;
use BookStack\Permissions\Models\JointPermission;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;

/**
* @property int $id
* @property int $user_id
* @property int $watchable_id
* @property string $watchable_type
* @property int $level
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class Watch extends Model
{
protected $guarded = [];

public function watchable(): MorphTo
{
return $this->morphTo();
}

public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');
}

public function getLevelName(): string
{
return WatchLevels::levelValueToName($this->level);
}

public function ignoring(): bool
{
return $this->level === WatchLevels::IGNORE;
}
}
42 changes: 42 additions & 0 deletions app/Activity/Notifications/Handlers/BaseNotificationHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace BookStack\Activity\Notifications\Handlers;

use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;

abstract class BaseNotificationHandler implements NotificationHandler
{
/**
* @param class-string<BaseActivityNotification> $notification
* @param int[] $userIds
*/
protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void
{
$users = User::query()->whereIn('id', array_unique($userIds))->get();

foreach ($users as $user) {
// Prevent sending to the user that initiated the activity
if ($user->id === $initiator->id) {
continue;
}

// Prevent sending of the user does not have notification permissions
if (!$user->can('receive-notifications')) {
continue;
}

// Prevent sending if the user does not have access to the related content
$permissions = new PermissionApplicator($user);
if (!$permissions->checkOwnableUserAccess($relatedModel, 'view')) {
continue;
}

// Send the notification
$user->notify(new $notification($detail, $initiator));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

namespace BookStack\Activity\Notifications\Handlers;

use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Comment;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
use BookStack\Activity\Tools\EntityWatchers;
use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\Page;
use BookStack\Settings\UserNotificationPreferences;
use BookStack\Users\Models\User;

class CommentCreationNotificationHandler extends BaseNotificationHandler
{
public function handle(Activity $activity, Loggable|string $detail, User $user): void
{
if (!($detail instanceof Comment)) {
throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
}

// Main watchers
/** @var Page $page */
$page = $detail->entity;
$watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
$watcherIds = $watchers->getWatcherUserIds();

// Page owner if user preferences allow
if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
$userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
if ($userNotificationPrefs->notifyOnOwnPageComments()) {
$watcherIds[] = $page->owned_by;
}
}

// Parent comment creator if preferences allow
$parentComment = $detail->parent()->first();
if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
$parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
$watcherIds[] = $parentComment->created_by;
}
}

$this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
}
}
17 changes: 17 additions & 0 deletions app/Activity/Notifications/Handlers/NotificationHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php

namespace BookStack\Activity\Notifications\Handlers;

use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Loggable;
use BookStack\Users\Models\User;

interface NotificationHandler
{
/**
* Run this handler.
* Provides the activity, related activity detail/model
* along with the user that triggered the activity.
*/
public function handle(Activity $activity, string|Loggable $detail, User $user): void;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

namespace BookStack\Activity\Notifications\Handlers;

use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\PageCreationNotification;
use BookStack\Activity\Tools\EntityWatchers;
use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;

class PageCreationNotificationHandler extends BaseNotificationHandler
{
public function handle(Activity $activity, Loggable|string $detail, User $user): void
{
if (!($detail instanceof Page)) {
throw new \InvalidArgumentException("Detail for page create notifications must be a page");
}

$watchers = new EntityWatchers($detail, WatchLevels::NEW);
$this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail, $detail);
}
}
Loading

0 comments on commit d9ffc33

Please sign in to comment.