Skip to content

Commit 04f7a7d

Browse files
committed
Merge branch 'master' into release
2 parents c10d2a1 + 0fb1fc8 commit 04f7a7d

File tree

129 files changed

+4422
-1637
lines changed

Some content is hidden

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

129 files changed

+4422
-1637
lines changed

.gitignore

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ Homestead.yaml
1010
/public/bower
1111
/storage/images
1212
_ide_helper.php
13-
/storage/debugbar
13+
/storage/debugbar
14+
.phpstorm.meta.php
15+
yarn.lock

app/Attachment.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php namespace BookStack;
2+
3+
4+
class Attachment extends Ownable
5+
{
6+
protected $fillable = ['name', 'order'];
7+
8+
/**
9+
* Get the downloadable file name for this upload.
10+
* @return mixed|string
11+
*/
12+
public function getFileName()
13+
{
14+
if (str_contains($this->name, '.')) return $this->name;
15+
return $this->name . '.' . $this->extension;
16+
}
17+
18+
/**
19+
* Get the page this file was uploaded to.
20+
* @return Page
21+
*/
22+
public function page()
23+
{
24+
return $this->belongsTo(Page::class, 'uploaded_to');
25+
}
26+
27+
/**
28+
* Get the url of this file.
29+
* @return string
30+
*/
31+
public function getUrl()
32+
{
33+
return baseUrl('/attachments/' . $this->id);
34+
}
35+
36+
}

app/Book.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@ class Book extends Entity
1313
public function getUrl($path = false)
1414
{
1515
if ($path !== false) {
16-
return baseUrl('/books/' . $this->slug . '/' . trim($path, '/'));
16+
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
1717
}
18-
return baseUrl('/books/' . $this->slug);
18+
return baseUrl('/books/' . urlencode($this->slug));
1919
}
2020

2121
/*

app/Chapter.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ public function getUrl($path = false)
3232
{
3333
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
3434
if ($path !== false) {
35-
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug . '/' . trim($path, '/'));
35+
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
3636
}
37-
return baseUrl('/books/' . $bookSlug. '/chapter/' . $this->slug);
37+
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
3838
}
3939

4040
/**

app/EmailConfirmation.php

Lines changed: 0 additions & 16 deletions
This file was deleted.

app/Entity.php

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,18 +162,21 @@ public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
162162
$exactTerms = [];
163163
$fuzzyTerms = [];
164164
$search = static::newQuery();
165+
165166
foreach ($terms as $key => $term) {
166-
$safeTerm = htmlentities($term, ENT_QUOTES);
167-
$safeTerm = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $safeTerm);
168-
if (preg_match('/&quot;.*?&quot;/', $safeTerm) || is_numeric($safeTerm)) {
169-
$safeTerm = preg_replace('/^"(.*?)"$/', '$1', $term);
170-
$exactTerms[] = '%' . $safeTerm . '%';
167+
$term = htmlentities($term, ENT_QUOTES);
168+
$term = preg_replace('/[+\-><\(\)~*\"@]+/', ' ', $term);
169+
if (preg_match('/&quot;.*?&quot;/', $term) || is_numeric($term)) {
170+
$term = str_replace('&quot;', '', $term);
171+
$exactTerms[] = '%' . $term . '%';
171172
} else {
172-
$safeTerm = '' . $safeTerm . '*';
173-
if (trim($safeTerm) !== '*') $fuzzyTerms[] = $safeTerm;
173+
$term = '' . $term . '*';
174+
if ($term !== '*') $fuzzyTerms[] = $term;
174175
}
175176
}
176-
$isFuzzy = count($exactTerms) === 0 || count($fuzzyTerms) > 0;
177+
178+
$isFuzzy = count($exactTerms) === 0 && count($fuzzyTerms) > 0;
179+
177180

178181
// Perform fulltext search if relevant terms exist.
179182
if ($isFuzzy) {
@@ -193,6 +196,7 @@ public function fullTextSearchQuery($fieldsToSearch, $terms, $wheres = [])
193196
}
194197
});
195198
}
199+
196200
$orderBy = $isFuzzy ? 'title_relevance' : 'updated_at';
197201

198202
// Add additional where terms

app/Events/Event.php

Lines changed: 0 additions & 8 deletions
This file was deleted.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php namespace BookStack\Exceptions;
2+
3+
4+
class FileUploadException extends PrettyException {}

app/Exceptions/Handler.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,20 @@ protected function getOriginalMessage(Exception $e) {
8787
} while ($e = $e->getPrevious());
8888
return $message;
8989
}
90+
91+
/**
92+
* Convert an authentication exception into an unauthenticated response.
93+
*
94+
* @param \Illuminate\Http\Request $request
95+
* @param \Illuminate\Auth\AuthenticationException $exception
96+
* @return \Illuminate\Http\Response
97+
*/
98+
protected function unauthenticated($request, AuthenticationException $exception)
99+
{
100+
if ($request->expectsJson()) {
101+
return response()->json(['error' => 'Unauthenticated.'], 401);
102+
}
103+
104+
return redirect()->guest('login');
105+
}
90106
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
<?php namespace BookStack\Http\Controllers;
2+
3+
use BookStack\Exceptions\FileUploadException;
4+
use BookStack\Attachment;
5+
use BookStack\Repos\PageRepo;
6+
use BookStack\Services\AttachmentService;
7+
use Illuminate\Http\Request;
8+
9+
class AttachmentController extends Controller
10+
{
11+
protected $attachmentService;
12+
protected $attachment;
13+
protected $pageRepo;
14+
15+
/**
16+
* AttachmentController constructor.
17+
* @param AttachmentService $attachmentService
18+
* @param Attachment $attachment
19+
* @param PageRepo $pageRepo
20+
*/
21+
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
22+
{
23+
$this->attachmentService = $attachmentService;
24+
$this->attachment = $attachment;
25+
$this->pageRepo = $pageRepo;
26+
parent::__construct();
27+
}
28+
29+
30+
/**
31+
* Endpoint at which attachments are uploaded to.
32+
* @param Request $request
33+
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
34+
*/
35+
public function upload(Request $request)
36+
{
37+
$this->validate($request, [
38+
'uploaded_to' => 'required|integer|exists:pages,id',
39+
'file' => 'required|file'
40+
]);
41+
42+
$pageId = $request->get('uploaded_to');
43+
$page = $this->pageRepo->getById($pageId, true);
44+
45+
$this->checkPermission('attachment-create-all');
46+
$this->checkOwnablePermission('page-update', $page);
47+
48+
$uploadedFile = $request->file('file');
49+
50+
try {
51+
$attachment = $this->attachmentService->saveNewUpload($uploadedFile, $pageId);
52+
} catch (FileUploadException $e) {
53+
return response($e->getMessage(), 500);
54+
}
55+
56+
return response()->json($attachment);
57+
}
58+
59+
/**
60+
* Update an uploaded attachment.
61+
* @param int $attachmentId
62+
* @param Request $request
63+
* @return mixed
64+
*/
65+
public function uploadUpdate($attachmentId, Request $request)
66+
{
67+
$this->validate($request, [
68+
'uploaded_to' => 'required|integer|exists:pages,id',
69+
'file' => 'required|file'
70+
]);
71+
72+
$pageId = $request->get('uploaded_to');
73+
$page = $this->pageRepo->getById($pageId, true);
74+
$attachment = $this->attachment->findOrFail($attachmentId);
75+
76+
$this->checkOwnablePermission('page-update', $page);
77+
$this->checkOwnablePermission('attachment-create', $attachment);
78+
79+
if (intval($pageId) !== intval($attachment->uploaded_to)) {
80+
return $this->jsonError('Page mismatch during attached file update');
81+
}
82+
83+
$uploadedFile = $request->file('file');
84+
85+
try {
86+
$attachment = $this->attachmentService->saveUpdatedUpload($uploadedFile, $attachment);
87+
} catch (FileUploadException $e) {
88+
return response($e->getMessage(), 500);
89+
}
90+
91+
return response()->json($attachment);
92+
}
93+
94+
/**
95+
* Update the details of an existing file.
96+
* @param $attachmentId
97+
* @param Request $request
98+
* @return Attachment|mixed
99+
*/
100+
public function update($attachmentId, Request $request)
101+
{
102+
$this->validate($request, [
103+
'uploaded_to' => 'required|integer|exists:pages,id',
104+
'name' => 'required|string|min:1|max:255',
105+
'link' => 'url|min:1|max:255'
106+
]);
107+
108+
$pageId = $request->get('uploaded_to');
109+
$page = $this->pageRepo->getById($pageId, true);
110+
$attachment = $this->attachment->findOrFail($attachmentId);
111+
112+
$this->checkOwnablePermission('page-update', $page);
113+
$this->checkOwnablePermission('attachment-create', $attachment);
114+
115+
if (intval($pageId) !== intval($attachment->uploaded_to)) {
116+
return $this->jsonError('Page mismatch during attachment update');
117+
}
118+
119+
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
120+
return $attachment;
121+
}
122+
123+
/**
124+
* Attach a link to a page.
125+
* @param Request $request
126+
* @return mixed
127+
*/
128+
public function attachLink(Request $request)
129+
{
130+
$this->validate($request, [
131+
'uploaded_to' => 'required|integer|exists:pages,id',
132+
'name' => 'required|string|min:1|max:255',
133+
'link' => 'required|url|min:1|max:255'
134+
]);
135+
136+
$pageId = $request->get('uploaded_to');
137+
$page = $this->pageRepo->getById($pageId, true);
138+
139+
$this->checkPermission('attachment-create-all');
140+
$this->checkOwnablePermission('page-update', $page);
141+
142+
$attachmentName = $request->get('name');
143+
$link = $request->get('link');
144+
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
145+
146+
return response()->json($attachment);
147+
}
148+
149+
/**
150+
* Get the attachments for a specific page.
151+
* @param $pageId
152+
* @return mixed
153+
*/
154+
public function listForPage($pageId)
155+
{
156+
$page = $this->pageRepo->getById($pageId, true);
157+
$this->checkOwnablePermission('page-view', $page);
158+
return response()->json($page->attachments);
159+
}
160+
161+
/**
162+
* Update the attachment sorting.
163+
* @param $pageId
164+
* @param Request $request
165+
* @return mixed
166+
*/
167+
public function sortForPage($pageId, Request $request)
168+
{
169+
$this->validate($request, [
170+
'files' => 'required|array',
171+
'files.*.id' => 'required|integer',
172+
]);
173+
$page = $this->pageRepo->getById($pageId);
174+
$this->checkOwnablePermission('page-update', $page);
175+
176+
$attachments = $request->get('files');
177+
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
178+
return response()->json(['message' => 'Attachment order updated']);
179+
}
180+
181+
/**
182+
* Get an attachment from storage.
183+
* @param $attachmentId
184+
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
185+
*/
186+
public function get($attachmentId)
187+
{
188+
$attachment = $this->attachment->findOrFail($attachmentId);
189+
$page = $this->pageRepo->getById($attachment->uploaded_to);
190+
$this->checkOwnablePermission('page-view', $page);
191+
192+
if ($attachment->external) {
193+
return redirect($attachment->path);
194+
}
195+
196+
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
197+
return response($attachmentContents, 200, [
198+
'Content-Type' => 'application/octet-stream',
199+
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
200+
]);
201+
}
202+
203+
/**
204+
* Delete a specific attachment in the system.
205+
* @param $attachmentId
206+
* @return mixed
207+
*/
208+
public function delete($attachmentId)
209+
{
210+
$attachment = $this->attachment->findOrFail($attachmentId);
211+
$this->checkOwnablePermission('attachment-delete', $attachment);
212+
$this->attachmentService->deleteFile($attachment);
213+
return response()->json(['message' => 'Attachment deleted']);
214+
}
215+
}

0 commit comments

Comments
 (0)