Compare commits

...

6 Commits

Author SHA1 Message Date
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
d3ec38bee3 Removed unused function in registration service 2020-05-02 01:07:30 +01:00
Dan Brown
413cac23ae Added command to regenerate comment content 2020-05-01 23:41:47 +01:00
Dan Brown
3c26e7b727 Updated comment md rendering to be server-side 2020-05-01 23:24:11 +01:00
Dan Brown
2a2d0aa15b Fixed incorrect color code causing yellow/orange code blocks 2020-04-29 18:28:26 +01:00
17 changed files with 230 additions and 85 deletions

View File

@@ -2,9 +2,15 @@
use BookStack\Ownable;
/**
* @property string text
* @property string html
* @property int|null parent_id
* @property int local_id
*/
class Comment extends Ownable
{
protected $fillable = ['text', 'html', 'parent_id'];
protected $fillable = ['text', 'parent_id'];
protected $appends = ['created', 'updated'];
/**

View File

@@ -1,23 +1,20 @@
<?php namespace BookStack\Actions;
use BookStack\Entities\Entity;
use League\CommonMark\CommonMarkConverter;
/**
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo
{
/**
* @var \BookStack\Actions\Comment $comment
* @var Comment $comment
*/
protected $comment;
/**
* CommentRepo constructor.
* @param \BookStack\Actions\Comment $comment
*/
public function __construct(Comment $comment)
{
$this->comment = $comment;
@@ -25,65 +22,71 @@ class CommentRepo
/**
* Get a comment by ID.
* @param $id
* @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
*/
public function getById($id)
public function getById(int $id): Comment
{
return $this->comment->newQuery()->findOrFail($id);
}
/**
* Create a new comment on an entity.
* @param \BookStack\Entities\Entity $entity
* @param array $data
* @return \BookStack\Actions\Comment
*/
public function create(Entity $entity, $data = [])
public function create(Entity $entity, string $text, ?int $parent_id): Comment
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
$comment = $this->comment->newInstance();
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
return $comment;
}
/**
* Update an existing comment.
* @param \BookStack\Actions\Comment $comment
* @param array $input
* @return mixed
*/
public function update($comment, $input)
public function update(Comment $comment, string $text): Comment
{
$comment->updated_by = user()->id;
$comment->update($input);
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
$comment->save();
return $comment;
}
/**
* Delete a comment from the system.
* @param \BookStack\Actions\Comment $comment
* @return mixed
*/
public function delete($comment)
public function delete(Comment $comment)
{
return $comment->delete();
$comment->delete();
}
/**
* Convert the given comment markdown text to HTML.
*/
public function commentToHtml(string $commentText): string
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'max_nesting_level' => 10,
'allow_unsafe_links' => false,
]);
return $converter->convertToHtml($commentText);
}
/**
* Get the next local ID relative to the linked entity.
* @param \BookStack\Entities\Entity $entity
* @return int
*/
protected function getNextLocalId(Entity $entity)
protected function getNextLocalId(Entity $entity): int
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) {
return 1;
}
return $comments->local_id + 1;
return ($comments->local_id ?? 0) + 1;
}
}

View File

@@ -106,13 +106,4 @@ class RegistrationService
}
}
/**
* Alias to the UserRepo method of the same name.
* Attaches the default system role, if configured, to the given user.
*/
public function attachDefaultRole(User $user): void
{
$this->userRepo->attachDefaultRole($user);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Actions\Comment;
use BookStack\Actions\CommentRepo;
use Illuminate\Console\Command;
class RegenerateCommentContent extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-comment-content {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Regenerate the stored HTML of all comments';
/**
* @var CommentRepo
*/
protected $commentRepo;
/**
* Create a new command instance.
*/
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
}
Comment::query()->chunk(100, function ($comments) {
foreach ($comments as $comment) {
$comment->html = $this->commentRepo->commentToHtml($comment->text);
$comment->save();
}
});
\DB::setDefaultConnection($connection);
$this->comment('Comment HTML content has been regenerated');
}
}

View File

@@ -30,8 +30,6 @@ class RegeneratePermissions extends Command
/**
* Create a new command instance.
*
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
*/
public function __construct(PermissionService $permissionService)
{

View File

@@ -10,9 +10,6 @@ class CommentController extends Controller
{
protected $commentRepo;
/**
* CommentController constructor.
*/
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
@@ -23,11 +20,11 @@ class CommentController extends Controller
* Save a new comment for a Page
* @throws ValidationException
*/
public function savePageComment(Request $request, int $pageId, int $commentId = null)
public function savePageComment(Request $request, int $pageId)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
'parent_id' => 'nullable|integer',
]);
$page = Page::visible()->find($pageId);
@@ -35,8 +32,6 @@ class CommentController extends Controller
return response('Not found', 404);
}
$this->checkOwnablePermission('page-view', $page);
// Prevent adding comments to draft pages
if ($page->draft) {
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
@@ -44,7 +39,7 @@ class CommentController extends Controller
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments.comment', ['comment' => $comment]);
}
@@ -57,14 +52,13 @@ class CommentController extends Controller
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
$comment = $this->commentRepo->update($comment, $request->get('text'));
return view('comments.comment', ['comment' => $comment]);
}

View File

@@ -2,6 +2,10 @@
use BookStack\Auth\User;
/**
* @property int created_by
* @property int updated_by
*/
abstract class Ownable extends Model
{
/**

View File

@@ -16,12 +16,15 @@
"barryvdh/laravel-dompdf": "^0.8.5",
"barryvdh/laravel-snappy": "^0.4.5",
"doctrine/dbal": "^2.9",
"facade/ignition": "^1.4",
"fideloper/proxy": "^4.0",
"gathercontent/htmldiff": "^0.2.1",
"intervention/image": "^2.5",
"laravel/framework": "^6.12",
"laravel/socialite": "^4.3.2",
"league/commonmark": "^1.4",
"league/flysystem-aws-s3-v3": "^1.0",
"nunomaduro/collision": "^3.0",
"onelogin/php-saml": "^3.3",
"predis/predis": "^1.1",
"socialiteproviders/discord": "^2.0",
@@ -29,9 +32,7 @@
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
"socialiteproviders/slack": "^3.0",
"socialiteproviders/twitch": "^5.0",
"facade/ignition": "^1.4",
"nunomaduro/collision": "^3.0"
"socialiteproviders/twitch": "^5.0"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.2.8",

43
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "7836017e48e93254d6ff924b07f84597",
"content-hash": "bbe47cff4f167fd6ce7047dff4602a78",
"packages": [
{
"name": "aws/aws-sdk-php",
@@ -1775,16 +1775,16 @@
},
{
"name": "league/commonmark",
"version": "1.3.2",
"version": "1.4.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "75542a366ccbe1896ed79fcf3e8e68206d6c4257"
"reference": "9e780d972185e4f737a03bade0fd34a9e67bbf31"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/75542a366ccbe1896ed79fcf3e8e68206d6c4257",
"reference": "75542a366ccbe1896ed79fcf3e8e68206d6c4257",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/9e780d972185e4f737a03bade0fd34a9e67bbf31",
"reference": "9e780d972185e4f737a03bade0fd34a9e67bbf31",
"shasum": ""
},
"require": {
@@ -1802,7 +1802,7 @@
"github/gfm": "0.29.0",
"michelf/php-markdown": "~1.4",
"mikehaertl/php-shellcommand": "^1.4",
"phpstan/phpstan-shim": "^0.11.5",
"phpstan/phpstan": "^0.12",
"phpunit/phpunit": "^7.5",
"scrutinizer/ocular": "^1.5",
"symfony/finder": "^4.2"
@@ -1845,7 +1845,33 @@
"md",
"parser"
],
"time": "2020-03-25T19:55:28+00:00"
"funding": [
{
"url": "https://enjoy.gitstore.app/repositories/thephpleague/commonmark",
"type": "custom"
},
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://www.patreon.com/colinodell",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2020-04-24T13:39:56+00:00"
},
{
"name": "league/flysystem",
@@ -7716,5 +7742,6 @@
"platform-dev": [],
"platform-overrides": {
"php": "7.2.0"
}
},
"plugin-api-version": "1.1.0"
}

6
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,8 +1,5 @@
import MarkdownIt from "markdown-it";
import {scrollAndHighlightElement} from "../services/util";
const md = new MarkdownIt({ html: false });
class PageComments {
constructor(elem) {
@@ -68,12 +65,11 @@ class PageComments {
let text = form.querySelector('textarea').value;
let reqData = {
text: text,
html: md.render(text),
parent_id: this.parentId || null,
};
this.showLoading(form);
let commentId = this.editingComment.getAttribute('comment');
window.$http.put(window.baseUrl(`/ajax/comment/${commentId}`), reqData).then(resp => {
window.$http.put(`/ajax/comment/${commentId}`, reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
this.editingComment.innerHTML = newComment.children[0].innerHTML;
@@ -88,7 +84,7 @@ class PageComments {
deleteComment(commentElem) {
let id = commentElem.getAttribute('comment');
this.showLoading(commentElem.querySelector('[comment-content]'));
window.$http.delete(window.baseUrl(`/ajax/comment/${id}`)).then(resp => {
window.$http.delete(`/ajax/comment/${id}`).then(resp => {
commentElem.parentNode.removeChild(commentElem);
window.$events.emit('success', window.trans('entities.comment_deleted_success'));
this.updateCount();
@@ -102,11 +98,10 @@ class PageComments {
let text = this.formInput.value;
let reqData = {
text: text,
html: md.render(text),
parent_id: this.parentId || null,
};
this.showLoading(this.form);
window.$http.post(window.baseUrl(`/ajax/page/${this.pageId}/comment`), reqData).then(resp => {
window.$http.post(`/ajax/page/${this.pageId}/comment`, reqData).then(resp => {
let newComment = document.createElement('div');
newComment.innerHTML = resp.data;
let newElem = newComment.children[0];
@@ -171,17 +166,17 @@ class PageComments {
}
showLoading(formElem) {
let groups = formElem.querySelectorAll('.form-group');
for (let i = 0, len = groups.length; i < len; i++) {
groups[i].style.display = 'none';
const groups = formElem.querySelectorAll('.form-group');
for (let group of groups) {
group.style.display = 'none';
}
formElem.querySelector('.form-group.loading').style.display = 'block';
}
hideLoading(formElem) {
let groups = formElem.querySelectorAll('.form-group');
for (let i = 0, len = groups.length; i < len; i++) {
groups[i].style.display = 'block';
const groups = formElem.querySelectorAll('.form-group');
for (let group of groups) {
group.style.display = 'block';
}
formElem.querySelector('.form-group.loading').style.display = 'none';
}

View File

@@ -234,7 +234,7 @@ blockquote {
font-size: 0.84em;
border: 1px solid #DDD;
border-radius: 3px;
@include lightDark(background-color, #f8f8f8f, #2b2b2b);
@include lightDark(background-color, #f8f8f8, #2b2b2b);
@include lightDark(border-color, #DDD, #444);
}

View File

@@ -1,5 +1,7 @@
<?php namespace Tests;
use BookStack\Actions\Comment;
use BookStack\Actions\CommentRepo;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Page;
@@ -194,4 +196,26 @@ class CommandsTest extends TestCase
$this->expectException(RuntimeException::class);
$this->artisan('bookstack:update-url https://cats.example.com');
}
public function test_regenerate_comment_content_command()
{
Comment::query()->forceCreate([
'html' => 'some_old_content',
'text' => 'some_fresh_content',
]);
$this->assertDatabaseHas('comments', [
'html' => 'some_old_content',
]);
$exitCode = \Artisan::call('bookstack:regenerate-comment-content');
$this->assertTrue($exitCode === 0, 'Command executed successfully');
$this->assertDatabaseMissing('comments', [
'html' => 'some_old_content',
]);
$this->assertDatabaseHas('comments', [
'html' => "<p>some_fresh_content</p>\n",
]);
}
}

View File

@@ -42,7 +42,6 @@ class CommentTest extends TestCase
$newText = 'updated text content';
$resp = $this->putJson("/ajax/comment/$comment->id", [
'text' => $newText,
'html' => '<p>'.$newText.'</p>',
]);
$resp->assertStatus(200);
@@ -72,4 +71,46 @@ class CommentTest extends TestCase
'id' => $comment->id
]);
}
public function test_comments_converts_markdown_input_to_html()
{
$page = Page::first();
$this->asAdmin()->postJson("/ajax/page/$page->id/comment", [
'text' => '# My Title',
]);
$this->assertDatabaseHas('comments', [
'entity_id' => $page->id,
'entity_type' => $page->getMorphClass(),
'text' => '# My Title',
'html' => "<h1>My Title</h1>\n",
]);
$pageView = $this->get($page->getUrl());
$pageView->assertSee('<h1>My Title</h1>');
}
public function test_html_cannot_be_injected_via_comment_content()
{
$this->asAdmin();
$page = Page::first();
$script = '<script>const a = "script";</script>\n\n# sometextinthecomment';
$this->postJson("/ajax/page/$page->id/comment", [
'text' => $script,
]);
$pageView = $this->get($page->getUrl());
$pageView->assertDontSee($script);
$pageView->assertSee('sometextinthecomment');
$comment = $page->comments()->first();
$this->putJson("/ajax/comment/$comment->id", [
'text' => $script . 'updated',
]);
$pageView = $this->get($page->getUrl());
$pageView->assertDontSee($script);
$pageView->assertSee('sometextinthecommentupdated');
}
}

View File

@@ -1 +1 @@
v0.29.1
v0.29.2