Compare commits

...

284 Commits

Author SHA1 Message Date
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
a7e3c26fe3 Fixed markdown content on revision restore
Closes #2496
2021-02-06 14:14:38 +00:00
Dan Brown
37de4e2e0a Added test for markdown page revision restore
Also added md change detection in revision saving.
2021-02-06 13:51:05 +00:00
Dan Brown
61a911dd39 Removed "isA" usages from trashcan 2021-02-06 13:29:39 +00:00
Aleksandr Sazhin
cc5d0ef4cf Update TrashCan.php
bookshelf
2021-02-06 13:23:12 +00:00
Dan Brown
7843d8f054 Added recycle-bin test to cover type deletions 2021-02-06 13:22:31 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
d547ed4a6b Updated laravel/framework to latest 6.x version 2021-02-02 20:56:19 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
d33f136660 Updated translator attribution before release v0.31.4 2021-01-16 17:49:56 +00:00
Dan Brown
173dad345e New Crowdin updates (#2482)
* New translations entities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)
2021-01-16 17:45:59 +00:00
Dan Brown
47b0eb6324 Updated framework and other php deps 2021-01-16 17:45:04 +00:00
Dan Brown
c35c37008d Added imagetools plugin back in
For #2493
2021-01-16 17:39:30 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
ee400eece6 New Crowdin updates (#2469)
* New translations settings.php (Turkish)

* New translations entities.php (Turkish)

* New translations settings.php (Turkish)

* New translations activities.php (Turkish)

* New translations validation.php (Turkish)
2021-01-10 23:28:33 +00:00
Dan Brown
da7c686541 Made books and shelf listing views slightly more efficient 2021-01-10 23:12:51 +00:00
Dan Brown
d0a7a8b890 Improved some query efficiencies on user list 2021-01-10 23:02:30 +00:00
Dan Brown
28c706fee3 Added strikethrough support to back-end md rendering
Needed to tweak the default library strikethrough extension
so that it uses the same element as front-end.
Added testing to cover.
For #2470.
2021-01-10 23:01:11 +00:00
Dan Brown
0e799a3857 Updated version and assets for release v0.31.2 2021-01-10 14:05:16 +00:00
Dan Brown
b91d6e2bfa Merge branch 'master' into release 2021-01-10 14:04:59 +00:00
Dan Brown
44315233d7 Updated translator attribution before release v0.31.2 2021-01-10 14:04:38 +00:00
Dan Brown
91efa601be New Crowdin updates (#2464)
* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Russian)

* New translations settings.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations activities.php (Russian)
2021-01-10 13:44:29 +00:00
Dan Brown
18f86fbf9b Made recycle-bin settings navbar full width
For #2468
2021-01-10 13:36:46 +00:00
Dan Brown
e5a96b0cb0 Added test case for avatar failed fetch
Fixed non-imported log issue while there.
For #2449
2021-01-10 13:29:13 +00:00
Dan Brown
526be33ab2 Fixed page copying not retaining content
Was when there was no markdown content.
Added tests to cover both HTML and markdown scenarios.
Also removed old console.log

Related to #2463
2021-01-09 19:39:09 +00:00
Dan Brown
831f441879 Added in table + tasklist markdown rendering
For parity with markdown-it renderer.
Added tests to cover.
For #2452
2021-01-09 19:04:23 +00:00
Dan Brown
ea16ad7e94 Updated version and assets for release v0.31.1 2021-01-04 18:41:55 +00:00
Dan Brown
ba6eb54552 Merge branch 'master' into release 2021-01-04 18:41:26 +00:00
Dan Brown
47e3ef1be2 New Crowdin updates (#2441)
* New translations entities.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (French)

* New translations entities.php (French)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations entities.php (Spanish, Argentina)
2021-01-04 18:35:31 +00:00
Dan Brown
7791599fb5 Fixed recycle bin dropdown being cut off in chrome
Fixes #2442
2021-01-04 18:24:34 +00:00
Dan Brown
bbfb330b92 Added check of owner field for manage-permissions-own
This permission was still checking based on created-by.
Updated testing to specifically check the owner since the tests
were passing by the fact of matching creator and owner.

Fixes #2445
2021-01-04 18:07:39 +00:00
Dan Brown
20729a618f Fixed markdown content not stored on first page save
HTML content was still saved.
This changes makes the back-end check for md content
instead of html to ensure that gets stored in cases
where both are sent to the system.

Closes #2446
2021-01-04 17:52:08 +00:00
Dan Brown
f705e7683b Updated assets for release v0.31.0 again 2021-01-03 22:33:36 +00:00
Dan Brown
dc996adb20 Merge branch 'master' into release 2021-01-03 22:32:40 +00:00
Dan Brown
14ea6c9de3 Made fixes/updates during pre-release review
- Fixed page editor default focus not working as expected due to
  misnamed attribute.
- Added owned_by to relevant areas of the API including the docs.
- Made book relation on page accessible even if deleted since it could cause an issue on views, such as audit trail, when the relation is accessed when the book is deleted.
2021-01-03 22:29:58 +00:00
Dan Brown
a64c638ccc Updated version and assets for release v0.31.0 2021-01-03 21:52:37 +00:00
Dan Brown
359c067279 Merge branch 'master' into release 2021-01-03 21:52:00 +00:00
Dan Brown
8bb6394b47 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-01-03 20:55:34 +00:00
Dan Brown
ea8083ac6e Updated translators for v0.31 2021-01-03 20:55:08 +00:00
Dan Brown
04367eb4c5 New Crowdin updates (#2439)
* New translations entities.php (Norwegian Bokmal)

* New translations settings.php (Norwegian Bokmal)

* New translations validation.php (Norwegian Bokmal)

* New translations common.php (Norwegian Bokmal)

* New translations activities.php (Norwegian Bokmal)

* New translations entities.php (Spanish)
2021-01-03 20:54:01 +00:00
Dan Brown
f160020941 Added API request issue template 2021-01-03 19:23:41 +00:00
Dan Brown
75a795ab72 Made a couple of fixes during testing
- Updated audit table so long entity names did not squish everything
  else.
- Added filtering to view service popular list so that recycle binned
  items did not cause issues.
2021-01-03 19:02:50 +00:00
Dan Brown
47a6c621ff Squashed commit of the following:
commit f35d815ba0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:54 2021 +0000

    New translations entities.php (Norwegian Bokmal)

commit 2aba672219
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:52 2021 +0000

    New translations entities.php (French)

commit 323ff9f0ef
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:51 2021 +0000

    New translations entities.php (Spanish)

commit c795e8649b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:49 2021 +0000

    New translations entities.php (Arabic)

commit 161011cf20
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:47 2021 +0000

    New translations entities.php (Bulgarian)

commit 7780f0a7e7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:46 2021 +0000

    New translations entities.php (Czech)

commit f145e78456
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:44 2021 +0000

    New translations entities.php (Danish)

commit 35ed2f7dda
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:42 2021 +0000

    New translations entities.php (German)

commit 0c645d0905
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:40 2021 +0000

    New translations entities.php (Hebrew)

commit c890484cc5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:38 2021 +0000

    New translations entities.php (Hungarian)

commit fab4a03ee7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:36 2021 +0000

    New translations entities.php (Italian)

commit a2aca03833
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:34 2021 +0000

    New translations entities.php (Japanese)

commit a61d59818d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:33 2021 +0000

    New translations entities.php (Korean)

commit 8a684d1825
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:31 2021 +0000

    New translations entities.php (Swedish)

commit ae1c43871c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:29 2021 +0000

    New translations entities.php (Dutch)

commit 61534ed8d1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:27 2021 +0000

    New translations entities.php (Russian)

commit ca80c63af0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:25 2021 +0000

    New translations entities.php (Slovak)

commit f1e75d4a8d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:24 2021 +0000

    New translations entities.php (Slovenian)

commit 88b261ef48
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:22 2021 +0000

    New translations entities.php (Turkish)

commit c9f085743b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:20 2021 +0000

    New translations entities.php (Ukrainian)

commit d3a36e2f2d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:18 2021 +0000

    New translations entities.php (Chinese Simplified)

commit a4bad00370
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:16 2021 +0000

    New translations entities.php (Chinese Traditional)

commit f20ad5ac7c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:14 2021 +0000

    New translations entities.php (Vietnamese)

commit 7c3e053567
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:12 2021 +0000

    New translations entities.php (Portuguese, Brazilian)

commit 5d6ff41061
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:11 2021 +0000

    New translations entities.php (Spanish, Argentina)

commit e20bf841ad
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:09 2021 +0000

    New translations entities.php (German Informal)

commit bb1cb02b40
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:49:07 2021 +0000

    New translations entities.php (Polish)

commit d0e713c27e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:09:05 2021 +0000

    New translations validation.php (German Informal)

commit b9510b3ff1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:08:43 2021 +0000

    New translations activities.php (German Informal)

commit 2e218cc6ea
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:08:42 2021 +0000

    New translations activities.php (Norwegian Bokmal)

commit 34eff6404e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:08:40 2021 +0000

    New translations activities.php (Spanish, Argentina)

commit ae11ceea66
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:08:28 2021 +0000

    New translations common.php (Norwegian Bokmal)

commit 33dd5743e9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:08:02 2021 +0000

    New translations auth.php (Norwegian Bokmal)

commit 370ce5a4f1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:56 2021 +0000

    New translations settings.php (Polish)

commit 3b2cf032e6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:53 2021 +0000

    New translations settings.php (Dutch)

commit 1e340dfcbd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:51 2021 +0000

    New translations settings.php (Korean)

commit d3b777db76
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:48 2021 +0000

    New translations settings.php (Japanese)

commit 2053eb7415
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:45 2021 +0000

    New translations settings.php (Italian)

commit e26cf93b3c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:43 2021 +0000

    New translations settings.php (Hungarian)

commit 8beeb843fd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:40 2021 +0000

    New translations settings.php (Hebrew)

commit e39753711a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:38 2021 +0000

    New translations settings.php (Russian)

commit d04c910dde
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:35 2021 +0000

    New translations settings.php (Danish)

commit ff07b618d4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:32 2021 +0000

    New translations settings.php (Czech)

commit d9e32ace61
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:29 2021 +0000

    New translations settings.php (Bulgarian)

commit 75b53ddc4a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:26 2021 +0000

    New translations settings.php (Arabic)

commit a04eaf2c65
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:21 2021 +0000

    New translations settings.php (Swedish)

commit 7245c1f2bd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:20 2021 +0000

    New translations settings.php (Spanish)

commit 030d146f1d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:18 2021 +0000

    New translations settings.php (French)

commit 717807791a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:07:16 2021 +0000

    New translations settings.php (German)

commit abc29948b4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:59 2021 +0000

    New translations settings.php (Slovak)

commit 35a59233cf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:57 2021 +0000

    New translations settings.php (German Informal)

commit 06105a7b88
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:55 2021 +0000

    New translations settings.php (Spanish, Argentina)

commit eb0ac00cdd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:53 2021 +0000

    New translations entities.php (Spanish, Argentina)

commit 1592c098f2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:52 2021 +0000

    New translations settings.php (Portuguese, Brazilian)

commit 37e6ae7e0a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:49 2021 +0000

    New translations settings.php (Vietnamese)

commit 19bdcaaeb6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:46 2021 +0000

    New translations settings.php (Chinese Traditional)

commit d26318342b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:42 2021 +0000

    New translations settings.php (Ukrainian)

commit fd9e377d34
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:40 2021 +0000

    New translations settings.php (Turkish)

commit a5824bd615
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:37 2021 +0000

    New translations settings.php (Slovenian)

commit b3f025561d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:13 2021 +0000

    New translations pagination.php (Norwegian Bokmal)

commit 76a6fb78bf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:06:00 2021 +0000

    New translations validation.php (Norwegian Bokmal)

commit d2f674dfc9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:05:58 2021 +0000

    New translations validation.php (Spanish, Argentina)

commit 0a5fd95c44
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:05:34 2021 +0000

    New translations settings.php (Norwegian Bokmal)

commit 669788bbe5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:05:31 2021 +0000

    New translations passwords.php (Norwegian Bokmal)

commit e9defbba8e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:05:16 2021 +0000

    New translations entities.php (Norwegian Bokmal)

commit 04d1e118d4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:05:14 2021 +0000

    New translations components.php (Norwegian Bokmal)

commit 93b79e5569
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:04:35 2021 +0000

    New translations errors.php (Norwegian Bokmal)

commit e1076d658d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 16:04:19 2021 +0000

    New translations settings.php (Chinese Simplified)

commit 9ca08f1913
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 01:03:29 2021 +0000

    New translations entities.php (Spanish)

commit ace8cec1bc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 01:03:28 2021 +0000

    New translations settings.php (Spanish)

commit ad0090aa45
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:33:00 2021 +0000

    New translations settings.php (German Informal)

commit d5d335352a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:58 2021 +0000

    New translations settings.php (Japanese)

commit 3054a9e421
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:56 2021 +0000

    New translations entities.php (Japanese)

commit e408360838
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:54 2021 +0000

    New translations settings.php (Italian)

commit 119b620af8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:52 2021 +0000

    New translations entities.php (Italian)

commit 72a89d7852
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:50 2021 +0000

    New translations settings.php (Hungarian)

commit cc941b8ac9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:49 2021 +0000

    New translations entities.php (Hungarian)

commit d9ec1bb0b0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:47 2021 +0000

    New translations settings.php (Hebrew)

commit a67d6b8eef
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:45 2021 +0000

    New translations entities.php (Hebrew)

commit 91e8a24268
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:43 2021 +0000

    New translations settings.php (German)

commit 1cf6d211fa
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:41 2021 +0000

    New translations entities.php (German)

commit 23939bdb9b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:39 2021 +0000

    New translations settings.php (Danish)

commit 349946b2ba
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:37 2021 +0000

    New translations entities.php (Korean)

commit 611623eb45
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:36 2021 +0000

    New translations entities.php (Danish)

commit b10bd60e4d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:34 2021 +0000

    New translations entities.php (Czech)

commit 58e9cad253
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:32 2021 +0000

    New translations settings.php (Bulgarian)

commit e239d61c23
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:30 2021 +0000

    New translations entities.php (Bulgarian)

commit 8593e7c0bf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:28 2021 +0000

    New translations settings.php (Arabic)

commit af4aefb89b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:26 2021 +0000

    New translations entities.php (Arabic)

commit 143d046ef0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:24 2021 +0000

    New translations entities.php (Spanish)

commit bdd8d189c3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:22 2021 +0000

    New translations entities.php (French)

commit ec761325e7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:20 2021 +0000

    New translations settings.php (Swedish)

commit c0ae3b5b51
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:19 2021 +0000

    New translations settings.php (Spanish)

commit 27092813cd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:17 2021 +0000

    New translations settings.php (French)

commit b5fde40cf5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:15 2021 +0000

    New translations settings.php (Czech)

commit 7aabc8cc94
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:13 2021 +0000

    New translations entities.php (Swedish)

commit ceb4fd2aa4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:11 2021 +0000

    New translations settings.php (Korean)

commit d764b5741c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:09 2021 +0000

    New translations settings.php (Dutch)

commit 3e3884360b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:07 2021 +0000

    New translations entities.php (German Informal)

commit 9f4f7f9368
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:05 2021 +0000

    New translations settings.php (Spanish, Argentina)

commit 7ac467b28b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:03 2021 +0000

    New translations entities.php (Spanish, Argentina)

commit 169574ac42
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:01 2021 +0000

    New translations settings.php (Portuguese, Brazilian)

commit d49813b13e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:32:00 2021 +0000

    New translations entities.php (Portuguese, Brazilian)

commit 5bb7221fa0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:58 2021 +0000

    New translations settings.php (Vietnamese)

commit 2cb435970f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:56 2021 +0000

    New translations entities.php (Vietnamese)

commit f1a2dd5b8b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:54 2021 +0000

    New translations settings.php (Chinese Traditional)

commit c7991782a2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:52 2021 +0000

    New translations entities.php (Chinese Traditional)

commit f85f71beca
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:50 2021 +0000

    New translations entities.php (Chinese Simplified)

commit 8131a4908d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:49 2021 +0000

    New translations entities.php (Dutch)

commit 3b0d5d2124
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:47 2021 +0000

    New translations settings.php (Ukrainian)

commit 178419208a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:45 2021 +0000

    New translations settings.php (Turkish)

commit 88d6750971
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:43 2021 +0000

    New translations entities.php (Turkish)

commit 8ca6a95407
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:41 2021 +0000

    New translations settings.php (Slovenian)

commit 0effb9b150
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:39 2021 +0000

    New translations entities.php (Slovenian)

commit 85859fd58c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:37 2021 +0000

    New translations settings.php (Slovak)

commit 1f2db3f4f0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:35 2021 +0000

    New translations entities.php (Slovak)

commit d677c4020f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:34 2021 +0000

    New translations settings.php (Russian)

commit 9cecf56ef3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:32 2021 +0000

    New translations entities.php (Russian)

commit fd58ed477e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:30 2021 +0000

    New translations settings.php (Polish)

commit 2e358289b6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:28 2021 +0000

    New translations entities.php (Polish)

commit 83a57c4b1d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:26 2021 +0000

    New translations entities.php (Ukrainian)

commit 8d819bac39
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Jan 2 00:31:24 2021 +0000

    New translations settings.php (Chinese Simplified)

commit 77bf64160c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 22 07:39:40 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 24df157734
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 21 17:11:43 2020 +0000

    New translations settings.php (French)

commit 28cde11dc2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 21 13:12:33 2020 +0000

    New translations settings.php (Swedish)

commit 630174f8e4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 21 13:12:31 2020 +0000

    New translations entities.php (Swedish)

commit a40fc2203b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Dec 19 01:26:57 2020 +0000

    New translations settings.php (Spanish)

commit abbdb233c4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:08:01 2020 +0000

    New translations settings.php (German Informal)

commit f7a8cae968
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:59 2020 +0000

    New translations settings.php (German)

commit e793fe8929
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:58 2020 +0000

    New translations settings.php (French)

commit f1efd0f559
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:56 2020 +0000

    New translations settings.php (Spanish)

commit 6fbec6be40
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:54 2020 +0000

    New translations settings.php (Bulgarian)

commit 708f5b1158
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:52 2020 +0000

    New translations settings.php (Czech)

commit 7c978c5822
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:50 2020 +0000

    New translations settings.php (Danish)

commit 875138d217
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:48 2020 +0000

    New translations settings.php (Hebrew)

commit ba24274078
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:46 2020 +0000

    New translations settings.php (Hungarian)

commit bd5fdbe981
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:45 2020 +0000

    New translations settings.php (Italian)

commit 62d71a20d7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:43 2020 +0000

    New translations settings.php (Japanese)

commit a83f9bccb4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:41 2020 +0000

    New translations settings.php (Korean)

commit 306a18aa87
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:39 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 75724a14f2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:37 2020 +0000

    New translations settings.php (Dutch)

commit b21ba5e3b7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:35 2020 +0000

    New translations settings.php (Russian)

commit c292ef1389
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:33 2020 +0000

    New translations settings.php (Slovak)

commit f0dd37d476
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:32 2020 +0000

    New translations settings.php (Slovenian)

commit 0ffeadb959
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:30 2020 +0000

    New translations settings.php (Swedish)

commit 9dfd89120d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:28 2020 +0000

    New translations settings.php (Turkish)

commit f90d1056eb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:26 2020 +0000

    New translations settings.php (Ukrainian)

commit 064222468a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:24 2020 +0000

    New translations settings.php (Chinese Traditional)

commit 9dd05cc3e8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:22 2020 +0000

    New translations settings.php (Vietnamese)

commit 70867261f0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:20 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit ff0c8151d0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:19 2020 +0000

    New translations settings.php (Spanish, Argentina)

commit 53658cada6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:16 2020 +0000

    New translations settings.php (Polish)

commit 223c4005ae
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 23:07:15 2020 +0000

    New translations settings.php (Arabic)

commit 503f166cc1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 18 17:09:41 2020 +0000

    New translations entities.php (Chinese Simplified)

commit 401ecbb930
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 14:08:21 2020 +0000

    New translations validation.php (German)

commit de0d58658a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 14:08:19 2020 +0000

    New translations settings.php (German)

commit 32b7036405
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 13:27:48 2020 +0000

    New translations settings.php (German)

commit 212f1f7639
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 13:27:46 2020 +0000

    New translations activities.php (German)

commit 853162082b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 13:27:44 2020 +0000

    New translations entities.php (German)

commit 9c72dc7a5e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 12:55:26 2020 +0000

    New translations activities.php (German)

commit 261f71a6f4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 07:52:54 2020 +0000

    New translations entities.php (Spanish)

commit e2d5ba8ff5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 07:52:52 2020 +0000

    New translations entities.php (French)

commit 8ec28c59b2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:49:08 2020 +0000

    New translations entities.php (German Informal)

commit dc8aec2b94
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:49:06 2020 +0000

    New translations entities.php (Spanish)

commit baa0cb24a3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:49:04 2020 +0000

    New translations entities.php (Bulgarian)

commit 4c312d5d5b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:49:02 2020 +0000

    New translations entities.php (Czech)

commit e8c06380c6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:49:00 2020 +0000

    New translations entities.php (Danish)

commit 79844ed95c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:58 2020 +0000

    New translations entities.php (German)

commit f6177c301d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:56 2020 +0000

    New translations entities.php (Hebrew)

commit bc0d5cc853
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:54 2020 +0000

    New translations entities.php (Hungarian)

commit dfbeb4415f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:52 2020 +0000

    New translations entities.php (Italian)

commit 851ab730fb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:50 2020 +0000

    New translations entities.php (Japanese)

commit a5cc758017
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:48 2020 +0000

    New translations entities.php (Korean)

commit 94e8b1fc0f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:46 2020 +0000

    New translations entities.php (Dutch)

commit 5ad322e822
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:44 2020 +0000

    New translations entities.php (French)

commit 69e709254e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:42 2020 +0000

    New translations entities.php (Polish)

commit a54d3ad498
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:41 2020 +0000

    New translations entities.php (Slovak)

commit 9e5ec3e252
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:39 2020 +0000

    New translations entities.php (Slovenian)

commit 6fa88c13b7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:37 2020 +0000

    New translations entities.php (Swedish)

commit 40528d97b2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:35 2020 +0000

    New translations entities.php (Turkish)

commit 93303e6a13
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:33 2020 +0000

    New translations entities.php (Ukrainian)

commit fe9b70ced1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:31 2020 +0000

    New translations entities.php (Chinese Simplified)

commit 4e34047747
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:29 2020 +0000

    New translations entities.php (Chinese Traditional)

commit 71e8f850dc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:28 2020 +0000

    New translations entities.php (Vietnamese)

commit 9cb1542f0b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:26 2020 +0000

    New translations entities.php (Portuguese, Brazilian)

commit 35658ff827
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:24 2020 +0000

    New translations entities.php (Spanish, Argentina)

commit 6f75d1d972
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:22 2020 +0000

    New translations entities.php (Russian)

commit acf2ae03f3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 17 02:48:20 2020 +0000

    New translations entities.php (Arabic)

commit c2f1e0d474
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 15 10:49:10 2020 +0000

    New translations settings.php (Chinese Simplified)

commit ef64a956d2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 15 10:22:49 2020 +0000

    New translations settings.php (Chinese Simplified)

commit c14a1fe8db
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 19:49:01 2020 +0000

    New translations validation.php (Arabic)

commit 73df1e1b88
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 19:21:05 2020 +0000

    New translations validation.php (Arabic)

commit 231ccc8494
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 18:52:38 2020 +0000

    New translations validation.php (Arabic)

commit 9f4039ad01
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 18:09:41 2020 +0000

    New translations validation.php (Arabic)

commit c011045d5e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 17:31:18 2020 +0000

    New translations settings.php (Arabic)

commit b3e6888e31
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 17:01:37 2020 +0000

    New translations settings.php (Arabic)

commit eaf13fb98d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 15:11:11 2020 +0000

    New translations settings.php (Arabic)

commit 3f8585b81f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 14:31:37 2020 +0000

    New translations settings.php (Arabic)

commit 84467849bf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 14:02:31 2020 +0000

    New translations settings.php (Arabic)

commit 185747b5e7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 13:34:36 2020 +0000

    New translations settings.php (Arabic)

commit 63201489d3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 13:06:27 2020 +0000

    New translations settings.php (Arabic)

commit f71d784ca5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 12:27:39 2020 +0000

    New translations settings.php (Arabic)

commit 188670ee9a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Dec 8 11:55:04 2020 +0000

    New translations settings.php (Arabic)

commit 8674b284e9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 22:59:30 2020 +0000

    New translations settings.php (Arabic)

commit 1589910fa4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 22:31:22 2020 +0000

    New translations settings.php (Arabic)

commit bc99709e51
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 21:56:46 2020 +0000

    New translations passwords.php (Arabic)

commit c5e89e997c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 21:56:44 2020 +0000

    New translations errors.php (Arabic)

commit 7a33938d5f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 21:56:42 2020 +0000

    New translations settings.php (Arabic)

commit 30de30d4bf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 21:28:31 2020 +0000

    New translations errors.php (Arabic)

commit b4e028dc51
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 20:29:51 2020 +0000

    New translations entities.php (Arabic)

commit d6e056b0ad
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 20:29:49 2020 +0000

    New translations errors.php (Arabic)

commit 4edb96f2bb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 19:59:59 2020 +0000

    New translations entities.php (Arabic)

commit c51cdf2936
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 19:33:28 2020 +0000

    New translations entities.php (Arabic)

commit afb058d36d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 18:57:54 2020 +0000

    New translations entities.php (Arabic)

commit 3b96a6d06b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 18:29:05 2020 +0000

    New translations entities.php (Arabic)

commit 874a40da76
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 17:53:03 2020 +0000

    New translations entities.php (Arabic)

commit e1c7f7e6a3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 17:19:51 2020 +0000

    New translations entities.php (Arabic)

commit 1e921679c2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 16:40:54 2020 +0000

    New translations entities.php (Arabic)

commit ffcfe54d65
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:56:17 2020 +0000

    New translations components.php (Arabic)

commit cb73a23985
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:56:15 2020 +0000

    New translations common.php (Arabic)

commit 8695dfe078
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:20:19 2020 +0000

    New translations validation.php (Chinese Simplified)

commit 671f9ec07b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:20:18 2020 +0000

    New translations activities.php (Chinese Simplified)

commit d9072286d7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:20:16 2020 +0000

    New translations common.php (Arabic)

commit 8d4c07decc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 14:20:14 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 9f44955b55
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Dec 7 13:47:48 2020 +0000

    New translations settings.php (Chinese Simplified)

commit fe2914d07b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Dec 5 18:36:25 2020 +0000

    New translations auth.php (Arabic)

commit f9bf9da8bb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Dec 5 05:59:40 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 2d3ee7ceaf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Dec 4 02:28:44 2020 +0000

    New translations errors.php (Arabic)

commit 49359be73e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 3 20:27:57 2020 +0000

    New translations common.php (Arabic)

commit 97df834566
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 3 20:27:56 2020 +0000

    New translations auth.php (Arabic)

commit 73e5cbf8ab
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 3 20:27:54 2020 +0000

    New translations activities.php (Arabic)

commit 248e70dfc6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Dec 3 19:58:00 2020 +0000

    New translations settings.php (Arabic)

commit b616c03db6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Nov 30 20:29:48 2020 +0000

    New translations auth.php (Slovak)

commit c7978833a9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Nov 30 20:29:46 2020 +0000

    New translations activities.php (Slovak)

commit 589d4ea407
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Nov 30 20:01:41 2020 +0000

    New translations activities.php (Slovak)

commit 4ae7e4f1cd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 23:48:32 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit 9b60eb7e6b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 23:18:00 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit c02c0fc237
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 20:20:52 2020 +0000

    New translations auth.php (Slovenian)

commit bb742db8eb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 19:54:55 2020 +0000

    New translations validation.php (Slovenian)

commit 159d4367ef
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 14:06:49 2020 +0000

    New translations validation.php (Swedish)

commit 1c95c87981
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 14:06:47 2020 +0000

    New translations activities.php (Swedish)

commit 4d869414d5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 14:06:45 2020 +0000

    New translations settings.php (Swedish)

commit 3fbefead8f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 09:52:12 2020 +0000

    New translations settings.php (Slovenian)

commit 57170d5af7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 09:07:31 2020 +0000

    New translations settings.php (Slovenian)

commit 62ec0fc156
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:40:38 2020 +0000

    New translations passwords.php (Slovenian)

commit 9133bd129b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:40:36 2020 +0000

    New translations pagination.php (Slovenian)

commit fde76e7eb8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:40:34 2020 +0000

    New translations errors.php (Slovenian)

commit 321e495024
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:40:31 2020 +0000

    New translations settings.php (Slovenian)

commit d982458c90
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:11:18 2020 +0000

    New translations errors.php (Slovenian)

commit 8fc4571743
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 08:11:16 2020 +0000

    New translations entities.php (Slovenian)

commit 0a83121214
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 07:16:09 2020 +0000

    New translations entities.php (Slovenian)

commit ff4a76a102
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 06:49:09 2020 +0000

    New translations entities.php (Slovenian)

commit a0d254e0a6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 06:18:19 2020 +0000

    New translations entities.php (Slovenian)

commit c57b8348e8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 05:38:04 2020 +0000

    New translations components.php (Slovenian)

commit 8fe04e76be
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 05:38:02 2020 +0000

    New translations common.php (Slovenian)

commit 39e92b0e0d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 05:07:51 2020 +0000

    New translations common.php (Slovenian)

commit ea76a7ebd8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 05:07:50 2020 +0000

    New translations activities.php (Slovenian)

commit 8f710f8b8a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 26 04:37:47 2020 +0000

    New translations activities.php (Slovenian)

commit ffdd1e213d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 22 12:09:18 2020 +0000

    New translations activities.php (French)

commit 9e2f0e9c87
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 22 12:09:16 2020 +0000

    New translations settings.php (French)

commit 4faacad5ac
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:56:47 2020 +0000

    New translations activities.php (Spanish)

commit ae555d65e8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:56:45 2020 +0000

    New translations settings.php (Spanish)

commit 19929c893d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:35 2020 +0000

    New translations activities.php (German Informal)

commit 72ab8cc461
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:33 2020 +0000

    New translations settings.php (Spanish, Argentina)

commit e3da50c52d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:31 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit 1594fec763
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:30 2020 +0000

    New translations settings.php (Vietnamese)

commit b220afcbcb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:28 2020 +0000

    New translations settings.php (Chinese Traditional)

commit 18ed9ecf20
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:26 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 8f336c444c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:25 2020 +0000

    New translations settings.php (Ukrainian)

commit d84e61e02e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:23 2020 +0000

    New translations settings.php (Turkish)

commit 3c410a9121
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:21 2020 +0000

    New translations settings.php (Slovenian)

commit ab37f7a9fd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:19 2020 +0000

    New translations settings.php (Slovak)

commit 432a1a8090
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:18 2020 +0000

    New translations settings.php (Russian)

commit 5a65dfabaf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:16 2020 +0000

    New translations settings.php (Polish)

commit 7b731a19d5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:14 2020 +0000

    New translations settings.php (German Informal)

commit 90ba8bf7b9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:12 2020 +0000

    New translations settings.php (Dutch)

commit 8eb94f8faf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:10 2020 +0000

    New translations settings.php (Japanese)

commit 5c4f07f5ef
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:09 2020 +0000

    New translations settings.php (Italian)

commit 892c2b6648
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:07 2020 +0000

    New translations settings.php (Hungarian)

commit 746924a3d9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:05 2020 +0000

    New translations settings.php (Hebrew)

commit 3a3fcb8af2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:03 2020 +0000

    New translations settings.php (German)

commit 6263182593
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:02 2020 +0000

    New translations settings.php (Danish)

commit fbd52a81c6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:18:00 2020 +0000

    New translations settings.php (Czech)

commit bf2a2cddf2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:58 2020 +0000

    New translations settings.php (Bulgarian)

commit f87af24738
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:56 2020 +0000

    New translations settings.php (Arabic)

commit e4e7930702
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:55 2020 +0000

    New translations settings.php (Spanish)

commit 1e74a74d18
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:53 2020 +0000

    New translations settings.php (Korean)

commit eeb0c0cd25
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:51 2020 +0000

    New translations settings.php (French)

commit 16ce659aa7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:49 2020 +0000

    New translations activities.php (French)

commit 6bdda1c8b0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:47 2020 +0000

    New translations activities.php (Arabic)

commit 3bf57e068f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:46 2020 +0000

    New translations activities.php (Spanish, Argentina)

commit 29b767c487
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:44 2020 +0000

    New translations activities.php (Portuguese, Brazilian)

commit 1a5223a346
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:43 2020 +0000

    New translations activities.php (Vietnamese)

commit 431eb55781
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:41 2020 +0000

    New translations activities.php (Chinese Traditional)

commit 510cc653fe
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:39 2020 +0000

    New translations activities.php (Chinese Simplified)

commit 2270e5a0d4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:38 2020 +0000

    New translations activities.php (Ukrainian)

commit 89d4410062
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:36 2020 +0000

    New translations activities.php (Turkish)

commit 021ac828b2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:34 2020 +0000

    New translations activities.php (Swedish)

commit 3d23b3f0c9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:33 2020 +0000

    New translations activities.php (Slovenian)

commit 8504d11fc7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:31 2020 +0000

    New translations activities.php (Slovak)

commit a81b167efd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:30 2020 +0000

    New translations activities.php (Spanish)

commit bec7efca48
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:28 2020 +0000

    New translations activities.php (Russian)

commit 45ca2fd453
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:26 2020 +0000

    New translations activities.php (Dutch)

commit e30a3eb6a8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:25 2020 +0000

    New translations activities.php (Korean)

commit 35476844f6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:23 2020 +0000

    New translations activities.php (Japanese)

commit d6e3e61a63
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:22 2020 +0000

    New translations activities.php (Italian)

commit 3a80e68289
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:20 2020 +0000

    New translations activities.php (Hungarian)

commit 1b3efcdb62
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:18 2020 +0000

    New translations activities.php (Hebrew)

commit 59fcf3c4eb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:17 2020 +0000

    New translations activities.php (German)

commit 8e838d09bc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:15 2020 +0000

    New translations activities.php (Danish)

commit 67a43a5ba3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:13 2020 +0000

    New translations activities.php (Czech)

commit cb166b1980
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:12 2020 +0000

    New translations activities.php (Bulgarian)

commit 5c86cc3dfc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:10 2020 +0000

    New translations activities.php (Polish)

commit 0a9d90ef69
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 21 16:17:09 2020 +0000

    New translations settings.php (Swedish)

commit d57f402a00
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 06:09:10 2020 +0000

    New translations errors.php (Vietnamese)

commit 71ce4bfd1f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 06:09:08 2020 +0000

    New translations settings.php (Vietnamese)

commit 95adbaa0a9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:57 2020 +0000

    New translations validation.php (Vietnamese)

commit 23f1e2b763
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:55 2020 +0000

    New translations passwords.php (Vietnamese)

commit efee1392d2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:53 2020 +0000

    New translations entities.php (Vietnamese)

commit 8154f8648d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:52 2020 +0000

    New translations components.php (Vietnamese)

commit 5632f64d80
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:50 2020 +0000

    New translations common.php (Vietnamese)

commit 051c0b7896
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 19 05:39:48 2020 +0000

    New translations auth.php (Vietnamese)

commit 14b125d325
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Nov 17 02:37:49 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit ee9692e65d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Nov 17 01:58:45 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit df140ca216
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 15 09:19:28 2020 +0000

    New translations settings.php (French)

commit ee691853dc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 15 08:47:39 2020 +0000

    New translations entities.php (French)

commit cc22bcf166
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 12 11:39:15 2020 +0000

    New translations entities.php (Hungarian)

commit a0f21aeebd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 12 11:39:13 2020 +0000

    New translations components.php (Hungarian)

commit 437cd823b8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 12 11:39:12 2020 +0000

    New translations common.php (Hungarian)

commit 367ba587c7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Nov 12 11:39:09 2020 +0000

    New translations settings.php (Hungarian)

commit d656b9b982
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 8 21:58:02 2020 +0000

    New translations settings.php (French)

commit f3ffe9b7c5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 8 21:27:30 2020 +0000

    New translations settings.php (French)

commit cc9e48f3af
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 18:21:29 2020 +0000

    New translations settings.php (Spanish)

commit cbb5aeaca4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 17:26:14 2020 +0000

    New translations settings.php (Spanish)

commit addb0abc7c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:19:00 2020 +0000

    New translations settings.php (German Informal)

commit d9912d7cda
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:58 2020 +0000

    New translations settings.php (Spanish)

commit b72960c002
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:57 2020 +0000

    New translations settings.php (Arabic)

commit 99248fdced
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:55 2020 +0000

    New translations settings.php (Bulgarian)

commit 3ac52510ee
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:53 2020 +0000

    New translations settings.php (Czech)

commit eea823faf3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:51 2020 +0000

    New translations settings.php (Danish)

commit feea7f8c55
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:50 2020 +0000

    New translations settings.php (German)

commit 91a9441c60
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:48 2020 +0000

    New translations settings.php (Hebrew)

commit ec8020e36d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:46 2020 +0000

    New translations settings.php (Hungarian)

commit 0c4e394024
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:45 2020 +0000

    New translations settings.php (Italian)

commit 3527571d92
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:43 2020 +0000

    New translations settings.php (Japanese)

commit eaade5cf89
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:41 2020 +0000

    New translations settings.php (Korean)

commit e6383352bd
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:39 2020 +0000

    New translations settings.php (French)

commit ce2f779683
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:38 2020 +0000

    New translations settings.php (Dutch)

commit 1ea58e2da1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:36 2020 +0000

    New translations settings.php (Russian)

commit 399b765d6f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:34 2020 +0000

    New translations settings.php (Slovak)

commit 1be992e9be
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:33 2020 +0000

    New translations settings.php (Slovenian)

commit 9b54b832e9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:31 2020 +0000

    New translations settings.php (Turkish)

commit a7df19eef9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:29 2020 +0000

    New translations settings.php (Ukrainian)

commit 2210d9d1a6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:28 2020 +0000

    New translations settings.php (Chinese Simplified)

commit 72874dcfd4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:26 2020 +0000

    New translations settings.php (Chinese Traditional)

commit b8d86a48ad
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:24 2020 +0000

    New translations settings.php (Vietnamese)

commit 54848d0c48
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:23 2020 +0000

    New translations settings.php (Portuguese, Brazilian)

commit 25cb81b85f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:21 2020 +0000

    New translations settings.php (Spanish, Argentina)

commit 5d318c1281
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:19 2020 +0000

    New translations settings.php (Polish)

commit b6aaba3496
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Nov 7 15:18:18 2020 +0000

    New translations settings.php (Swedish)

commit 820959b7df
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Nov 3 12:01:14 2020 +0000

    New translations validation.php (Russian)

commit 2527970488
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Nov 1 18:36:50 2020 +0000

    New translations validation.php (French)

commit d7edc4440d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 18:08:28 2020 +0000

    New translations validation.php (Spanish)

commit e91ffcb06f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:44 2020 +0000

    New translations validation.php (German Informal)

commit 64d67bec66
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:42 2020 +0000

    New translations validation.php (Arabic)

commit 387f20c865
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:41 2020 +0000

    New translations validation.php (Bulgarian)

commit 73b59470c2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:39 2020 +0000

    New translations validation.php (Czech)

commit 12cec9b4c1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:38 2020 +0000

    New translations validation.php (Danish)

commit 151c518683
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:36 2020 +0000

    New translations validation.php (German)

commit 6b1beff616
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:34 2020 +0000

    New translations validation.php (Hebrew)

commit 746c5f99fa
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:33 2020 +0000

    New translations validation.php (Hungarian)

commit d295fbc104
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:31 2020 +0000

    New translations validation.php (Italian)

commit ac828a6f92
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:30 2020 +0000

    New translations validation.php (Japanese)

commit 5a58e1010b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:28 2020 +0000

    New translations validation.php (Korean)

commit faeb298d66
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:27 2020 +0000

    New translations validation.php (Dutch)

commit b6450b012c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:25 2020 +0000

    New translations validation.php (Spanish)

commit 8bc8bf1dd4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:23 2020 +0000

    New translations validation.php (Polish)

commit 7361e41ef9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:22 2020 +0000

    New translations validation.php (Slovak)

commit 2994fd900c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:20 2020 +0000

    New translations validation.php (Slovenian)

commit 00e3803591
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:18 2020 +0000

    New translations validation.php (Swedish)

commit fb07b50ac0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:17 2020 +0000

    New translations validation.php (Turkish)

commit 35f9f42517
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:15 2020 +0000

    New translations validation.php (Ukrainian)

commit 53c566289b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:14 2020 +0000

    New translations validation.php (Chinese Simplified)

commit f404ed79f1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:12 2020 +0000

    New translations validation.php (Chinese Traditional)

commit 0b91647b17
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:10 2020 +0000

    New translations validation.php (Vietnamese)

commit 7f9be00e7f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:08 2020 +0000

    New translations validation.php (Portuguese, Brazilian)

commit 7e343bdfd9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:07 2020 +0000

    New translations validation.php (Spanish, Argentina)

commit f2b5066533
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:05 2020 +0000

    New translations validation.php (Russian)

commit bdc22b7a24
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Oct 31 16:58:04 2020 +0000

    New translations validation.php (French)

commit ac6cd8c80a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:25 2020 +0000

    New translations settings.php (Swedish)

commit 72188ca92f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:24 2020 +0000

    New translations passwords.php (Swedish)

commit 371ade8f33
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:22 2020 +0000

    New translations entities.php (Swedish)

commit 0f69dafe97
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:21 2020 +0000

    New translations components.php (Swedish)

commit 2a4607bb3f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:19 2020 +0000

    New translations common.php (Swedish)

commit 359c132354
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 30 10:51:17 2020 +0000

    New translations auth.php (Swedish)

commit eb5f7b13da
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 23 18:22:40 2020 +0100

    New translations settings.php (Ukrainian)

commit b844928ee1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 23 17:51:17 2020 +0100

    New translations settings.php (Ukrainian)

commit 06af684aa4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 22:03:20 2020 +0100

    New translations settings.php (Ukrainian)

commit 27e499efe8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 21:11:53 2020 +0100

    New translations settings.php (Ukrainian)

commit 4994febc68
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 21:11:51 2020 +0100

    New translations errors.php (Ukrainian)

commit de521b393b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 21:11:49 2020 +0100

    New translations entities.php (Ukrainian)

commit 35cc7f933c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 21:11:47 2020 +0100

    New translations components.php (Ukrainian)

commit 04b2769fd5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 21:11:46 2020 +0100

    New translations common.php (Ukrainian)

commit 803c377b3b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 13:31:18 2020 +0100

    New translations entities.php (Spanish, Argentina)

commit 5701efa78e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 13:31:16 2020 +0100

    New translations settings.php (Spanish, Argentina)

commit 11d85ebd71
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Thu Oct 22 13:04:52 2020 +0100

    New translations settings.php (Spanish, Argentina)

commit 271a7e0683
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 15:50:10 2020 +0100

    New translations settings.php (Chinese Simplified)

commit 7a4663fba8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 14:18:38 2020 +0100

    New translations components.php (Korean)

commit 32b439a829
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 14:18:36 2020 +0100

    New translations settings.php (Korean)

commit 5c70727c11
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 13:42:10 2020 +0100

    New translations settings.php (Korean)

commit 9d66a04a2e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 13:42:09 2020 +0100

    New translations entities.php (Korean)

commit 4fcc18cd5a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Oct 16 13:42:07 2020 +0100

    New translations common.php (Korean)
2021-01-02 17:49:57 +00:00
Dan Brown
024b0d8a64 Fixed restore revision save order, Added restore summary testing
Found during review of #2353, A revision would be stored before a
restore which would result with a duplicate revision and the new summary
would be assigned against the wrong content.
This change saves the revison after restore and adds test to check the
content and summary text.
2021-01-02 16:42:05 +00:00
Dan Brown
83d77d5166 Merge branch 'master' of git://github.com/rondaa/BookStack into rondaa-master 2021-01-02 16:25:59 +00:00
Dan Brown
e53e4f85c7 Aligned norwegian lang with others and used correct locale 2021-01-02 15:58:23 +00:00
Dan Brown
a04a800258 Merge branch 'master' of git://github.com/Swoy/BookStack into Swoy-master 2021-01-02 15:45:18 +00:00
Dan Brown
e6ea53b3e6 Merge pull request #2416 from shubhamosmosys/master
Fix issue with viewing export dropdown list
2021-01-02 15:36:35 +00:00
Dan Brown
fd67f61b43 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-01-02 02:46:25 +00:00
Dan Brown
92922288dd Added iframe CSP, improved session cookie security
Added iframe CSP headers with configuration via .env.
Updated session cookies to be lax by default, dynamically changing to
none when iframes configured to allow third-party control.
Updated cookie security to be auto-secure if a https APP_URL is set.

Related to #2427 and #2207.
2021-01-02 02:43:50 +00:00
Dan Brown
280f1a0a5b Create FUNDING.yml 2021-01-02 01:25:49 +00:00
Dan Brown
588fd7d165 Fixed short editor in firefox and optimised some queries
Optimised permission fetching so that it won't initialise a bunch
of models for the role permissions and instead does a manual
query to get the data directly.
2021-01-02 01:22:41 +00:00
Dan Brown
857d9ed3f1 Merge pull request #2436 from BookStackApp/ownership_system
Entity Ownership System
2021-01-02 00:29:22 +00:00
Dan Brown
d27875bad1 Added owner field to DummyContentSeeder 2021-01-02 00:09:29 +00:00
Dan Brown
de989ffa9a Tested new ownership + (delete/change) systems 2021-01-01 23:58:53 +00:00
Dan Brown
b43f997dab Added manual type conversion to fix failing tests 2021-01-01 18:38:54 +00:00
Dan Brown
5e686bb624 Added user ownership migrate to delete screen. 2021-01-01 18:31:01 +00:00
Dan Brown
99b14621f9 Moved permission updating to its own tool
And added support for owner changing.
2021-01-01 17:49:48 +00:00
Dan Brown
da9083bf1f Fixed view path 2020-12-31 17:27:23 +00:00
Dan Brown
8833b5bc3b Added user-select input 2020-12-31 17:25:20 +00:00
Dan Brown
33e35c9a8a Converted breadcrumb-listing to new component system 2020-12-31 15:27:25 +00:00
Dan Brown
e408067b10 Fixed test helper method signature 2020-12-30 22:25:10 +00:00
Dan Brown
4c580d1571 Added owners to entity creation and updated tests 2020-12-30 22:18:28 +00:00
Dan Brown
b493becadf Started change for entities to have concept of owners 2020-12-30 18:25:35 +00:00
Dan Brown
c71f00b2ec Updated readme newsletter links 2020-12-30 16:51:55 +00:00
Shubham Tiwari
564f4f7c74 Remove unnecessary changes 2020-12-21 11:53:33 +05:30
Dan Brown
4e82d93350 Updated wording of image cleanup option
As per #2352
2020-12-18 22:59:47 +00:00
Dan Brown
f1e1a745b0 Fixed failing home test after changes in last commit
Also made a restriction test more reliable.
Also renamed restrictionstest to entitypermissionstest to be more
consistent with newer app wording.
2020-12-18 21:44:35 +00:00
Dan Brown
4b4642c8ea Aligned book and shelf grid item views
Updated the titles so they are limited via CSS rather than by a
estimated hardcoded limit.

For #1469
2020-12-18 21:26:22 +00:00
Dan Brown
2b603b0488 Updated deps based on changes done for php8 readiness
Commit cherry-picked from branch then made further changes.
Updates min php version.
2020-12-18 20:29:33 +00:00
Dan Brown
20bb76afdb Fixed changed namespaces for merged test 2020-12-18 20:04:48 +00:00
Dan Brown
cf04a0d818 Merge branch 'v0.30.x' 2020-12-18 14:16:13 +00:00
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
2acef3c2ec Fixed issue where restricted page content in plaintext export
The content of pages made non-viewable to a user via permissions, within a visible parent, could be seen via the plaintext export option. Before v0.30.6 this would have applied only to scenarios where all pages within the chapter were made non-visible. In v0.30.6 this would make all pages within the chapter visible.

As per #2414
2020-12-18 13:56:00 +00:00
Dan Brown
9884cca00c Merge branch 'v0.30.x' 2020-12-17 21:47:59 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
3f3fad7113 Fixed book-tree-gen page visibility issue
When book trees were generated, pages in chapters where ALL pages within
were not supposed to be visibile, would be visible due to the code
falling back on the raw relation which would not account for
permissions.

This has now been changed so that a custom 'visible_pages' attribute is set and used by any book tree structures, to ensure it does not fall back to the raw relation.

Added an extra test to cover.

For #2414
2020-12-17 17:31:18 +00:00
shubhamosmosys
d7e0d3e2d6 Fix issue with viewing export dropdown list
There is issue with viewing all the export list
2020-12-17 19:47:15 +05:30
Dan Brown
5ab0db9690 Updated chapter delete wording to fit with new logic 2020-12-17 02:29:53 +00:00
Dan Brown
00308ad4ab Cleaned up some user/image areas of the app
Further cleanup of docblocks and standardisation of repos.
2020-12-08 23:46:38 +00:00
Dan Brown
6c09334ba0 Fixed issue where page export contain system would miss images 2020-12-06 22:23:21 +00:00
Dan Brown
65b2c90522 Merge branch 'v0.30.x' 2020-12-06 21:32:01 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
884664bfe9 Ensured base64 images are read from image upload folder
Also removed unused storage systems and updated testing.
2020-12-06 15:34:18 +00:00
Dan Brown
8911e3f441 Removed http fetching from image base64 generation 2020-12-06 14:24:22 +00:00
Dan Brown
7d38c96a23 Removed generic "UploadService" which was doing very little 2020-12-06 12:58:40 +00:00
Dan Brown
162d893143 Updated .env.example to encorage use of setting APP_URL
For the purposes of secure URL generation and to avoid common problems
found when people are using reverse proxies.
2020-12-06 12:31:36 +00:00
Dan Brown
0b01a77c16 Swapped out HTML diff implementation for own, removes tidy depdendancy 2020-11-29 19:08:13 +00:00
Dan Brown
bf8716bb22 Fixed bad collection/array mixing causing error on seed 2020-11-28 16:42:12 +00:00
Dan Brown
d56e7e7c79 Merge pull request #2382 from BookStackApp/pages_api
Pages API
2020-11-28 16:31:35 +00:00
Dan Brown
57754c8211 Added testing to cover the pages API 2020-11-28 16:30:30 +00:00
Dan Brown
8aedba14a3 Added page export API controller 2020-11-28 15:39:40 +00:00
Dan Brown
875a8bdaff Made docs sidebar a slight bit easier to scroll
Now it easily goes off the page, made it indapentally scrollable.
Will probably do something different in future as it grows more.
2020-11-28 15:28:44 +00:00
Dan Brown
53bcfe528d Added pages API doc examples
Made some tweaks to related content and other examples while there.
2020-11-28 15:21:54 +00:00
Dan Brown
1c8102bb89 Started pages API 2020-11-22 14:56:19 +00:00
Dan Brown
ebeca256f0 Updated old exportService name in controllers 2020-11-22 01:26:14 +00:00
Dan Brown
a042e22481 Focused base Entity class cleanup
Removed some common functions from other entities.
Aligned implementation of getUrl()
Cleaned phpdocs and added typehinting.
Also extracted sibling search logic out of controller.
2020-11-22 01:20:38 +00:00
Dan Brown
ef1b98019a Fixed some mis-refactoring and split search service
Search service broken into index and runner tools.
2020-11-22 00:17:45 +00:00
Dan Brown
c7a2d568bf Moved models to folder, renamed managers to tools
Tools seems to fit better since the classes were a bit of a mixed bunch
and did not always manage.
Also simplified the structure of the SlugGenerator class.
Also focused EntityContext on shelves and simplified to use session
helper.
2020-11-21 23:20:54 +00:00
Dan Brown
66917520cb Service provider and other cleanup
- Removed old 'exposeTranslations' system to instead use new component
 option system.
- Extracted validation rules into their own service provider.
- Cleaned up some formatting/comments in the repos.
2020-11-21 17:52:49 +00:00
Dan Brown
5e01c30882 Aligned constructors across controller classes
Since they no longer needed to run the parent contructor
since the parent constructor was no longer needed.
2020-11-21 17:08:37 +00:00
Dan Brown
f76a2a69f7 Cleaned up api docs implementation, added missing titles 2020-11-21 17:03:24 +00:00
Dan Brown
65ddd16532 Merge pull request #2360 from BookStackApp/activity_revamp
Tracked activity update
2020-11-21 16:12:25 +00:00
Dan Brown
c0680d5717 Added latest activity into users list view 2020-11-20 20:10:18 +00:00
Dan Brown
bd6a1a66d1 Implemented remainder of activity types
Also fixed audit log to work for non-entity items.
2020-11-20 19:33:11 +00:00
Dan Brown
da37700ac2 Implemented user, api_tokem & role activity logging
Also refactored some role content, primarily updating the permission
controller to be RoleController since it only dealt with roles.
2020-11-20 18:53:01 +00:00
Dan Brown
3f7180fa99 Started widening of activity logging
In progress, Need to implement much of the logging in controllers.
Also cleaned up base controller along the way.
2020-11-18 23:40:39 +00:00
Dan Brown
712ccd23c4 Updated activities table format
Renamed some columns to be more generic and applicable.
Removed now redundant book_id column.
Allowed nullable entity morph columns for non-entity activity.

Ran tests and made required changes.
2020-11-08 00:03:19 +00:00
Dan Brown
ee7e1122d3 Removed use of book_id in activity 2020-11-07 23:15:13 +00:00
Dan Brown
c157dc3490 Organised activity types and moved most to repos
Repos are generally better since otherwise we end up duplicating
things between front-end and API.

Types moved to by CONST values within a class for better visibilty
of usage and listing of types.
2020-11-07 22:37:27 +00:00
Dan Brown
4824ef2760 Merge pull request #2283 from BookStackApp/recycle_bin
Recycle Bin Implementation
2020-11-07 15:10:17 +00:00
Dan Brown
b4da081552 Checked over recycle bin parent/child flows 2020-11-07 15:05:13 +00:00
Dan Brown
df10b508d8 Enhanced how activities are shown on items in recycle bin 2020-11-07 14:28:50 +00:00
Dan Brown
ec3aeb3315 Added recycle bin auto-clear lifetime functionality 2020-11-07 13:58:23 +00:00
Dan Brown
68b1d87ebe Added test coverage of recycle bin actions 2020-11-07 13:19:23 +00:00
Dan Brown
483cb41665 Started testing work for recycle bin implementation 2020-11-06 12:54:39 +00:00
Anthony Ronda
34dc4a1b6d Automatic Restored Revision Changelog Summary Text 2020-11-03 20:46:47 -05:00
Dan Brown
3e70c661a1 Cleaned up duplicate code in recycle-bin restore 2020-11-02 22:54:00 +00:00
Dan Brown
9e033709a7 Added per-item recycle-bin delete and restore 2020-11-02 22:47:48 +00:00
Dan Brown
82e671a06d Re-aligned init files with Laravel default
Removed the custom init elements that we added in 2017 to
custom load the helpers file and instead load via composer.

Also removed laravel-microscope package due to not running due to
helpers file.
2020-10-31 23:05:48 +00:00
Dan Brown
474770af51 Merge branch 'fixes' of git://github.com/imanghafoori1/BookStack into imanghafoori1-fixes 2020-10-31 22:11:27 +00:00
Dan Brown
78be644332 Merge pull request #2298 from timoschwarzer/composer-install-in-entrypoint
Install composer dependencies in Docker entrypoint
2020-10-31 21:56:48 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
6d8b0605a0 Merge branch 'xss_and_redir_patch' of git://github.com/PercussiveElbow/BookStack into xss_and_redirect 2020-10-31 15:19:33 +00:00
Dan Brown
349162ea13 Prevented possible XSS via link attachments
This filters out potentially malicious javascript: or data: uri's coming
through to be attached to attachments.
Added tests to cover.

Thanks to Yassine ABOUKIR (@yassineaboukir on twitter) for reporting this
vulnerability.
2020-10-31 15:01:52 +00:00
PercussiveElbow
bbd1384acb XSS and redirect fixes with test cases 2020-10-27 01:34:51 +00:00
Ole Aldric
36daa09441 Update Localization.php in Middleware with "no" tag for estimate. 2020-10-19 12:43:41 +02:00
Ole Aldric
4c5566755f updated config to also include Norwegian 2020-10-19 12:35:05 +02:00
Ole Aldric
461977cf9a added missing comma that caused the testprocess to fail. 2020-10-19 12:26:18 +02:00
Ole Aldric
837cccd4d4 Added translation for Norwegian (Bokmål)
This will add translations for Norwegian to BookStack. It is identified by the langID no_NB
2020-10-19 11:43:43 +02:00
imanghafoori
7a5442e81b Adds laravel-microscope package 2020-10-16 18:40:44 +03:30
imanghafoori
704b808e9e fixes from laravel-microscope 2020-10-16 18:40:10 +03:30
Dan Brown
6aa2bf9e27 Merge pull request #2296 from timoschwarzer/esbuild-watch-first-time-fix
Fix build:js:watch not building at first launch in Docker
2020-10-13 23:17:23 +01:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
18bcafaee4 Updated translator attribution before release v0.30.3 2020-10-13 22:49:55 +01:00
Dan Brown
8d07b7cf1c Added alias for vbscript 2020-10-13 22:44:33 +01:00
Dan Brown
080f9c3025 Merge pull request #2302 from nutsflag/master
Add VBScript Codemirror
2020-10-13 22:41:09 +01:00
Dan Brown
617fe6bc8c Merge pull request #2303 from BookStackApp/l10n_master
New Crowdin updates
2020-10-13 22:39:52 +01:00
Dan Brown
bb1f1a9ecd Fixed error on drawing edit on markdown editor
Was preventing save of drawings.
For #2313
2020-10-13 22:36:07 +01:00
Dan Brown
d688e43197 New translations settings.php (Chinese Simplified) 2020-10-05 06:26:38 +01:00
Dan Brown
ff7cbd14fc Added recycle bin empty notification response with count 2020-10-03 18:53:09 +01:00
Dan Brown
04197e393a Started work on the recycle bin interface 2020-10-03 18:44:12 +01:00
Dan Brown
c82c3023c5 New translations settings.php (Spanish) 2020-10-02 17:18:27 +01:00
Dan Brown
d0d75afc66 New translations settings.php (Chinese Simplified) 2020-10-02 15:55:46 +01:00
nutsflag
467176ee78 Update code.js 2020-10-02 15:14:29 +02:00
nutsflag
521a002001 Update code-editor.blade.php 2020-10-02 15:13:31 +02:00
Timo Schwarzer
a74d551bd6 Install composer dependencies in Docker entrypoint 2020-10-01 11:34:56 +02:00
Timo Schwarzer
aca37b8784 Fix build:js:watch not building at first launch in Docker 2020-10-01 11:25:22 +02:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
f3ee8f2d4c Updated http service to not read 204 response data 2020-09-30 22:32:03 +01:00
Dan Brown
ea406690f5 Updated esbuild options and version & updated npm deps
Had to change way sortable is imported due to changes, Still
seemed to have functioning multi-select.
2020-09-30 22:28:53 +01:00
Dan Brown
465d405926 Updated page content related links on content id changes
For #2278
2020-09-28 22:26:50 +01:00
Dan Brown
1097c61d6d Fixed duplicate requests in attachment manager issue
Closes #2286
2020-09-28 21:55:24 +01:00
Dan Brown
def2d61ad8 Merge pull request #2272 from jakubboucek/feature/fix-invalid-canonical-redirect
Fixed canonical redirects on non-root url app instances
2020-09-28 21:15:23 +01:00
Dan Brown
8b0f5e7000 Updated draw.io references to diagrams.net
Related to #2044
2020-09-28 20:45:38 +01:00
Dan Brown
691027a522 Started implementation of recycle bin functionality 2020-09-27 23:24:33 +01:00
Jakub Bouček
1e88e8086f Fixed canonical redirects on non-root url app instances
If BookStack instance is deployed to any non-root path, e.g. http://example.com/wiki/,
requests for http://example.com/wiki/shelves/
was redirected to http://example.com/shelves
instead of http://example.com/wiki/shelves

Synced with: https://github.com/laravel/laravel/blob/master/public/.htaccess
2020-09-27 02:50:37 +02:00
Dan Brown
d48ac0a37d Removed redundant test
Now replaced in recent commit by one that checks actual message gets
displayed on the redirect page.
Redirect page changed to login page.
2020-09-26 18:24:05 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
3eeb1e7d08 Updated translators fiel with latest 2020-09-26 17:48:02 +01:00
Dan Brown
0d43b50f9d New Crowdin updates (#2262)
* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Czech)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations settings.php (Czech)

* New translations errors.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (German)

* New translations settings.php (German)

* New translations entities.php (German)

* New translations validation.php (Czech)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (German Informal)

* New translations settings.php (German Informal)

* New translations auth.php (Czech)
2020-09-26 17:46:32 +01:00
Dan Brown
6bcfac6751 Updated codemirror and updated codemirror base styles
Aligns styles with current release, since was causing overflow
with scrollbars.

Fixes #2267
2020-09-26 17:33:43 +01:00
Dan Brown
68489e5b44 Updated PR code to use isA and updated that function definition
Related to #2227
2020-09-26 17:00:17 +01:00
Dan Brown
fe0e307313 Merge branch 'renderpages' of git://github.com/mr-vinn/BookStack into mr-vinn-renderpages 2020-09-26 16:55:05 +01:00
Dan Brown
9985046685 Added test for includes on book export
Related to #2227
2020-09-26 16:54:24 +01:00
Dan Brown
53ec794e53 Fixed issue where SAML login not notifiy on existing user
Added testing to cover

Fixes #2263
2020-09-26 16:43:06 +01:00
Dan Brown
328d2514c4 Updated settings nav to be more flexible
Uses flexbox layout, flexed to content instead of rigid thirds like
before. Also extracted row into own file
2020-09-26 16:26:30 +01:00
Dan Brown
de2756dd95 Updated callout links to be correct colors
- Also updated to be underlined instead of bold
2020-09-26 15:40:51 +01:00
Dan Brown
1f97047799 Merge branch 'master' of git://github.com/alexmannuk/BookStack into alexmannuk-master 2020-09-26 15:35:13 +01:00
Dan Brown
c870c10e38 Merge pull request #2270 from gertjankrol/feature/test-migrations-workflow
Add `test-migrations` workflow
2020-09-26 15:25:17 +01:00
Dan Brown
49fa21c1e2 Merge pull request #2268 from gertjankrol/master
Fix the `AddActivityIndexes` migration's `down()` method
2020-09-26 15:21:21 +01:00
Dan Brown
9f87423584 Merge pull request #2274 from abulgatz/patch-1
Fixed "Ubunto Mono" $mono type misspelling
2020-09-26 12:11:53 +01:00
Dan Brown
08fbd39fcb Fixed markdown iframe loading and content alignment
Fixes #2280
2020-09-26 12:01:01 +01:00
Adam
5f75a9f32c Fix "Ubunto Mono" $mono type misspelling 2020-09-23 16:19:30 -05:00
Gertjan Krol
3750922c3e Added the test-migrations workflow 2020-09-22 19:53:45 +02:00
Gertjan Krol
4b0d1ddf39 Fixed the AddActivityIndexes migration's down() method 2020-09-22 19:22:27 +02:00
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
a6c20c321f Merged latest translation changes 2020-09-20 10:28:01 +01:00
Dan Brown
e12012a6fc Updated translation contributors 2020-09-20 09:15:02 +01:00
Dan Brown
73b4c6d947 Fixed some wording in example env 2020-09-19 23:09:08 +01:00
Dan Brown
9e11fc33fa Updated example env with helpful info
- Added comments to explain the use of the file.
- Added comments to advise that space/hash containing values would need
to be quoted.

Related to #2258
2020-09-19 16:09:43 +01:00
Dan Brown
ff46d81681 Merge branch 'jb-l10n-fix-czech' of git://github.com/jakubboucek/BookStack into jakubboucek-jb-l10n-fix-czech 2020-09-19 15:44:18 +01:00
Dan Brown
1f202f6dbc Updated locale lists for Bulgarian 2020-09-19 15:36:17 +01:00
Dan Brown
bccf4653cb New Crowdin translations (#2077)
* New translations entities.php (Portuguese, Brazilian)

* New translations entities.php (Persian)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Thai)

* New translations errors.php (German Informal)

* New translations entities.php (Spanish)

* New translations entities.php (French)

* New translations entities.php (Arabic)

* New translations entities.php (Arabic)

* New translations components.php (Portuguese, Brazilian)

* New translations entities.php (Portuguese, Brazilian)

* New translations auth.php (Italian)

* New translations common.php (Italian)

* New translations components.php (Italian)

* New translations entities.php (Italian)

* New translations settings.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Spanish)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Ukrainian)

* New translations components.php (Portuguese)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations common.php (Slovenian)

* New translations components.php (Slovenian)

* New translations common.php (Swedish)

* New translations components.php (Swedish)

* New translations common.php (Turkish)

* New translations components.php (Turkish)

* New translations components.php (Ukrainian)

* New translations components.php (Polish)

* New translations common.php (Chinese Simplified)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations common.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Persian)

* New translations components.php (Persian)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations common.php (Thai)

* New translations components.php (Thai)

* New translations common.php (Portuguese)

* New translations common.php (Polish)

* New translations common.php (Italian)

* New translations common.php (Bulgarian)

* New translations components.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations common.php (Arabic)

* New translations components.php (Arabic)

* New translations components.php (Bulgarian)

* New translations common.php (Dutch)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations common.php (Danish)

* New translations components.php (Danish)

* New translations common.php (German)

* New translations common.php (Hebrew)

* New translations components.php (Hebrew)

* New translations common.php (Hungarian)

* New translations components.php (Hungarian)

* New translations common.php (Japanese)

* New translations common.php (Korean)

* New translations components.php (Korean)

* New translations common.php (German Informal)

* New translations components.php (German)

* New translations common.php (German)

* New translations entities.php (German)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations components.php (Chinese Simplified)

* New translations common.php (Chinese Simplified)

* New translations common.php (Polish)

* New translations components.php (Polish)

* New translations auth.php (Polish)

* New translations entities.php (Polish)

* New translations errors.php (Polish)

* New translations passwords.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations auth.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations passwords.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations entities.php (German)

* New translations components.php (German Informal)

* New translations common.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (Italian)

* New translations settings.php (Dutch)

* New translations settings.php (Thai)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Korean)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Japanese)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (French)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations settings.php (Spanish)

* New translations settings.php (French)

* New translations components.php (Turkish)

* New translations settings.php (Turkish)

* New translations entities.php (Turkish)

* New translations common.php (Turkish)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Chinese Simplified)

* New translations activities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations passwords.php (Chinese Traditional)

* New translations settings.php (German)

* New translations settings.php (German Informal)

* New translations activities.php (Slovak)

* New translations auth.php (Slovak)

* New translations auth.php (Slovak)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations components.php (Slovak)

* New translations entities.php (Slovak)

* New translations common.php (Slovak)

* New translations entities.php (Slovak)

* New translations passwords.php (Slovak)

* New translations settings.php (Dutch)

* New translations components.php (Dutch)

* New translations entities.php (Dutch)

* New translations passwords.php (Dutch)

* New translations activities.php (Arabic)

* New translations entities.php (French)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations activities.php (Japanese)

* New translations auth.php (Japanese)

* New translations entities.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations entities.php (Russian)

* New translations entities.php (Portuguese)

* New translations entities.php (Thai)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Persian)

* New translations entities.php (Portuguese, Brazilian)

* New translations entities.php (Vietnamese)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Simplified)

* New translations entities.php (Ukrainian)

* New translations entities.php (Turkish)

* New translations entities.php (Swedish)

* New translations entities.php (Slovenian)

* New translations entities.php (Slovak)

* New translations entities.php (Polish)

* New translations entities.php (French)

* New translations entities.php (Dutch)

* New translations entities.php (Korean)

* New translations entities.php (Japanese)

* New translations entities.php (Italian)

* New translations entities.php (Hungarian)

* New translations entities.php (Hebrew)

* New translations entities.php (German)

* New translations entities.php (Danish)

* New translations entities.php (Czech)

* New translations entities.php (Bulgarian)

* New translations entities.php (Arabic)

* New translations entities.php (Spanish)

* New translations entities.php (German Informal)

* New translations entities.php (French)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Thai)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Polish)

* New translations settings.php (French)

* New translations settings.php (Dutch)

* New translations settings.php (Korean)

* New translations settings.php (Japanese)

* New translations settings.php (Italian)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations entities.php (Spanish)

* New translations settings.php (Spanish)
2020-09-19 15:22:32 +01:00
Dan Brown
31eec34b5d Moved decode and updated page plaintext decode test 2020-09-19 15:13:18 +01:00
Dan Brown
44f3508171 Merge branch 'preview-entities' of git://github.com/mr-vinn/BookStack into mr-vinn-preview-entities 2020-09-19 14:58:56 +01:00
Dan Brown
2e39e45886 Added test to check text gen decodes HTML entities 2020-09-19 14:58:18 +01:00
Dan Brown
458aa72c2f Updated composer deps 2020-09-19 12:12:48 +01:00
Dan Brown
78bf044a7a Added audit log interface
- Displays the currently tracked activities in the system.

Related to #2173 and #1167
2020-09-19 12:06:45 +01:00
Dan Brown
e5f0b4dd85 Split out Maintenance to separate controller 2020-09-19 09:24:58 +01:00
Vinnie Okada
311a12b7ef Decode HTML entities
Decode HTML entities in page text before saving it to the database.
2020-09-18 06:54:30 -06:00
Dan Brown
d9e2bddee4 Added some robustness to page draft saving
- Updated so that a warning is always shown on error, Not just on first
in chain.
- Added last-resort localStorage content saving.
2020-09-13 19:32:45 +01:00
Dan Brown
6578ac0b4a Fixed visible revision delete menu 2020-09-13 19:12:15 +01:00
Dan Brown
09c6d6c722 Added button for inserting attachment link to a page
For #1460
2020-09-13 18:58:05 +01:00
Dan Brown
ad48cd3e48 Continued implementation of attachment drag+drop
Cannot get working in chrome reliably due to conflicting handling of
events and drag+drop API. Getting attachment drop working breaks other
parts of TinyMCE.
Implementing current work as should still work for MD editor and within
FireFox.

Related to #1460
2020-09-13 18:31:14 +01:00
Dan Brown
e305ba14d9 Merge branch 'master' into attachment_drag_drop 2020-09-13 16:33:31 +01:00
Vinnie Okada
2c3f453c1f Implement the renderPages parameter
Render page content when getTree() is called with a true $renderPages
argument.
2020-09-07 09:05:51 -06:00
Dan Brown
b87e97f99e Added punnycode since its reuquired by markdownit
Is a native, although depricated, nodejs module. Have installed manually
since esbuild could not resolve the nodejs module
2020-09-05 20:37:23 +01:00
Dan Brown
e5377d5f46 Updated saml2 slo config so url is used if no repsonse url
Updated config to change empty string to null since the empty string was
hitting an isset check which caused an empty string to be used instead
of the slo url as a backup option.

Closes #2002
2020-09-05 19:26:47 +01:00
Dan Brown
ff1ee2d71f Updated flow to ensure /register/confirm route is used where needed
Was accidentally skipped during previous updates. Will now be used on
saml, ldap & standard registration where required.
Uses session to know if the email was just sent and, if so, show the
confirmation route.
2020-09-05 17:26:48 +01:00
Dan Brown
c029741a17 Updated npm deps 2020-09-05 16:54:25 +01:00
Dan Brown
ac83c349da Migrated from webpack to esbuild 2020-09-05 16:50:20 +01:00
Jakub Bouček
fefcaa21e7 Fix English translations
- Fix obvious bug
- Reunite capitalisation
2020-08-31 20:45:09 +02:00
Jakub Bouček
6a36db3cde Czech translations: Fix broken labels 2020-08-31 20:45:09 +02:00
Jakub Bouček
c9352bfd42 Czech translations: Add new translations to cs, improve existing 2020-08-31 20:45:09 +02:00
Jakub Bouček
9c457d9ffe Fix Czech translations (email -> e-mail)
In Czech language "email" does not means "email" but "enamel paint", correct is "e-mail".
See in Wikipedia:
- https://cs.wikipedia.org/wiki/E-mail
- https://cs.wikipedia.org/wiki/Email_(barva)
2020-08-31 17:34:46 +02:00
alexmannuk
7837b8c4ee Updated callout link formatting
Updated callout links to use font colouring based on type, with bold text to denote link, instead of using the theme link colour per issue #303.
2020-08-24 20:03:08 +01:00
Dan Brown
87a5340a05 Prevented email confirmation exception throw on registration
Was preventing any other registration actions from taking place such as
LDAP/SAML group sync. Email confirmation should be actioned by
middleware on post-registration redirect.

Added testing to cover.
Tested for LDAP, SAML and normal registration with email confirmation
required to ensure flows work as expected.

Fixes #2082
2020-08-04 17:54:50 +01:00
Dan Brown
c076ca408c Fixed non-visible horizontal rules in dark mode
Fixes #2209
2020-08-04 15:39:07 +01:00
Dan Brown
1ac11c1852 Added warning to role screen for important permissions
Warning related to permissions that could allow a person to promote
their own permissions to gain more privileges than expected.

For #2105.
2020-08-04 15:26:13 +01:00
Dan Brown
5f1ee5fb0e Removed role 'name' field from database
The 'name' field was really redundant and caused confusion in the
codebase, since the 'Display' name is often used and we have a
'system_name' for the admin and public role.

This fixes #2032, Where external auth group matching has confusing
behaviour as matching was done against the display_name, if no
external_auth field is set, but only roles with a match 'name' field
would be considered.

This also fixes and error where the role users migration, on role
delete, would not actually fire due to mis-matching http body keys.
Looks like this has been an issue from the start. Added some testing to
cover. Fixes #2211.

Also converted phpdoc to typehints in many areas of the reviewed code
during the above.
2020-08-04 14:55:01 +01:00
Dan Brown
a9f02550f0 Removed joint_permissions auto_increment id
Removed auto_incrementing id and set a primary key of the [role_id,
entity_type, entity_id, action] instead since this table could recieve a
lot of activity, especially when permission regeneration was automated,
leading to very high auto_increment counts which could max out the
integer limit.

Also updated some RolesTest comment endpoints to align with
recent route changes.

Should fix #2091
2020-08-04 13:02:31 +01:00
Dan Brown
7590ecd37c Updated some comment elements and standardised more JS
- Updated comment routes to be simpler.
- Updated comments JS to align better with updated component system.
- Documented available global JS functions/services.
- Removed redundant controller method.
- Added window.$events helpers for validation messages and
success/error.
- Updated JS events system to not be class based for simplicity.
- Added window.trans_plural method to handle pluralisation/replacements
where you already have the translation string itself.

Fixes #1836
2020-07-28 18:19:18 +01:00
Dan Brown
2c0fdf83c1 Updated public-login redirect to check url
Direct links to the login pages for public instances could lead to a
redirect back to an external page upon login.
This adds a check to ensure the URL is a URL expected from the current
bookstack instance, or at least under the same domain.

Fixes #2073
2020-07-28 16:29:06 +01:00
Dan Brown
2ed0317129 Updated functionality for logging failed access
- Added testing to cover.
- Linked logging into Laravel's monolog logging system and made log
channel configurable.
- Updated env var names to be specific to login access.
- Added extra locations as to where failed logins would be captured.

Related to #1881 and #728
2020-07-28 12:59:43 +01:00
Dan Brown
2f6ff07347 Merge branch 'auth' of git://github.com/benrubson/BookStack into benrubson-auth 2020-07-28 10:46:40 +01:00
Dan Brown
18f406d97b Started attachment drag/drop
Currently fighting between sortable and tinymce mechanisms which prevent
this working due to the different events stopping the drop event while
needing the dragover for cursor placement.
2020-07-28 10:45:28 +01:00
Dan Brown
76fcbd3752 Removed default anchor CSS filtering in dark mode
Due to causing content images to be rendered in unexpected ways.

- Also removed CSS filters from other image usage.
- Tweaked header CSS filtering to not be so aggressive.
- Forced WYSIWYG editor to be on its own layer since that would allow
massive larger performance increases in Safari, especially when using
dark mode.

Closes #2045.
Closes #2154.
2020-07-26 16:36:15 +01:00
Dan Brown
6e4132121c Updated pagination colors for visibility
Fixes #1839
2020-07-26 15:07:47 +01:00
Dan Brown
f5fefbdb06 Removed a few remaining vue references 2020-07-26 14:49:05 +01:00
Dan Brown
a46b248cf4 Fixed some image manager behaviour
fixed:
- Double click not working after tab usage.
- Synced edit form with select button.
2020-07-25 11:47:12 +01:00
Dan Brown
8213ea9a71 Fixed issue where URL params in image names would cause loading failure
Updated file name handling to route through str:slug to be cleaned up
a little.
Added testing to cover.

Fixes #2161
2020-07-25 11:18:40 +01:00
Dan Brown
03211ebea6 Removed unused tinymce imagetools plugin 2020-07-25 01:09:35 +01:00
Dan Brown
2bacc3c967 Removed vuejs from the project 2020-07-25 00:25:30 +01:00
Dan Brown
02dc3154e3 Converted image-manager to be component/HTML based
Instead of vue based.
2020-07-25 00:20:58 +01:00
Dan Brown
b6aa232205 Fixed issue where more images than expected could be deleted
When deleting images, images within the same directory, that have
a suffix of the delete image name, would also be deleted.

Added test to cover.
2020-07-24 23:41:59 +01:00
Dan Brown
b383f5776d Tweaked dropdown shadows a tad 2020-07-05 21:23:57 +01:00
Dan Brown
3bfd26bf86 Converted the page editor from vue to component 2020-07-05 21:18:17 +01:00
Dan Brown
9d6f574494 Updated attachment tests to align with front-end changes 2020-07-04 17:04:26 +01:00
Dan Brown
d41452f39c Finished breakdown of attachment vue into components 2020-07-04 16:53:02 +01:00
Dan Brown
14b6cd1091 Started migration of attachment manager from vue
- Created new dropzone component.
- Added standard component event system using custom DOM events.
- Added tabs component.
- Added ajax-delete-row component.
2020-06-30 22:12:45 +01:00
Dan Brown
8dc9689c6d Removed tests for removed ajax tag route 2020-06-29 23:46:08 +01:00
Dan Brown
181ae6d055 Fixed tag-manager loading on entity-creation 2020-06-29 23:40:34 +01:00
Dan Brown
573c4e26d5 Finished moving tag-manager from a vue to a component
Now tags load with the page, not via AJAX.
2020-06-29 22:11:03 +01:00
Dan Brown
4e107b9160 Started migrating tag manager JS to HTML-first component 2020-06-28 23:15:05 +01:00
Dan Brown
10305a4446 Converted entity-dash from vue to a component 2020-06-28 21:15:00 +01:00
Dan Brown
a5fa745749 Moved overlay component, migrated code-editor & added features
- Moved Code-editor from vue to component.
- Updated popup code so it background click only hides if the click
originated on the same background. Clicks within the popup will no
longer cause it to hide.
- Added session-level history tracking to code editor.
2020-06-28 00:06:47 +01:00
Dan Brown
9023f78cdc Merge branch 'master' of github.com:BookStackApp/BookStack 2020-06-27 17:19:05 +01:00
Dan Brown
8bc3e0f31a Merge branch 'master' of git://github.com/drzippie/BookStack into drzippie-master 2020-06-27 17:11:11 +01:00
Dan Brown
afed379c5c Merge pull request #2157 from Honvid/fix/lang_error
fix the translate error
2020-06-27 17:06:38 +01:00
Dan Brown
540119f133 Moved sass build out of webpack, updated npm deps
Moving sass out of webpack cleans the setup quite considerably and
brings a good speed improvement.
Made use of npm-run-all so the previous commands still run like before.
2020-06-27 16:52:26 +01:00
Dan Brown
d5de28c444 Merge branch 'use-dart-sass' of git://github.com/timoschwarzer/BookStack into timoschwarzer-use-dart-sass 2020-06-27 15:59:38 +01:00
Dan Brown
7a2e39212e Fixed empty search scenario 2020-06-27 13:37:18 +01:00
Dan Brown
715dee2d0e Converted search filters to not be vue based 2020-06-27 13:29:00 +01:00
Timo Schwarzer
0f55d776a6 Replace node-sass with dart-sass 2020-06-26 12:44:41 +02:00
Antonio Cortés (DrZippie)
d617dba61c removed test_slug_multi_byte_lower_casing and added new test test_slug_multi_byte_url_safe 2020-06-25 18:42:28 +02:00
Antonio Cortés (DrZippie)
ca202c1819 Added Illuminate\Support\Str::slug to generate slug from text to improve the creation of slugs with non-English characters 2020-06-25 18:08:13 +02:00
Dan Brown
76d02cd472 Started attempt at formalising component system used in BookStack
Added a document to try to define things.
Updated the loading so components are registed dynamically.
Added some standardised ways to reference other elems & define options
2020-06-24 20:38:08 +01:00
Honvid
118e31608a fix the bug for lang's extra letter. 2020-06-16 11:44:08 +08:00
Honvid
418fd9037f Merge pull request #1 from BookStackApp/master
sync the remote master
2020-06-10 07:46:06 +08:00
benrubson
9d7ce59b18 Move logFailedAccess into Activity 2020-05-23 15:37:38 +02:00
Dan Brown
71e7dd5894 Removed failing URL test
- Was found that the test was not testing the actual situation anyway.
- A work-around in the request creation, within testing, just happened
 to result in the desired outcome.

For reference: https://github.com/laravel/framework/pull/32345
2020-05-23 12:56:31 +01:00
Dan Brown
3502abdd49 Fixed revision issues caused by page fillable changes 2020-05-23 12:28:14 +01:00
Dan Brown
31514bae06 Updated framework and other deps 2020-05-23 11:50:44 +01:00
Dan Brown
19bfc8ad37 Prevented entity "Not Found" events from being logged
- Added testing to cover, which was more hassle than thought
  since Laravel did not have built in log test helpers, so:
- Added Log testing helper.

Related to #2110
2020-05-23 11:28:59 +01:00
benrubson
8f1f73defa Properly use env/config functions 2020-05-23 12:06:37 +02:00
Dan Brown
bf4a3b73f8 Updated listing endpoints to be clickable in api docs 2020-05-23 00:53:13 +01:00
Dan Brown
00c0815808 Fixed issue where updated page content would not be indexed
- Also updated html field of pages to not be fillable.
   (Since HTML should always go through app id parsing)

Related to #2042
2020-05-23 00:46:13 +01:00
Dan Brown
b61f950560 Incremented version number 2020-05-23 00:29:09 +01:00
Dan Brown
8a6cf0cdec Added chapters to the API 2020-05-23 00:28:41 +01:00
Dan Brown
24bad5034a Updated API auth to allow public user if given permission 2020-05-22 22:34:18 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
9666c8c0f7 Updated shelf-list view to enforce view permissions for child books
- Aligned shelf-homepage behaviour to match
- Updated testing to cover.

For #2111
2020-05-12 22:21:45 +01:00
benrubson
58df3ad956 Log failed accesses option 2020-05-03 16:20:02 +02:00
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
benrubson
12a9a45747 Log failed accesses 2020-02-09 10:01:33 +01:00
641 changed files with 18411 additions and 13512 deletions

View File

@@ -1,14 +1,24 @@
# This file, when named as ".env" in the root of your BookStack install
# folder, is used for the core configuration of the application.
# By default this file contains the most common required options but
# a full list of options can be found in the '.env.example.complete' file.
# NOTE: If any of your values contain a space or a hash you will need to
# wrap the entire value in quotes. (eg. MAIL_FROM_NAME="BookStack Mailer")
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# Application URL
# Remove the hash below and set a URL if using BookStack behind
# a proxy, if using a third-party authentication option.
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
#APP_URL=https://example.com
# All URLs in BookStack will be generated using this value
# to ensure URLs generated are consistent and secure.
# If you change this in the future you may need to run a command
# to update stored URLs in the database. Command example:
# php artisan bookstack:update-url https://old.example.com https://new.example.com
APP_URL=https://example.com
# Database details
DB_HOST=localhost
@@ -20,16 +30,15 @@ DB_PASSWORD=database_user_password
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sender options
MAIL_FROM_NAME=BookStack
# Mail sender details
MAIL_FROM_NAME="BookStack"
MAIL_FROM=bookstack@example.com
# SMTP mail options
# These settings can be checked using the "Send a Test Email"
# feature found in the "Settings > Maintenance" area of the system.
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# A full list of options can be found in the '.env.example.complete' file.
MAIL_ENCRYPTION=null

View File

@@ -238,9 +238,9 @@ DISABLE_EXTERNAL_SERVICES=false
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable draw.io integration
# Enable diagrams.net integration
# Can simply be true/false to enable/disable the integration.
# Alternatively, It can be URL to the draw.io instance you want to use.
# Alternatively, It can be URL to the diagrams.net instance you want to use.
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
DRAWIO=true
@@ -255,6 +255,14 @@ APP_VIEWS_BOOKSHELVES=grid
# If set to 'false' a limit will not be enforced.
REVISION_LIMIT=50
# Recycle Bin Lifetime
# The number of days that content will remain in the recycle bin before
# being considered for auto-removal. It is not a guarantee that content will
# be removed after this time.
# Set to 0 for no recycle bin functionality.
# Set to -1 for unlimited recycle bin lifetime.
RECYCLE_BIN_LIFETIME=30
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
@@ -265,9 +273,23 @@ ALLOW_CONTENT_SCRIPTS=false
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null
# A list of hosts that BookStack can be iframed within.
# Space separated if multiple. BookStack host domain is auto-inferred.
# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
# Setting this option will also auto-adjust cookies to be SameSite=None.
ALLOWED_IFRAME_HOSTS=null
# The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
# The number of API requests that can be made per minute by a single user.
API_REQUESTS_PER_MIN=180
API_REQUESTS_PER_MIN=180
# Enable the logging of failed email+password logins with the given message.
# The default log channel below uses the php 'error_log' function which commonly
# results in messages being output to the webserver error logs.
# The message can contain a %u parameter which will be replaced with the login
# user identifier (Username or email).
LOG_FAILED_LOGIN_MESSAGE=false
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver

3
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1,3 @@
# These are supported funding model platforms
github: [ssddanbrown]

17
.github/ISSUE_TEMPLATE/api_request.md vendored Normal file
View File

@@ -0,0 +1,17 @@
---
name: New API Endpoint or Feature
about: Request a new endpoint or API feature be added
labels: ":nut_and_bolt: API Request"
---
#### API Endpoint or Feature
Clearly describe what you'd like to have added to the API.
#### Use-Case
Explain the use-case that you're working-on that requires the above request.
#### Additional Context
If required, add any other context about the feature request here.

View File

@@ -61,7 +61,7 @@ Rodrigo Saczuk Niz (rodrigoniz) :: Portuguese, Brazilian
aekramer :: Dutch
JachuPL :: Polish
milesteg :: Hungarian
Beenbag :: German
Beenbag :: German; German Informal
Lett3rs :: Danish
Julian (julian.henneberg) :: German; German Informal
3GNWn :: Danish
@@ -98,3 +98,39 @@ Thinkverse (thinkverse) :: Swedish
alef (toishoki) :: Turkish
Robbert Feunekes (Muukuro) :: Dutch
seohyeon.joo :: Korean
Orenda (OREDNA) :: Bulgarian
Marek Pavelka (marapavelka) :: Czech
Venkinovec :: Czech
Tommy Ku (tommyku) :: Chinese Traditional; Japanese
Michał Bielejewski (bielej) :: Polish
jozefrebjak :: Slovak
Ikhwan Koo (Ikhwan.Koo) :: Korean
Whay (remkovdhoef) :: Dutch
jc7115 :: Chinese Traditional
주서현 (seohyeon.joo) :: Korean
ReadySystems :: Arabic
HFinch :: German; German Informal
brechtgijsens :: Dutch
Lowkey (v587ygq) :: Chinese Simplified
sdl-blue :: German Informal
sqlik :: Polish
Roy van Schaijk (royvanschaijk) :: Dutch
Simsimpicpic :: French
Zenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal
tatsuya.info :: Japanese
fadiapp :: Arabic
Jakub Bouček (jakubboucek) :: Czech
Marco (cdrfun) :: German
10935336 :: Chinese Simplified
孟繁阳 (FanyangMeng) :: Chinese Simplified
Andrej Močan (andrejm) :: Slovenian
gilane9_ :: Arabic
Raed alnahdi (raednahdi) :: Arabic
Xiphoseer :: German
MerlinSVK (merlinsvk) :: Slovak
Kauê Sena (kaue.sena.ks) :: Portuguese, Brazilian
MatthieuParis :: French
Douradinho :: Portuguese, Brazilian
Gaku Yaguchi (tama11) :: Japanese
johnroyer :: Chinese Traditional
jackaaa :: Chinese Traditional

58
.github/workflows/test-migrations.yml vendored Normal file
View File

@@ -0,0 +1,58 @@
name: test-migrations
on:
push:
branches:
- master
- release
pull_request:
branches:
- '*'
- '*/*'
- '!l10n_master'
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
php: [7.2, 7.4]
steps:
- uses: actions/checkout@v1
- name: Get Composer Cache Directory
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer packages
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}
- name: Start MySQL
run: |
sudo /etc/init.d/mysql start
- name: Create database & user
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED BY 'bookstack-test';"
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'
- name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --ansi
- name: Start migration test
run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
- name: Start migration:rollback test
run: |
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
- name: Start migration rerun test
run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing

View File

@@ -3,18 +3,19 @@
namespace BookStack\Actions;
use BookStack\Auth\User;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
/**
* @property string $key
* @property string $type
* @property User $user
* @property Entity $entity
* @property string $extra
* @property string $detail
* @property string $entity_type
* @property int $entity_id
* @property int $user_id
* @property int $book_id
*/
class Activity extends Model
{
@@ -32,20 +33,28 @@ class Activity extends Model
/**
* Get the user this activity relates to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Returns text from the language files, Looks up by using the
* activity key.
* Returns text from the language files, Looks up by using the activity key.
*/
public function getText()
public function getText(): string
{
return trans('activities.' . $this->key);
return trans('activities.' . $this->type);
}
/**
* Check if this activity is intended to be for an entity.
*/
public function isForEntity(): bool
{
return Str::startsWith($this->type, [
'page_', 'chapter_', 'book_', 'bookshelf_'
]);
}
/**
@@ -53,6 +62,6 @@ class Activity extends Model
*/
public function isSimilarTo(Activity $activityB): bool
{
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
return [$this->type, $this->entity_type, $this->entity_id] === [$activityB->type, $activityB->entity_type, $activityB->entity_id];
}
}

View File

@@ -2,56 +2,59 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
use BookStack\Entities\Entity;
use Illuminate\Support\Collection;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Log;
class ActivityService
{
protected $activity;
protected $user;
protected $permissionService;
/**
* ActivityService constructor.
*/
public function __construct(Activity $activity, PermissionService $permissionService)
{
$this->activity = $activity;
$this->permissionService = $permissionService;
$this->user = user();
}
/**
* Add activity data to database.
* Add activity data to database for an entity.
*/
public function add(Entity $entity, string $activityKey, ?int $bookId = null)
public function addForEntity(Entity $entity, string $type)
{
$activity = $this->newActivityForUser($activityKey, $bookId);
$activity = $this->newActivityForUser($type);
$entity->activity()->save($activity);
$this->setNotification($activityKey);
$this->setNotification($type);
}
/**
* Adds a activity history with a message, without binding to a entity.
* Add a generic activity event to the database.
* @param string|Loggable $detail
*/
public function addMessage(string $activityKey, string $message, ?int $bookId = null)
public function add(string $type, $detail = '')
{
$this->newActivityForUser($activityKey, $bookId)->forceFill([
'extra' => $message
])->save();
if ($detail instanceof Loggable) {
$detail = $detail->logDescriptor();
}
$this->setNotification($activityKey);
$activity = $this->newActivityForUser($type);
$activity->detail = $detail;
$activity->save();
$this->setNotification($type);
}
/**
* Get a new activity instance for the current user.
*/
protected function newActivityForUser(string $key, ?int $bookId = null): Activity
protected function newActivityForUser(string $type): Activity
{
return $this->activity->newInstance()->forceFill([
'key' => strtolower($key),
'user_id' => $this->user->id,
'book_id' => $bookId ?? 0,
'type' => strtolower($type),
'user_id' => user()->id,
]);
}
@@ -60,15 +63,13 @@ class ActivityService
* and instead uses the 'extra' field with the entities name.
* Used when an entity is deleted.
*/
public function removeEntity(Entity $entity): Collection
public function removeEntity(Entity $entity)
{
$activities = $entity->activity()->get();
$entity->activity()->update([
'extra' => $entity->name,
'entity_id' => 0,
'entity_type' => '',
'detail' => $entity->name,
'entity_id' => null,
'entity_type' => null,
]);
return $activities;
}
/**
@@ -93,17 +94,30 @@ class ActivityService
*/
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
{
/** @var [string => int[]] $queryIds */
$queryIds = [$entity->getMorphClass() => [$entity->id]];
if ($entity->isA('book')) {
$query = $this->activity->newQuery()->where('book_id', '=', $entity->id);
} else {
$query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id);
$queryIds[(new Chapter)->getMorphClass()] = $entity->chapters()->visible()->pluck('id');
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['entity', 'user.avatar'])
if ($entity->isA('book') || $entity->isA('chapter')) {
$queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id');
}
$query = $this->activity->newQuery();
$query->where(function (Builder $query) use ($queryIds) {
foreach ($queryIds as $morphClass => $idArr) {
$query->orWhere(function (Builder $innerQuery) use ($morphClass, $idArr) {
$innerQuery->where('entity_type', '=', $morphClass)
->whereIn('entity_id', $idArr);
});
}
});
$activity = $query->orderBy('created_at', 'desc')
->with(['entity' => function (Relation $query) {
$query->withTrashed();
}, 'user.avatar'])
->skip($count * ($page - 1))
->take($count)
->get();
@@ -151,12 +165,28 @@ class ActivityService
/**
* Flashes a notification message to the session if an appropriate message is available.
*/
protected function setNotification(string $activityKey)
protected function setNotification(string $type)
{
$notificationTextKey = 'activities.' . $activityKey . '_notification';
$notificationTextKey = 'activities.' . $type . '_notification';
if (trans()->has($notificationTextKey)) {
$message = trans($notificationTextKey);
session()->flash('success', $message);
}
}
/**
* Log out a failed login attempt, Providing the given username
* as part of the message if the '%u' string is used.
*/
public function logFailedLogin(string $username)
{
$message = config('logging.failed_login.message');
if (!$message) {
return;
}
$message = str_replace("%u", $username, $message);
$channel = config('logging.failed_login.channel');
Log::channel($channel)->warning($message);
}
}

View File

@@ -0,0 +1,51 @@
<?php namespace BookStack\Actions;
class ActivityType
{
const PAGE_CREATE = 'page_create';
const PAGE_UPDATE = 'page_update';
const PAGE_DELETE = 'page_delete';
const PAGE_RESTORE = 'page_restore';
const PAGE_MOVE = 'page_move';
const CHAPTER_CREATE = 'chapter_create';
const CHAPTER_UPDATE = 'chapter_update';
const CHAPTER_DELETE = 'chapter_delete';
const CHAPTER_MOVE = 'chapter_move';
const BOOK_CREATE = 'book_create';
const BOOK_UPDATE = 'book_update';
const BOOK_DELETE = 'book_delete';
const BOOK_SORT = 'book_sort';
const BOOKSHELF_CREATE = 'bookshelf_create';
const BOOKSHELF_UPDATE = 'bookshelf_update';
const BOOKSHELF_DELETE = 'bookshelf_delete';
const COMMENTED_ON = 'commented_on';
const PERMISSIONS_UPDATE = 'permissions_update';
const SETTINGS_UPDATE = 'settings_update';
const MAINTENANCE_ACTION_RUN = 'maintenance_action_run';
const RECYCLE_BIN_EMPTY = 'recycle_bin_empty';
const RECYCLE_BIN_RESTORE = 'recycle_bin_restore';
const RECYCLE_BIN_DESTROY = 'recycle_bin_destroy';
const USER_CREATE = 'user_create';
const USER_UPDATE = 'user_update';
const USER_DELETE = 'user_delete';
const API_TOKEN_CREATE = 'api_token_create';
const API_TOKEN_UPDATE = 'api_token_update';
const API_TOKEN_DELETE = 'api_token_delete';
const ROLE_CREATE = 'role_create';
const ROLE_UPDATE = 'role_update';
const ROLE_DELETE = 'role_delete';
const AUTH_PASSWORD_RESET = 'auth_password_reset_request';
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
const AUTH_LOGIN = 'auth_login';
const AUTH_REGISTER = 'auth_register';
}

View File

@@ -1,26 +1,34 @@
<?php namespace BookStack\Actions;
use BookStack\Ownable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Comment extends Ownable
/**
* @property string text
* @property string html
* @property int|null parent_id
* @property int local_id
*/
class Comment extends Model
{
protected $fillable = ['text', 'html', 'parent_id'];
use HasCreatorAndUpdater;
protected $fillable = ['text', 'parent_id'];
protected $appends = ['created', 'updated'];
/**
* Get the entity that this comment belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
public function entity(): MorphTo
{
return $this->morphTo('entity');
}
/**
* Check if a comment has been updated since creation.
* @return bool
*/
public function isUpdated()
public function isUpdated(): bool
{
return $this->updated_at->timestamp > $this->created_at->timestamp;
}

View File

@@ -1,23 +1,21 @@
<?php namespace BookStack\Actions;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use League\CommonMark\CommonMarkConverter;
use BookStack\Facades\Activity as ActivityService;
/**
* 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 +23,72 @@ 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);
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
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

@@ -2,13 +2,10 @@
use BookStack\Model;
/**
* Class Attribute
* @package BookStack
*/
class Tag extends Model
{
protected $fillable = ['name', 'value', 'order'];
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
/**
* Get the entity that this tag belongs to

View File

@@ -1,72 +1,32 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use DB;
use Illuminate\Support\Collection;
/**
* Class TagRepo
* @package BookStack\Repos
*/
class TagRepo
{
protected $tag;
protected $entity;
protected $permissionService;
/**
* TagRepo constructor.
* @param \BookStack\Actions\Tag $attr
* @param \BookStack\Entities\Entity $ent
* @param \BookStack\Auth\Permissions\PermissionService $ps
*/
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
public function __construct(Tag $tag, PermissionService $ps)
{
$this->tag = $attr;
$this->entity = $ent;
$this->tag = $tag;
$this->permissionService = $ps;
}
/**
* Get an entity instance of its particular type.
* @param $entityType
* @param $entityId
* @param string $action
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
$entityInstance = $this->entity->getEntityInstance($entityType);
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
return $searchQuery->first();
}
/**
* Get all tags for a particular entity.
* @param string $entityType
* @param int $entityId
* @return mixed
*/
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) {
return collect();
}
return $entity->tags;
}
/**
* Get tag name suggestions from scanning existing tag names.
* If no search term is given the 50 most popular tag names are provided.
* @param $searchTerm
* @return array
*/
public function getNameSuggestions($searchTerm = false)
public function getNameSuggestions(?string $searchTerm): Collection
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@@ -82,13 +42,10 @@ class TagRepo
* Get tag value suggestions from scanning existing tag values.
* If no search is given the 50 most popular values are provided.
* Passing a tagName will only find values for a tags with a particular name.
* @param $searchTerm
* @param $tagName
* @return array
*/
public function getValueSuggestions($searchTerm = false, $tagName = false)
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
@@ -96,7 +53,7 @@ class TagRepo
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) {
if ($tagName) {
$query = $query->where('name', '=', $tagName);
}
@@ -106,35 +63,28 @@ class TagRepo
/**
* Save an array of tags to an entity
* @param \BookStack\Entities\Entity $entity
* @param array $tags
* @return array|\Illuminate\Database\Eloquent\Collection
*/
public function saveTagsToEntity(Entity $entity, $tags = [])
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
{
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') {
continue;
}
$newTags[] = $this->newInstanceFromInput($tag);
}
$newTags = collect($tags)->filter(function ($tag) {
return boolval(trim($tag['name']));
})->map(function ($tag) {
return $this->newInstanceFromInput($tag);
})->all();
return $entity->tags()->saveMany($newTags);
}
/**
* Create a new Tag instance from user input.
* @param $input
* @return \BookStack\Actions\Tag
* Input must be an array with a 'name' and an optional 'value' key.
*/
protected function newInstanceFromInput($input)
protected function newInstanceFromInput(array $input): Tag
{
$name = trim($input['name']);
$value = isset($input['value']) ? trim($input['value']) : '';
// Any other modification or cleanup required can go here
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
}
}

View File

@@ -1,8 +1,8 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Book;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use DB;
use Illuminate\Support\Collection;
@@ -28,7 +28,7 @@ class ViewService
/**
* Add a view to the given entity.
* @param \BookStack\Entities\Entity $entity
* @param \BookStack\Entities\Models\Entity $entity
* @return int
*/
public function add(Entity $entity)
@@ -74,34 +74,36 @@ class ViewService
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
return $query->with('viewable')
->skip($skipCount)
->take($count)
->get()
->pluck('viewable')
->filter();
}
/**
* Get all recently viewed entities for the current user.
* @param int $count
* @param int $page
* @param Entity|bool $filterModel
* @return mixed
*/
public function getUserRecentlyViewed($count = 10, $page = 0, $filterModel = false)
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
if ($filterModel) {
$query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
$all = collect();
/** @var Entity $instance */
foreach ($this->entityProvider->all() as $name => $instance) {
$items = $instance::visible()->withLastView()
->orderBy('last_viewed_at', 'desc')
->skip($count * ($page - 1))
->take($count)
->get();
$all = $all->concat($items);
}
$query = $query->where('user_id', '=', $user->id);
$viewables = $query->with('viewable')->orderBy('updated_at', 'desc')
->skip($count * $page)->take($count)->get()->pluck('viewable');
return $viewables;
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
}
/**

View File

@@ -1,7 +1,9 @@
<?php namespace BookStack\Api;
use BookStack\Http\Controllers\Api\ApiController;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use ReflectionClass;
@@ -14,10 +16,27 @@ class ApiDocsGenerator
protected $reflectionClasses = [];
protected $controllerClasses = [];
/**
* Load the docs form the cache if existing
* otherwise generate and store in the cache.
*/
public static function generateConsideringCache(): Collection
{
$appVersion = trim(file_get_contents(base_path('version')));
$cacheKey = 'api-docs::' . $appVersion;
if (Cache::has($cacheKey) && config('app.env') === 'production') {
$docs = Cache::get($cacheKey);
} else {
$docs = (new static())->generate();
Cache::put($cacheKey, $docs, 60 * 24);
}
return $docs;
}
/**
* Generate API documentation.
*/
public function generate(): Collection
protected function generate(): Collection
{
$apiRoutes = $this->getFlatApiRoutes();
$apiRoutes = $this->loadDetailsFromControllers($apiRoutes);
@@ -58,7 +77,7 @@ class ApiDocsGenerator
/**
* Load body params and their rules by inspecting the given class and method name.
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws BindingResolutionException
*/
protected function getBodyParamsFromClass(string $className, string $methodName): ?array
{

View File

@@ -1,11 +1,21 @@
<?php namespace BookStack\Api;
use BookStack\Auth\User;
use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Carbon;
class ApiToken extends Model
/**
* Class ApiToken
* @property int $id
* @property string $token_id
* @property string $secret
* @property string $name
* @property Carbon $expires_at
* @property User $user
*/
class ApiToken extends Model implements Loggable
{
protected $fillable = ['name', 'expires_at'];
protected $casts = [
@@ -28,4 +38,12 @@ class ApiToken extends Model
{
return Carbon::now()->addYears(100)->format('Y-m-d');
}
/**
* @inheritdoc
*/
public function logDescriptor(): string
{
return "({$this->id}) {$this->name}; User: {$this->user->logDescriptor()}";
}
}

View File

@@ -3,6 +3,8 @@
use BookStack\Auth\Role;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ExternalAuthService
{
@@ -39,22 +41,14 @@ class ExternalAuthService
/**
* Match an array of group names to BookStack system roles.
* Formats group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchGroupsToSystemsRoles(array $groupNames)
protected function matchGroupsToSystemsRoles(array $groupNames): Collection
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});

View File

@@ -15,8 +15,6 @@ use Illuminate\Contracts\Session\Session;
* guard with 'remember' functionality removed. Basic auth and event emission
* has also been removed to keep this simple. Designed to be extended by external
* Auth Guards.
*
* @package Illuminate\Auth
*/
class ExternalBaseSessionGuard implements StatefulGuard
{

View File

@@ -9,8 +9,6 @@ namespace BookStack\Auth\Access\Guards;
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
*
* @package BookStack\Auth\Access\Guards
*/
class Saml2SessionGuard extends ExternalBaseSessionGuard
{

View File

@@ -4,7 +4,6 @@
* Class Ldap
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
* Allows the standard LDAP functions to be mocked for testing.
* @package BookStack\Services
*/
class Ldap
{

View File

@@ -1,9 +1,11 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use Exception;
class RegistrationService
@@ -57,7 +59,7 @@ class RegistrationService
// Ensure user does not already exist
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
if ($alreadyUser) {
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
}
// Create the user
@@ -68,18 +70,20 @@ class RegistrationService
$newUser->socialAccounts()->save($socialAccount);
}
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
// Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
$newUser->save();
$message = '';
try {
$this->emailConfirmationService->sendConfirmation($newUser);
session()->flash('sent-email-confirmation', true);
} catch (Exception $e) {
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
throw new UserRegistrationException($message, '/register/confirm');
}
return $newUser;
@@ -106,13 +110,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

@@ -1,9 +1,11 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\SamlException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Support\Str;
use OneLogin\Saml2\Auth;
@@ -311,7 +313,6 @@ class Saml2Service extends ExternalAuthService
/**
* Get the user from the database for the specified details.
* @throws SamlException
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User
@@ -373,6 +374,7 @@ class Saml2Service extends ExternalAuthService
}
auth()->login($user);
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
return $user;
}
}

View File

@@ -1,10 +1,12 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider;
@@ -98,6 +100,7 @@ class SocialAuthService
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
auth()->login($socialAccount->user);
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
return redirect()->intended('/');
}

View File

@@ -1,27 +1,28 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class JointPermission extends Model
{
protected $primaryKey = null;
public $timestamps = false;
/**
* Get the role that this points to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function entity()
public function entity(): MorphOne
{
return $this->morphOne(Entity::class, 'entity');
}

View File

@@ -2,13 +2,12 @@
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Page;
use BookStack\Ownable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
@@ -51,11 +50,6 @@ class PermissionService
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db
* @param EntityProvider $entityProvider
*/
public function __construct(
JointPermission $jointPermission,
@@ -82,7 +76,7 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param \BookStack\Entities\Entity[] $entities
* @param \BookStack\Entities\Models\Entity[] $entities
*/
protected function readyEntityCache($entities = [])
{
@@ -119,7 +113,7 @@ class PermissionService
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return \BookStack\Entities\Book
* @return \BookStack\Entities\Models\Book
*/
protected function getChapter($chapterId)
{
@@ -176,7 +170,7 @@ class PermissionService
});
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
$this->entityProvider->bookshelf->newQuery()->withTrashed()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
@@ -188,11 +182,11 @@ class PermissionService
*/
protected function bookFetchQuery()
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
return $this->entityProvider->book->withTrashed()->newQuery()
->select(['id', 'restricted', 'owned_by'])->with(['chapters' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
}]);
}
@@ -238,7 +232,7 @@ class PermissionService
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param \BookStack\Entities\Entity $entity
* @param \BookStack\Entities\Models\Entity $entity
* @throws \Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
@@ -294,7 +288,7 @@ class PermissionService
});
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
@@ -333,7 +327,7 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param \BookStack\Entities\Entity[] $entities
* @param \BookStack\Entities\Models\Entity[] $entities
* @throws \Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
@@ -414,7 +408,7 @@ class PermissionService
/**
* Get the actions related to an entity.
* @param \BookStack\Entities\Entity $entity
* @param \BookStack\Entities\Models\Entity $entity
* @return array
*/
protected function getActions(Entity $entity)
@@ -500,7 +494,7 @@ class PermissionService
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
* @param \BookStack\Entities\Entity $entity
* @param \BookStack\Entities\Models\Entity $entity
* @param Role $role
* @param $action
* @param $permissionAll
@@ -516,21 +510,19 @@ class PermissionService
'action' => $action,
'has_permission' => $permissionAll,
'has_permission_own' => $permissionOwn,
'created_by' => $entity->getRawAttribute('created_by')
'owned_by' => $entity->getRawAttribute('owned_by')
];
}
/**
* Checks if an entity has a restriction set upon it.
* @param Ownable $ownable
* @param $permission
* @return bool
* @param HasCreatorAndUpdater|HasOwner $ownable
*/
public function checkOwnableUserAccess(Ownable $ownable, $permission)
public function checkOwnableUserAccess(Model $ownable, string $permission): bool
{
$explodedPermission = explode('-', $permission);
$baseQuery = $ownable->where('id', '=', $ownable->id);
$baseQuery = $ownable->newQuery()->where('id', '=', $ownable->id);
$action = end($explodedPermission);
$this->currentAction = $action;
@@ -541,7 +533,8 @@ class PermissionService
$allPermission = $this->currentUser() && $this->currentUser()->can($permission . '-all');
$ownPermission = $this->currentUser() && $this->currentUser()->can($permission . '-own');
$this->currentAction = 'view';
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->created_by;
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField;
return ($allPermission || ($isOwner && $ownPermission));
}
@@ -574,7 +567,7 @@ class PermissionService
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
->where('owned_by', '=', $userId);
});
});
@@ -591,7 +584,7 @@ class PermissionService
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param \BookStack\Entities\Entity $entity
* @param \BookStack\Entities\Models\Entity $entity
* @param $action
* @return bool|mixed
*/
@@ -623,7 +616,7 @@ class PermissionService
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
});
@@ -647,7 +640,7 @@ class PermissionService
$query->where('has_permission', '=', true)
->orWhere(function (Builder $query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
});
@@ -664,7 +657,7 @@ class PermissionService
$query->where('draft', '=', false)
->orWhere(function (Builder $query) {
$query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
}
@@ -672,7 +665,7 @@ class PermissionService
/**
* Add restrictions for a generic entity
* @param string $entityType
* @param Builder|\BookStack\Entities\Entity $query
* @param Builder|\BookStack\Entities\Models\Entity $query
* @param string $action
* @return Builder
*/
@@ -684,7 +677,7 @@ class PermissionService
$query->where('draft', '=', false)
->orWhere(function ($query) {
$query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
}
@@ -718,7 +711,7 @@ class PermissionService
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
});
@@ -754,7 +747,7 @@ class PermissionService
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
});

View File

@@ -1,9 +1,11 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use Illuminate\Support\Str;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo
{
@@ -16,11 +18,8 @@ class PermissionsRepo
/**
* PermissionsRepo constructor.
* @param RolePermission $permission
* @param Role $role
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
*/
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
{
$this->permission = $permission;
$this->role = $role;
@@ -29,64 +28,51 @@ class PermissionsRepo
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->all();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
public function getAllRolesExcept(Role $role): Collection
{
return $this->role->where('id', '!=', $role->id)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
public function getRoleById($id): Role
{
return $this->role->findOrFail($id);
return $this->role->newQuery()->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
public function saveNewRole(array $roleData): Role
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(Str::random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionService->buildJointPermissionForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
return $role;
}
/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
*/
public function updateRole($roleId, $roleData)
public function updateRole($roleId, array $roleData)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if ($role->system_name === 'admin') {
@@ -104,20 +90,24 @@ class PermissionsRepo
$role->fill($roleData);
$role->save();
$this->permissionService->buildJointPermissionForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
}
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
public function assignRolePermissions(Role $role, $permissionNameArray = [])
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray && count($permissionNameArray) > 0) {
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
if ($permissionNameArray) {
$permissions = $this->permission->newQuery()
->whereIn('name', $permissionNameArray)
->pluck('id')
->toArray();
}
$role->permissions()->sync($permissions);
}
@@ -126,13 +116,13 @@ class PermissionsRepo
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
@@ -142,14 +132,15 @@ class PermissionsRepo
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
$newRole = $this->role->newQuery()->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$users = $role->users()->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}
$this->permissionService->deleteJointPermissionsForRole($role);
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}
}

View File

@@ -3,6 +3,9 @@
use BookStack\Auth\Role;
use BookStack\Model;
/**
* @property int $id
*/
class RolePermission extends Model
{
/**

View File

@@ -2,16 +2,21 @@
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class Role
* @property int $id
* @property string $display_name
* @property string $description
* @property string $external_auth_id
* @package BookStack\Auth
* @property string $system_name
*/
class Role extends Model
class Role extends Model implements Loggable
{
protected $fillable = ['display_name', 'description', 'external_auth_id'];
@@ -19,16 +24,15 @@ class Role extends Model
/**
* The roles that belong to the role.
*/
public function users()
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
* Get all related JointPermissions.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function jointPermissions()
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
@@ -36,17 +40,15 @@ class Role extends Model
/**
* The RolePermissions that belong to the role.
*/
public function permissions()
public function permissions(): BelongsToMany
{
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
/**
* Check if this role has a permission.
* @param $permissionName
* @return bool
*/
public function hasPermission($permissionName)
public function hasPermission(string $permissionName): bool
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
@@ -59,7 +61,6 @@ class Role extends Model
/**
* Add a permission to this role.
* @param RolePermission $permission
*/
public function attachPermission(RolePermission $permission)
{
@@ -68,7 +69,6 @@ class Role extends Model
/**
* Detach a single permission from this role.
* @param RolePermission $permission
*/
public function detachPermission(RolePermission $permission)
{
@@ -76,40 +76,42 @@ class Role extends Model
}
/**
* Get the role object for the specified role.
* @param $roleName
* @return Role
* Get the role of the specified display name.
*/
public static function getRole($roleName)
public static function getRole(string $displayName): ?Role
{
return static::query()->where('name', '=', $roleName)->first();
return static::query()->where('display_name', '=', $displayName)->first();
}
/**
* Get the role object for the specified system role.
* @param $roleName
* @return Role
*/
public static function getSystemRole($roleName)
public static function getSystemRole(string $systemName): ?Role
{
return static::query()->where('system_name', '=', $roleName)->first();
return static::query()->where('system_name', '=', $systemName)->first();
}
/**
* Get all visible roles
* @return mixed
*/
public static function visible()
public static function visible(): Collection
{
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
}
/**
* Get the roles that can be restricted.
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public static function restrictable()
public static function restrictable(): Collection
{
return static::query()->where('system_name', '!=', 'admin')->get();
}
/**
* @inheritdoc
*/
public function logDescriptor(): string
{
return "({$this->id}) {$this->display_name}";
}
}

View File

@@ -1,8 +1,14 @@
<?php namespace BookStack\Auth;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
class SocialAccount extends Model
/**
* Class SocialAccount
* @property string $driver
* @property User $user
*/
class SocialAccount extends Model implements Loggable
{
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
@@ -11,4 +17,12 @@ class SocialAccount extends Model
{
return $this->belongsTo(User::class);
}
/**
* @inheritDoc
*/
public function logDescriptor(): string
{
return "{$this->driver}; {$this->user->logDescriptor()}";
}
}

View File

@@ -1,21 +1,25 @@
<?php namespace BookStack\Auth;
use BookStack\Api\ApiToken;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
use BookStack\Notifications\ResetPassword;
use BookStack\Uploads\Image;
use Carbon\Carbon;
use Exception;
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Collection;
/**
* Class User
* @package BookStack\Auth
* @property string $id
* @property string $name
* @property string $email
@@ -27,7 +31,7 @@ use Illuminate\Notifications\Notifiable;
* @property string $external_auth_id
* @property string $system_name
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
{
use Authenticatable, CanResetPassword, Notifiable;
@@ -43,18 +47,20 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $fillable = ['name', 'email'];
protected $casts = ['last_activity_at' => 'datetime'];
/**
* The attributes excluded from the model's JSON form.
* @var array
*/
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at',
'created_at', 'updated_at', 'image_id',
];
/**
* This holds the user's permissions when loaded.
* @var array
* @var ?Collection
*/
protected $permissions;
@@ -101,12 +107,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasRole($role)
public function hasRole($roleId): bool
{
return $this->roles->pluck('name')->contains($role);
return $this->roles->pluck('id')->contains($roleId);
}
/**
@@ -130,40 +134,48 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}
}
/**
* Get all permissions belonging to a the current user.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function permissions($cache = true)
{
if (isset($this->permissions) && $cache) {
return $this->permissions;
}
$this->load('roles.permissions');
$permissions = $this->roles->map(function ($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
}
/**
* Check if the user has a particular permission.
* @param $permissionName
* @return bool
*/
public function can($permissionName)
public function can(string $permissionName): bool
{
if ($this->email === 'guest') {
return false;
}
return $this->permissions()->pluck('name')->contains($permissionName);
return $this->permissions()->contains($permissionName);
}
/**
* Get all permissions belonging to a the current user.
*/
protected function permissions(): Collection
{
if (isset($this->permissions)) {
return $this->permissions;
}
$this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
->select('role_permissions.name as name')->distinct()
->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
->where('ru.user_id', '=', $this->id)
->get()
->pluck('name');
return $this->permissions;
}
/**
* Clear any cached permissions on this instance.
*/
public function clearPermissionCache()
{
$this->permissions = null;
}
/**
* Attach a role to this user.
* @param Role $role
*/
public function attachRole(Role $role)
{
@@ -172,7 +184,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the social account associated with this user.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
* @return HasMany
*/
public function socialAccounts()
{
@@ -209,7 +221,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
try {
$avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
} catch (\Exception $err) {
} catch (Exception $err) {
$avatar = $default;
}
return $avatar;
@@ -217,7 +229,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the avatar for the user.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
* @return BelongsTo
*/
public function avatar()
{
@@ -232,6 +244,19 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->hasMany(ApiToken::class);
}
/**
* Get the last activity time for this user.
*/
public function scopeWithLastActivityAt(Builder $query)
{
$query->addSelect(['activities.created_at as last_activity_at'])
->leftJoinSub(function (\Illuminate\Database\Query\Builder $query) {
$query->from('activities')->select('user_id')
->selectRaw('max(created_at) as created_at')
->groupBy('user_id');
}, 'activities', 'users.id', '=', 'activities.user_id');
}
/**
* Get the url for editing this user.
*/
@@ -277,4 +302,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
$this->notify(new ResetPassword($token));
}
/**
* @inheritdoc
*/
public function logDescriptor(): string
{
return "({$this->id}) {$this->name}";
}
}

View File

@@ -1,31 +1,32 @@
<?php namespace BookStack\Auth;
use Activity;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
{
protected $user;
protected $role;
protected $userAvatar;
/**
* UserRepo constructor.
*/
public function __construct(User $user, Role $role)
public function __construct(UserAvatars $userAvatar)
{
$this->user = $user;
$this->role = $role;
$this->userAvatar = $userAvatar;
}
/**
@@ -33,36 +34,36 @@ class UserRepo
*/
public function getByEmail(string $email): ?User
{
return $this->user->where('email', '=', $email)->first();
return User::query()->where('email', '=', $email)->first();
}
/**
* @param int $id
* @return User
* Get a user by their ID.
*/
public function getById($id)
public function getById(int $id): User
{
return $this->user->newQuery()->findOrFail($id);
return User::query()->findOrFail($id);
}
/**
* Get all the users with their permissions.
* @return Builder|static
*/
public function getAllUsers()
public function getAllUsers(): Collection
{
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
return User::query()->with('roles', 'avatar')->orderBy('name', 'asc')->get();
}
/**
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return Builder|static
*/
public function getAllUsersPaginatedAndSorted($count, $sortData)
public function getAllUsersPaginatedAndSorted(int $count, array $sortData): LengthAwarePaginator
{
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
$sort = $sortData['sort'];
$query = User::query()->select(['*'])
->withLastActivityAt()
->with(['roles', 'avatar'])
->orderBy($sort, $sortData['order']);
if ($sortData['search']) {
$term = '%' . $sortData['search'] . '%';
@@ -89,14 +90,12 @@ class UserRepo
/**
* Assign a user to a system-level role.
* @param User $user
* @param $systemRoleName
* @throws NotFoundException
*/
public function attachSystemRole(User $user, $systemRoleName)
public function attachSystemRole(User $user, string $systemRoleName)
{
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
if ($role === null) {
$role = Role::getSystemRole($systemRoleName);
if (is_null($role)) {
throw new NotFoundException("Role '{$systemRoleName}' not found");
}
$user->attachRole($role);
@@ -104,26 +103,23 @@ class UserRepo
/**
* Checks if the give user is the only admin.
* @param User $user
* @return bool
*/
public function isOnlyAdmin(User $user)
public function isOnlyAdmin(User $user): bool
{
if (!$user->hasSystemRole('admin')) {
return false;
}
$adminRole = $this->role->getSystemRole('admin');
if ($adminRole->users->count() > 1) {
$adminRole = Role::getSystemRole('admin');
if ($adminRole->users()->count() > 1) {
return false;
}
return true;
}
/**
* Set the assigned user roles via an array of role IDs.
* @param User $user
* @param array $roles
* @throws UserUpdateException
*/
public function setUserRoles(User $user, array $roles)
@@ -138,14 +134,11 @@ class UserRepo
/**
* Check if the given user is the last admin and their new roles no longer
* contains the admin role.
* @param User $user
* @param array $newRoles
* @return bool
*/
protected function demotingLastAdmin(User $user, array $newRoles) : bool
{
if ($this->isOnlyAdmin($user)) {
$adminRole = $this->role->getSystemRole('admin');
$adminRole = Role::getSystemRole('admin');
if (!in_array(strval($adminRole->id), $newRoles)) {
return true;
}
@@ -159,41 +152,59 @@ class UserRepo
*/
public function create(array $data, bool $emailConfirmed = false): User
{
return $this->user->forceCreate([
$details = [
'name' => $data['name'],
'email' => $data['email'],
'password' => bcrypt($data['password']),
'email_confirmed' => $emailConfirmed,
'external_auth_id' => $data['external_auth_id'] ?? '',
]);
];
return User::query()->forceCreate($details);
}
/**
* Remove the given user from storage, Delete all related content.
* @param User $user
* @throws Exception
*/
public function destroy(User $user)
public function destroy(User $user, ?int $newOwnerId = null)
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
$user->delete();
// Delete user profile images
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {
$this->migrateOwnership($user, $newOwner);
}
}
}
/**
* Migrate ownership of items in the system from one user to another.
*/
protected function migrateOwnership(User $fromUser, User $toUser)
{
$entities = (new EntityProvider)->all();
foreach ($entities as $instance) {
$instance->newQuery()->where('owned_by', '=', $fromUser->id)
->update(['owned_by' => $toUser->id]);
}
}
/**
* Get the latest activity for a user.
* @param User $user
* @param int $count
* @param int $page
* @return array
*/
public function getActivity(User $user, $count = 20, $page = 0)
public function getActivity(User $user, int $count = 20, int $page = 0): array
{
return Activity::userActivity($user, $count, $page);
}
@@ -234,33 +245,22 @@ class UserRepo
/**
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->newQuery()->orderBy('name', 'asc')->get();
return Role::query()->orderBy('display_name', 'asc')->get();
}
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
* @param User $user
* @return bool
*/
public function downloadAndAssignUserAvatar(User $user)
public function downloadAndAssignUserAvatar(User $user): void
{
if (!Images::avatarFetchEnabled()) {
return false;
}
try {
$avatar = Images::saveUserAvatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
$this->userAvatar->fetchAndAssignToUser($user);
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
return false;
}
}
}

View File

@@ -31,6 +31,13 @@ return [
// If set to false then a limit will not be enforced.
'revision_limit' => env('REVISION_LIMIT', 50),
// The number of days that content will remain in the recycle bin before
// being considered for auto-removal. It is not a guarantee that content will
// be removed after this time.
// Set to 0 for no recycle bin functionality.
// Set to -1 for unlimited recycle bin lifetime.
'recycle_bin_lifetime' => env('RECYCLE_BIN_LIFETIME', 30),
// Allow <script> tags to entered within page content.
// <script> tags are escaped by default.
// Even when overridden the WYSIWYG editor may still escape script content.
@@ -45,6 +52,10 @@ return [
// and used by BookStack in URL generation.
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
// A list of hosts that BookStack can be iframed within.
// Space separated if multiple. BookStack host domain is auto-inferred.
'iframe_hosts' => env('ALLOWED_IFRAME_HOSTS', null),
// Application timezone for back-end date functions.
'timezone' => env('APP_TIMEZONE', 'UTC'),
@@ -52,7 +63,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
// Application Fallback Locale
'fallback_locale' => 'en',
@@ -117,6 +128,7 @@ return [
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class,
BookStack\Providers\CustomValidationServiceProvider::class,
],
/*

View File

@@ -42,13 +42,6 @@ return [
'root' => storage_path(),
],
'ftp' => [
'driver' => 'ftp',
'host' => 'ftp.example.com',
'username' => 'your-username',
'password' => 'your-password',
],
's3' => [
'driver' => 's3',
'key' => env('STORAGE_S3_KEY', 'your-key'),
@@ -59,16 +52,6 @@ return [
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
'rackspace' => [
'driver' => 'rackspace',
'username' => 'your-username',
'key' => 'your-key',
'container' => 'your-container',
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
'region' => 'IAD',
'url_type' => 'publicURL',
],
],
];

View File

@@ -1,5 +1,7 @@
<?php
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
@@ -73,10 +75,38 @@ return [
'level' => 'debug',
],
// Custom errorlog implementation that logs out a plain,
// non-formatted message intended for the webserver log.
'errorlog_plain_webserver' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => ErrorLogHandler::class,
'handler_with' => [4],
'formatter' => LineFormatter::class,
'formatter_with' => [
'format' => "%message%",
],
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
// Testing channel
// Uses a shared testing instance during tests
// so that logs can be checked against.
'testing' => [
'driver' => 'testing',
],
],
// Failed Login Message
// Allows a configurable message to be logged when a login request fails.
'failed_login' => [
'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),
'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),
],
];

View File

@@ -101,7 +101,7 @@ return [
'url' => env('SAML2_IDP_SLO', null),
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
// if not set, url for the SLO Request will be used
'responseUrl' => '',
'responseUrl' => null,
// SAML protocol binding to be used when returning the <Response>
// message. Onelogin Toolkit supports for this endpoint the
// HTTP-Redirect binding only

View File

@@ -1,5 +1,7 @@
<?php
use \Illuminate\Support\Str;
/**
* Session configuration options.
*
@@ -69,7 +71,8 @@ return [
// By setting this option to true, session cookies will only be sent back
// to the server if the browser has a HTTPS connection. This will keep
// the cookie from being sent to you if it can not be done securely.
'secure' => env('SESSION_SECURE_COOKIE', false),
'secure' => env('SESSION_SECURE_COOKIE', null)
?? Str::startsWith(env('APP_URL'), 'https:'),
// HTTP Access Only
// Setting this value to true will prevent JavaScript from accessing the
@@ -80,6 +83,6 @@ return [
// This option determines how your cookies behave when cross-site requests
// take place, and can be used to mitigate CSRF attacks. By default, we
// do not enable this as other CSRF protection services are in place.
// Options: lax, strict
'same_site' => null,
// Options: lax, strict, none
'same_site' => 'lax',
];

View File

@@ -14,8 +14,8 @@ class CleanupImages extends Command
* @var string
*/
protected $signature = 'bookstack:cleanup-images
{--a|all : Include images that are used in page revisions}
{--f|force : Actually run the deletions}
{--a|all : Also delete images that are only used in old revisions}
{--f|force : Actually run the deletions, Defaults to a dry-run}
';
/**

View File

@@ -2,7 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Entities\PageRevision;
use BookStack\Entities\Models\PageRevision;
use Illuminate\Console\Command;
class ClearRevisions extends Command

View File

@@ -2,7 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Console\Command;

View File

@@ -28,8 +28,6 @@ class CreateAdmin extends Command
/**
* Create a new command instance.
*
* @param UserRepo $userRepo
*/
public function __construct(UserRepo $userRepo)
{

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

@@ -2,7 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Entities\SearchService;
use BookStack\Entities\Tools\SearchIndex;
use DB;
use Illuminate\Console\Command;
@@ -22,17 +22,15 @@ class RegenerateSearch extends Command
*/
protected $description = 'Re-index all content for searching';
protected $searchService;
protected $searchIndex;
/**
* Create a new command instance.
*
* @param SearchService $searchService
*/
public function __construct(SearchService $searchService)
public function __construct(SearchIndex $searchIndex)
{
parent::__construct();
$this->searchService = $searchService;
$this->searchIndex = $searchIndex;
}
/**
@@ -45,10 +43,9 @@ class RegenerateSearch extends Command
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(DB::connection($this->option('database')));
}
$this->searchService->indexAllEntities();
$this->searchIndex->indexAllEntities();
DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Managers\EntityContext;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\ShelfContext;
use Illuminate\View\View;
class BreadcrumbsViewComposer
@@ -10,9 +11,9 @@ class BreadcrumbsViewComposer
/**
* BreadcrumbsViewComposer constructor.
* @param EntityContext $entityContextManager
* @param ShelfContext $entityContextManager
*/
public function __construct(EntityContext $entityContextManager)
public function __construct(ShelfContext $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}

View File

@@ -1,73 +0,0 @@
<?php namespace BookStack\Entities;
use Illuminate\Support\Collection;
/**
* Class Chapter
* @property Collection<Page> $pages
* @package BookStack\Entities
*/
class Chapter extends BookChild
{
public $searchFactor = 1.3;
protected $fillable = ['name', 'description', 'priority', 'book_id'];
/**
* Get the pages that this chapter contains.
* @param string $dir
* @return mixed
*/
public function pages($dir = 'ASC')
{
return $this->hasMany(Page::class)->orderBy('priority', $dir);
}
/**
* Get the url of this chapter.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
$fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
if ($path !== false) {
$fullPath .= '/' . trim($path, '/');
}
return url($fullPath);
}
/**
* Get an excerpt of this chapter's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt(int $length = 100)
{
$description = $this->text ?? $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**
* Check if this chapter has any child pages.
* @return bool
*/
public function hasChildren()
{
return count($this->pages) > 0;
}
/**
* Get the visible pages in this chapter.
*/
public function getVisiblePages(): Collection
{
return $this->pages()->visible()
->orderBy('draft', 'desc')
->orderBy('priority', 'asc')
->get();
}
}

View File

@@ -1,13 +1,18 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
/**
* Class EntityProvider
*
* Provides access to the core entity models.
* Wrapped up in this provider since they are often used together
* so this is a neater alternative to injecting all in individually.
*
* @package BookStack\Entities
*/
class EntityProvider
{
@@ -37,26 +42,20 @@ class EntityProvider
*/
public $pageRevision;
/**
* EntityProvider constructor.
*/
public function __construct(
Bookshelf $bookshelf,
Book $book,
Chapter $chapter,
Page $page,
PageRevision $pageRevision
) {
$this->bookshelf = $bookshelf;
$this->book = $book;
$this->chapter = $chapter;
$this->page = $page;
$this->pageRevision = $pageRevision;
public function __construct()
{
$this->bookshelf = new Bookshelf();
$this->book = new Book();
$this->chapter = new Chapter();
$this->page = new Page();
$this->pageRevision = new PageRevision();
}
/**
* Fetch all core entity types as an associated array
* with their basic names as the keys.
* @return array<Entity>
*/
public function all(): array
{

View File

@@ -1,109 +0,0 @@
<?php namespace BookStack\Entities\Managers;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\HasCoverImage;
use BookStack\Entities\Page;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\ImageService;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
class TrashCan
{
/**
* Remove a bookshelf from the system.
* @throws Exception
*/
public function destroyShelf(Bookshelf $shelf)
{
$this->destroyCommonRelations($shelf);
$shelf->delete();
}
/**
* Remove a book from the system.
* @throws NotifyException
* @throws BindingResolutionException
*/
public function destroyBook(Book $book)
{
foreach ($book->pages as $page) {
$this->destroyPage($page);
}
foreach ($book->chapters as $chapter) {
$this->destroyChapter($chapter);
}
$this->destroyCommonRelations($book);
$book->delete();
}
/**
* Remove a page from the system.
* @throws NotifyException
*/
public function destroyPage(Page $page)
{
// Check if set as custom homepage & remove setting if not used or throw error if active
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
setting()->remove('app-homepage');
}
$this->destroyCommonRelations($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
$attachmentService->deleteFile($attachment);
}
$page->delete();
}
/**
* Remove a chapter from the system.
* @throws Exception
*/
public function destroyChapter(Chapter $chapter)
{
if (count($chapter->pages) > 0) {
foreach ($chapter->pages as $page) {
$page->chapter_id = 0;
$page->save();
}
}
$this->destroyCommonRelations($chapter);
$chapter->delete();
}
/**
* Update entity relations to remove or update outstanding connections.
*/
protected function destroyCommonRelations(Entity $entity)
{
Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);
$imageService->destroy($entity->cover);
}
}
}

View File

@@ -1,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Exception;
@@ -12,26 +12,20 @@ use Illuminate\Support\Collection;
* @property string $description
* @property int $image_id
* @property Image|null $cover
* @package BookStack\Entities
*/
class Book extends Entity implements HasCoverImage
{
public $searchFactor = 2;
protected $fillable = ['name', 'description'];
protected $hidden = ['restricted', 'pivot'];
protected $hidden = ['restricted', 'pivot', 'image_id', 'deleted_at'];
/**
* Get the url for this book.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
public function getUrl(string $path = ''): string
{
if ($path !== false) {
return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return url('/books/' . urlencode($this->slug));
return url('/books/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
}
/**
@@ -117,15 +111,4 @@ class Book extends Entity implements HasCoverImage
$chapters = $this->chapters()->visible()->get();
return $pages->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
}

View File

@@ -1,5 +1,8 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Book;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -10,7 +13,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
* @property Book $book
* @method Builder whereSlugs(string $bookSlug, string $childSlug)
*/
class BookChild extends Entity
abstract class BookChild extends Entity
{
/**
@@ -28,11 +31,10 @@ class BookChild extends Entity
/**
* Get the book this page sits in.
* @return BelongsTo
*/
public function book(): BelongsTo
{
return $this->belongsTo(Book::class);
return $this->belongsTo(Book::class)->withTrashed();
}
/**
@@ -45,9 +47,6 @@ class BookChild extends Entity
$this->save();
$this->refresh();
// Update related activity
$this->activity()->update(['book_id' => $newBookId]);
// Update all child pages if a chapter
if ($this instanceof Chapter) {
foreach ($this->pages as $page) {

View File

@@ -1,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Uploads\Image;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -12,7 +12,7 @@ class Bookshelf extends Entity implements HasCoverImage
protected $fillable = ['name', 'description', 'image_id'];
protected $hidden = ['restricted'];
protected $hidden = ['restricted', 'image_id', 'deleted_at'];
/**
* Get the books in this shelf.
@@ -36,15 +36,10 @@ class Bookshelf extends Entity implements HasCoverImage
/**
* Get the url for this bookshelf.
* @param string|bool $path
* @return string
*/
public function getUrl($path = false)
public function getUrl(string $path = ''): string
{
if ($path !== false) {
return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
}
return url('/shelves/' . urlencode($this->slug));
return url('/shelves/' . implode('/', [urlencode($this->slug), trim($path, '/')]));
}
/**
@@ -85,17 +80,6 @@ class Bookshelf extends Entity implements HasCoverImage
return 'cover_shelf';
}
/**
* Get an excerpt of this book's description to the specified length or less.
* @param int $length
* @return string
*/
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**
* Check if this shelf contains the given book.
* @param Book $book

View File

@@ -0,0 +1,52 @@
<?php namespace BookStack\Entities\Models;
use Illuminate\Support\Collection;
/**
* Class Chapter
* @property Collection<Page> $pages
*/
class Chapter extends BookChild
{
public $searchFactor = 1.3;
protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $hidden = ['restricted', 'pivot', 'deleted_at'];
/**
* Get the pages that this chapter contains.
* @param string $dir
* @return mixed
*/
public function pages($dir = 'ASC')
{
return $this->hasMany(Page::class)->orderBy('priority', $dir);
}
/**
* Get the url of this chapter.
*/
public function getUrl($path = ''): string
{
$parts = [
'books',
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
'chapter',
urlencode($this->slug),
trim($path, '/'),
];
return url('/' . implode('/', $parts));
}
/**
* Get the visible pages in this chapter.
*/
public function getVisiblePages(): Collection
{
return $this->pages()->visible()
->orderBy('draft', 'desc')
->orderBy('priority', 'asc')
->get();
}
}

View File

@@ -0,0 +1,48 @@
<?php namespace BookStack\Entities\Models;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Interfaces\Loggable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Deletion extends Model implements Loggable
{
/**
* Get the related deletable record.
*/
public function deletable(): MorphTo
{
return $this->morphTo('deletable')->withTrashed();
}
/**
* The the user that performed the deletion.
*/
public function deleter(): BelongsTo
{
return $this->belongsTo(User::class, 'deleted_by');
}
/**
* Create a new deletion record for the provided entity.
*/
public static function createForEntity(Entity $entity): Deletion
{
$record = (new self())->forceFill([
'deleted_by' => user()->id,
'deletable_type' => $entity->getMorphClass(),
'deletable_id' => $entity->id,
]);
$record->save();
return $record;
}
public function logDescriptor(): string
{
$deletable = $this->deletable()->first();
return "Deletion ({$this->id}) for {$deletable->getType()} ({$deletable->id}) {$deletable->name}";
}
}

View File

@@ -1,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
@@ -6,12 +6,17 @@ use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions;
use BookStack\Ownable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* Class Entity
@@ -31,11 +36,12 @@ use Illuminate\Database\Eloquent\Relations\MorphMany;
* @method static Entity|Builder hasPermission(string $permission)
* @method static Builder withLastView()
* @method static Builder withViewCount()
*
* @package BookStack\Entities
*/
class Entity extends Ownable
abstract class Entity extends Model
{
use SoftDeletes;
use HasCreatorAndUpdater;
use HasOwner;
/**
* @var string - Name of property where the main text content is found
@@ -50,7 +56,7 @@ class Entity extends Ownable
/**
* Get the entities that are visible to the current user.
*/
public function scopeVisible(Builder $query)
public function scopeVisible(Builder $query): Builder
{
return $this->scopeHasPermission($query, 'view');
}
@@ -92,24 +98,18 @@ class Entity extends Ownable
/**
* Compares this entity to another given entity.
* Matches by comparing class and id.
* @param $entity
* @return bool
*/
public function matches($entity)
public function matches(Entity $entity): bool
{
return [get_class($this), $this->id] === [get_class($entity), $entity->id];
}
/**
* Checks if an entity matches or contains another given entity.
* @param Entity $entity
* @return bool
* Checks if the current entity matches or contains the given.
*/
public function matchesOrContains(Entity $entity)
public function matchesOrContains(Entity $entity): bool
{
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
if ($matches) {
if ($this->matches($entity)) {
return true;
}
@@ -126,9 +126,8 @@ class Entity extends Ownable
/**
* Gets the activity objects for this entity.
* @return MorphMany
*/
public function activity()
public function activity(): MorphMany
{
return $this->morphMany(Activity::class, 'entity')
->orderBy('created_at', 'desc');
@@ -137,26 +136,23 @@ class Entity extends Ownable
/**
* Get View objects for this entity.
*/
public function views()
public function views(): MorphMany
{
return $this->morphMany(View::class, 'viewable');
}
/**
* Get the Tag models that have been user assigned to this entity.
* @return MorphMany
*/
public function tags()
public function tags(): MorphMany
{
return $this->morphMany(Tag::class, 'entity')->orderBy('order', 'asc');
}
/**
* Get the comments for an entity
* @param bool $orderByCreated
* @return MorphMany
*/
public function comments($orderByCreated = true)
public function comments(bool $orderByCreated = true): MorphMany
{
$query = $this->morphMany(Comment::class, 'entity');
return $orderByCreated ? $query->orderBy('created_at', 'asc') : $query;
@@ -164,9 +160,8 @@ class Entity extends Ownable
/**
* Get the related search terms.
* @return MorphMany
*/
public function searchTerms()
public function searchTerms(): MorphMany
{
return $this->morphMany(SearchTerm::class, 'entity');
}
@@ -174,18 +169,15 @@ class Entity extends Ownable
/**
* Get this entities restrictions.
*/
public function permissions()
public function permissions(): MorphMany
{
return $this->morphMany(EntityPermission::class, 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
public function hasRestriction(int $role_id, string $action): bool
{
return $this->permissions()->where('role_id', '=', $role_id)
->where('action', '=', $action)->count() > 0;
@@ -193,55 +185,42 @@ class Entity extends Ownable
/**
* Get the entity jointPermissions this is connected to.
* @return MorphMany
*/
public function jointPermissions()
public function jointPermissions(): MorphMany
{
return $this->morphMany(JointPermission::class, 'entity');
}
/**
* Allows checking of the exact class, Used to check entity type.
* Cleaner method for is_a.
* @param $type
* @return bool
* Get the related delete records for this entity.
*/
public static function isA($type)
public function deletions(): MorphMany
{
return $this->morphMany(Deletion::class, 'deletable');
}
/**
* Check if this instance or class is a certain type of entity.
* Examples of $type are 'page', 'book', 'chapter'
*/
public static function isA(string $type): bool
{
return static::getType() === strtolower($type);
}
/**
* Get entity type.
* @return mixed
* Get the entity type as a simple lowercase word.
*/
public static function getType()
public static function getType(): string
{
return strtolower(static::getClassName());
}
/**
* Get an instance of an entity of the given type.
* @param $type
* @return Entity
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
}
return app('BookStack\\Entities\\' . $className);
$className = array_slice(explode('\\', static::class), -1, 1)[0];
return strtolower($className);
}
/**
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public function getShortName($length = 25)
public function getShortName(int $length = 25): string
{
if (mb_strlen($this->name) <= $length) {
return $this->name;
@@ -251,35 +230,45 @@ class Entity extends Ownable
/**
* Get the body text of this entity.
* @return mixed
*/
public function getText()
public function getText(): string
{
return $this->{$this->textField};
return $this->{$this->textField} ?? '';
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt(int $length = 100)
public function getExcerpt(int $length = 100): string
{
$text = $this->getText();
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
return trim($text);
}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path = '/')
abstract public function getUrl(string $path = '/'): string;
/**
* Get the parent entity if existing.
* This is the "static" parent and does not include dynamic
* relations such as shelves to books.
*/
public function getParent(): ?Entity
{
return $path;
if ($this->isA('page')) {
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
}
if ($this->isA('chapter')) {
return $this->book()->withTrashed()->first();
}
return null;
}
/**
@@ -288,7 +277,7 @@ class Entity extends Ownable
public function rebuildPermissions()
{
/** @noinspection PhpUnhandledExceptionInspection */
Permissions::buildJointPermissionsForEntity($this);
Permissions::buildJointPermissionsForEntity(clone $this);
}
/**
@@ -296,8 +285,7 @@ class Entity extends Ownable
*/
public function indexForSearch()
{
$searchService = app()->make(SearchService::class);
$searchService->indexEntity($this);
app(SearchIndex::class)->indexEntity(clone $this);
}
/**
@@ -305,8 +293,7 @@ class Entity extends Ownable
*/
public function refreshSlug(): string
{
$generator = new SlugGenerator($this);
$this->slug = $generator->generate();
$this->slug = (new SlugGenerator)->generate($this);
return $this->slug;
}
}

View File

@@ -1,7 +1,7 @@
<?php
namespace BookStack\Entities;
namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Relations\BelongsTo;

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\Attachment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
@@ -21,16 +22,23 @@ use Permissions;
*/
class Page extends BookChild
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $fillable = ['name', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
public $textField = 'text';
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
protected $casts = [
'draft' => 'boolean',
'template' => 'boolean',
];
/**
* Get the entities that are visible to the current user.
*/
public function scopeVisible(Builder $query)
public function scopeVisible(Builder $query): Builder
{
$query = Permissions::enforceDraftVisiblityOnQuery($query);
return parent::scopeVisible($query);
@@ -47,14 +55,6 @@ class Page extends BookChild
return $array;
}
/**
* Get the parent item
*/
public function parent(): Entity
{
return $this->chapter_id ? $this->chapter : $this->book;
}
/**
* Get the chapter that this page is in, If applicable.
* @return BelongsTo
@@ -92,22 +92,19 @@ class Page extends BookChild
}
/**
* Get the url for this page.
* @param string|bool $path
* @return string
* Get the url of this page.
*/
public function getUrl($path = false)
public function getUrl($path = ''): string
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
$parts = [
'books',
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
$this->draft ? 'draft' : 'page',
$this->draft ? $this->id : urlencode($this->slug),
trim($path, '/'),
];
$url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
if ($path !== false) {
$url .= '/' . trim($path, '/');
}
return url($url);
return url('/' . implode('/', $parts));
}
/**
@@ -118,4 +115,15 @@ class Page extends BookChild
{
return $this->revisions()->first();
}
/**
* Get this page for JSON display.
*/
public function forJsonDisplay(): Page
{
$refreshed = $this->refresh()->unsetRelations()->load(['tags', 'createdBy', 'updatedBy', 'ownedBy']);
$refreshed->setHidden(array_diff($refreshed->getHidden(), ['html', 'markdown']));
$refreshed->html = (new PageContent($refreshed))->render();
return $refreshed;
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use Carbon\Carbon;

View File

@@ -1,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Model;

View File

@@ -2,11 +2,13 @@
namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
use BookStack\Entities\Book;
use BookStack\Entities\Entity;
use BookStack\Entities\HasCoverImage;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Activity;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
@@ -18,10 +20,6 @@ class BaseRepo
protected $imageRepo;
/**
* BaseRepo constructor.
* @param $tagRepo
*/
public function __construct(TagRepo $tagRepo, ImageRepo $imageRepo)
{
$this->tagRepo = $tagRepo;
@@ -37,6 +35,7 @@ class BaseRepo
$entity->forceFill([
'created_by' => user()->id,
'updated_by' => user()->id,
'owned_by' => user()->id,
]);
$entity->refreshSlug();
$entity->save();
@@ -91,29 +90,4 @@ class BaseRepo
$entity->save();
}
}
/**
* Update the permissions of an entity.
*/
public function updatePermissions(Entity $entity, bool $restricted, Collection $permissions = null)
{
$entity->restricted = $restricted;
$entity->permissions()->delete();
if (!is_null($permissions)) {
$entityPermissionData = $permissions->flatMap(function ($restrictions, $roleId) {
return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
return [
'role_id' => $roleId,
'action' => strtolower($action),
] ;
});
});
$entity->permissions()->createMany($entityPermissionData);
}
$entity->save();
$entity->rebuildPermissions();
}
}

View File

@@ -1,14 +1,14 @@
<?php namespace BookStack\Entities\Repos;
use BookStack\Actions\ActivityType;
use BookStack\Actions\TagRepo;
use BookStack\Entities\Book;
use BookStack\Entities\Managers\TrashCan;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Collection;
@@ -22,7 +22,6 @@ class BookRepo
/**
* BookRepo constructor.
* @param $tagRepo
*/
public function __construct(BaseRepo $baseRepo, TagRepo $tagRepo, ImageRepo $imageRepo)
{
@@ -36,7 +35,7 @@ class BookRepo
*/
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
{
return Book::visible()->orderBy($sort, $order)->paginate($count);
return Book::visible()->with('cover')->orderBy($sort, $order)->paginate($count);
}
/**
@@ -91,6 +90,7 @@ class BookRepo
{
$book = new Book();
$this->baseRepo->create($book, $input);
Activity::addForEntity($book, ActivityType::BOOK_CREATE);
return $book;
}
@@ -100,6 +100,7 @@ class BookRepo
public function update(Book $book, array $input): Book
{
$this->baseRepo->update($book, $input);
Activity::addForEntity($book, ActivityType::BOOK_UPDATE);
return $book;
}
@@ -113,22 +114,16 @@ class BookRepo
$this->baseRepo->updateCoverImage($book, $coverImage, $removeImage);
}
/**
* Update the permissions of a book.
*/
public function updatePermissions(Book $book, bool $restricted, Collection $permissions = null)
{
$this->baseRepo->updatePermissions($book, $restricted, $permissions);
}
/**
* Remove a book from the system.
* @throws NotifyException
* @throws BindingResolutionException
* @throws Exception
*/
public function destroy(Book $book)
{
$trashCan = new TrashCan();
$trashCan->destroyBook($book);
$trashCan->softDestroyBook($book);
Activity::addForEntity($book, ActivityType::BOOK_DELETE);
$trashCan->autoClearOld();
}
}

View File

@@ -1,10 +1,12 @@
<?php namespace BookStack\Entities\Repos;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Managers\TrashCan;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Http\UploadedFile;
@@ -16,7 +18,6 @@ class BookshelfRepo
/**
* BookshelfRepo constructor.
* @param $baseRepo
*/
public function __construct(BaseRepo $baseRepo)
{
@@ -28,8 +29,10 @@ class BookshelfRepo
*/
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
{
return Bookshelf::visible()->with('visibleBooks')
->orderBy($sort, $order)->paginate($count);
return Bookshelf::visible()
->with(['visibleBooks', 'cover'])
->orderBy($sort, $order)
->paginate($count);
}
/**
@@ -85,11 +88,12 @@ class BookshelfRepo
$shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input);
$this->updateBooks($shelf, $bookIds);
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_CREATE);
return $shelf;
}
/**
* Create a new shelf in the system.
* Update an existing shelf in the system using the given input.
*/
public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf
{
@@ -99,6 +103,7 @@ class BookshelfRepo
$this->updateBooks($shelf, $bookIds);
}
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
return $shelf;
}
@@ -132,14 +137,6 @@ class BookshelfRepo
$this->baseRepo->updateCoverImage($shelf, $coverImage, $removeImage);
}
/**
* Update the permissions of a bookshelf.
*/
public function updatePermissions(Bookshelf $shelf, bool $restricted, Collection $permissions = null)
{
$this->baseRepo->updatePermissions($shelf, $restricted, $permissions);
}
/**
* Copy down the permissions of the given shelf to all child books.
*/
@@ -172,6 +169,8 @@ class BookshelfRepo
public function destroy(Bookshelf $shelf)
{
$trashCan = new TrashCan();
$trashCan->destroyShelf($shelf);
$trashCan->softDestroyShelf($shelf);
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_DELETE);
$trashCan->autoClearOld();
}
}

View File

@@ -1,15 +1,14 @@
<?php namespace BookStack\Entities\Repos;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Managers\BookContents;
use BookStack\Entities\Managers\TrashCan;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
class ChapterRepo
@@ -19,7 +18,6 @@ class ChapterRepo
/**
* ChapterRepo constructor.
* @param $baseRepo
*/
public function __construct(BaseRepo $baseRepo)
{
@@ -50,6 +48,7 @@ class ChapterRepo
$chapter->book_id = $parentBook->id;
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
Activity::addForEntity($chapter, ActivityType::CHAPTER_CREATE);
return $chapter;
}
@@ -59,17 +58,10 @@ class ChapterRepo
public function update(Chapter $chapter, array $input): Chapter
{
$this->baseRepo->update($chapter, $input);
Activity::addForEntity($chapter, ActivityType::CHAPTER_UPDATE);
return $chapter;
}
/**
* Update the permissions of a chapter.
*/
public function updatePermissions(Chapter $chapter, bool $restricted, Collection $permissions = null)
{
$this->baseRepo->updatePermissions($chapter, $restricted, $permissions);
}
/**
* Remove a chapter from the system.
* @throws Exception
@@ -77,7 +69,9 @@ class ChapterRepo
public function destroy(Chapter $chapter)
{
$trashCan = new TrashCan();
$trashCan->destroyChapter($chapter);
$trashCan->softDestroyChapter($chapter);
Activity::addForEntity($chapter, ActivityType::CHAPTER_DELETE);
$trashCan->autoClearOld();
}
/**
@@ -96,6 +90,7 @@ class ChapterRepo
throw new MoveOperationException('Chapters can only be moved into books');
}
/** @var Book $parent */
$parent = Book::visible()->where('id', '=', $entityId)->first();
if ($parent === null) {
throw new MoveOperationException('Book to move chapter into not found');
@@ -103,6 +98,8 @@ class ChapterRepo
$chapter->changeBook($parent->id);
$chapter->rebuildPermissions();
Activity::addForEntity($chapter, ActivityType::CHAPTER_MOVE);
return $parent;
}
}

View File

@@ -1,17 +1,19 @@
<?php namespace BookStack\Entities\Repos;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\Managers\BookContents;
use BookStack\Entities\Managers\PageContent;
use BookStack\Entities\Managers\TrashCan;
use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Collection;
@@ -33,9 +35,9 @@ class PageRepo
* Get a page by ID.
* @throws NotFoundException
*/
public function getById(int $id): Page
public function getById(int $id, array $relations = ['book']): Page
{
$page = Page::visible()->with(['book'])->find($id);
$page = Page::visible()->with($relations)->find($id);
if (!$page) {
throw new NotFoundException(trans('errors.page_not_found'));
@@ -128,6 +130,7 @@ class PageRepo
$page = (new Page())->forceFill([
'name' => trans('entities.pages_initial_name'),
'created_by' => user()->id,
'owned_by' => user()->id,
'updated_by' => user()->id,
'draft' => true,
]);
@@ -150,12 +153,8 @@ class PageRepo
public function publishDraft(Page $draft, array $input): Page
{
$this->baseRepo->update($draft, $input);
if (isset($input['template']) && userCan('templates-manage')) {
$draft->template = ($input['template'] === 'true');
}
$this->updateTemplateStatusAndContentFromInput($draft, $input);
$pageContent = new PageContent($draft);
$pageContent->setNewHTML($input['html']);
$draft->draft = false;
$draft->revision_count = 1;
$draft->priority = $this->getNewPriority($draft);
@@ -164,7 +163,10 @@ class PageRepo
$this->savePageRevision($draft, trans('entities.pages_initial_revision'));
$draft->indexForSearch();
return $draft->refresh();
$draft->refresh();
Activity::addForEntity($draft, ActivityType::PAGE_CREATE);
return $draft;
}
/**
@@ -175,23 +177,13 @@ class PageRepo
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
$oldMarkdown = $page->markdown;
if (isset($input['template']) && userCan('templates-manage')) {
$page->template = ($input['template'] === 'true');
}
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
// Update with new details
$page->fill($input);
$pageContent = new PageContent($page);
$pageContent->setNewHTML($input['html']);
$page->revision_count++;
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->save();
// Remove all update drafts for this user & page.
@@ -199,23 +191,37 @@ class PageRepo
// Save a revision after updating
$summary = $input['summary'] ?? null;
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
if ($htmlChanged || $nameChanged || $markdownChanged || $summary !== null) {
$this->savePageRevision($page, $summary);
}
Activity::addForEntity($page, ActivityType::PAGE_UPDATE);
return $page;
}
protected function updateTemplateStatusAndContentFromInput(Page $page, array $input)
{
if (isset($input['template']) && userCan('templates-manage')) {
$page->template = ($input['template'] === 'true');
}
$pageContent = new PageContent($page);
if (!empty($input['markdown'] ?? '')) {
$pageContent->setNewMarkdown($input['markdown']);
} else {
$pageContent->setNewHTML($input['html']);
}
}
/**
* Saves a page revision into the system.
*/
protected function savePageRevision(Page $page, string $summary = null)
protected function savePageRevision(Page $page, string $summary = null): PageRevision
{
$revision = new PageRevision($page->toArray());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision = new PageRevision($page->getAttributes());
$revision->page_id = $page->id;
$revision->slug = $page->slug;
@@ -238,11 +244,10 @@ class PageRepo
{
// If the page itself is a draft simply update that
if ($page->draft) {
$page->fill($input);
if (isset($input['html'])) {
$content = new PageContent($page);
$content->setNewHTML($input['html']);
(new PageContent($page))->setNewHTML($input['html']);
}
$page->fill($input);
$page->save();
return $page;
}
@@ -260,12 +265,14 @@ class PageRepo
/**
* Destroy a page from the system.
* @throws NotifyException
* @throws Exception
*/
public function destroy(Page $page)
{
$trashCan = new TrashCan();
$trashCan->destroyPage($page);
$trashCan->softDestroyPage($page);
Activity::addForEntity($page, ActivityType::PAGE_DELETE);
$trashCan->autoClearOld();
}
/**
@@ -274,17 +281,26 @@ class PageRepo
public function restoreRevision(Page $page, int $revisionId): Page
{
$page->revision_count++;
$this->savePageRevision($page);
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$content = new PageContent($page);
$content->setNewHTML($page->html);
if (!empty($revision->markdown)) {
$content->setNewMarkdown($revision->markdown);
} else {
$content->setNewHTML($revision->html);
}
$page->updated_by = user()->id;
$page->refreshSlug();
$page->save();
$page->indexForSearch();
$summary = trans('entities.pages_revision_restored_from', ['id' => strval($revisionId), 'summary' => $revision->summary]);
$this->savePageRevision($page, $summary);
Activity::addForEntity($page, ActivityType::PAGE_RESTORE);
return $page;
}
@@ -295,7 +311,7 @@ class PageRepo
* @throws MoveOperationException
* @throws PermissionsException
*/
public function move(Page $page, string $parentIdentifier): Book
public function move(Page $page, string $parentIdentifier): Entity
{
$parent = $this->findParentByIdentifier($parentIdentifier);
if ($parent === null) {
@@ -310,7 +326,8 @@ class PageRepo
$page->changeBook($parent instanceof Book ? $parent->id : $parent->book->id);
$page->rebuildPermissions();
return ($parent instanceof Book ? $parent : $parent->book);
Activity::addForEntity($page, ActivityType::PAGE_MOVE);
return $parent;
}
/**
@@ -321,7 +338,7 @@ class PageRepo
*/
public function copy(Page $page, string $parentIdentifier = null, string $newName = null): Page
{
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->parent();
$parent = $parentIdentifier ? $this->findParentByIdentifier($parentIdentifier) : $page->getParent();
if ($parent === null) {
throw new MoveOperationException('Book or chapter to move page into not found');
}
@@ -369,14 +386,6 @@ class PageRepo
return $parentClass::visible()->where('id', '=', $entityId)->first();
}
/**
* Update the permissions of a page.
*/
public function updatePermissions(Page $page, bool $restricted, Collection $permissions = null)
{
$this->baseRepo->updatePermissions($page, $restricted, $permissions);
}
/**
* Change the page's parent to the given entity.
*/
@@ -440,8 +449,9 @@ class PageRepo
*/
protected function getNewPriority(Page $page): int
{
if ($page->parent() instanceof Chapter) {
$lastPage = $page->parent()->pages('desc')->first();
$parent = $page->getParent();
if ($parent instanceof Chapter) {
$lastPage = $parent->pages('desc')->first();
return $lastPage ? $lastPage->priority + 1 : 0;
}

View File

@@ -1,62 +0,0 @@
<?php namespace BookStack\Entities;
class SlugGenerator
{
protected $entity;
/**
* SlugGenerator constructor.
* @param $entity
*/
public function __construct(Entity $entity)
{
$this->entity = $entity;
}
/**
* Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item.
*/
public function generate(): string
{
$slug = $this->formatNameAsSlug($this->entity->name);
while ($this->slugInUse($slug)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Format a name as a url slug.
*/
protected function formatNameAsSlug(string $name): string
{
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
return $slug;
}
/**
* Check if a slug is already in-use for this
* type of model within the same parent.
*/
protected function slugInUse(string $slug): bool
{
$query = $this->entity->newQuery()->where('slug', '=', $slug);
if ($this->entity instanceof BookChild) {
$query->where('book_id', '=', $this->entity->book_id);
}
if ($this->entity->id) {
$query->where('id', '!=', $this->entity->id);
}
return $query->count() > 0;
}
}

View File

@@ -1,10 +1,10 @@
<?php namespace BookStack\Entities\Managers;
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Book;
use BookStack\Entities\BookChild;
use BookStack\Entities\Chapter;
use BookStack\Entities\Entity;
use BookStack\Entities\Page;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\SortOperationException;
use Illuminate\Support\Collection;
@@ -18,7 +18,6 @@ class BookContents
/**
* BookContents constructor.
* @param $book
*/
public function __construct(Book $book)
{
@@ -41,7 +40,6 @@ class BookContents
/**
* Get the contents as a sorted collection tree.
* TODO - Support $renderPages option
*/
public function getTree(bool $showDrafts = false, bool $renderPages = false): Collection
{
@@ -54,14 +52,22 @@ class BookContents
$pages->groupBy('chapter_id')->each(function ($pages, $chapter_id) use ($chapterMap, &$lonePages) {
$chapter = $chapterMap->get($chapter_id);
if ($chapter) {
$chapter->setAttribute('pages', collect($pages)->sortBy($this->bookChildSortFunc()));
$chapter->setAttribute('visible_pages', collect($pages)->sortBy($this->bookChildSortFunc()));
} else {
$lonePages = $lonePages->concat($pages);
}
});
$all->each(function (Entity $entity) {
$chapters->whereNull('visible_pages')->each(function (Chapter $chapter) {
$chapter->setAttribute('visible_pages', collect([]));
});
$all->each(function (Entity $entity) use ($renderPages) {
$entity->setRelation('book', $this->book);
if ($renderPages && $entity->isA('page')) {
$entity->html = (new PageContent($entity))->render();
}
});
return collect($chapters)->concat($lonePages)->sortBy($this->bookChildSortFunc());

View File

@@ -1,14 +1,15 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Managers\BookContents;
use BookStack\Entities\Managers\PageContent;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\ImageService;
use DomPDF;
use Exception;
use SnappyPDF;
use Throwable;
class ExportService
class ExportFormatter
{
protected $imageService;
@@ -142,7 +143,7 @@ class ExportService
protected function containHtml(string $htmlContent): string
{
$imageTagsOutput = [];
preg_match_all("/\<img.*src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
preg_match_all("/\<img.*?src\=(\'|\")(.*?)(\'|\").*?\>/i", $htmlContent, $imageTagsOutput);
// Replace image src with base64 encoded image strings
if (isset($imageTagsOutput[0]) && count($imageTagsOutput[0]) > 0) {
@@ -203,7 +204,7 @@ class ExportService
{
$text = $chapter->name . "\n\n";
$text .= $chapter->description . "\n\n";
foreach ($chapter->pages as $page) {
foreach ($chapter->getVisiblePages() as $page) {
$text .= $this->pageToPlainText($page);
}
return $text;
@@ -214,7 +215,7 @@ class ExportService
*/
public function bookToPlainText(Book $book): string
{
$bookTree = (new BookContents($book))->getTree(false, true);
$bookTree = (new BookContents($book))->getTree(false, false);
$text = $book->name . "\n\n";
foreach ($bookTree as $bookChild) {
if ($bookChild->isA('chapter')) {

View File

@@ -0,0 +1,16 @@
<?php namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
class CustomStrikeThroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
}
}

View File

@@ -0,0 +1,24 @@
<?php namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\HtmlElement;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Inline\Renderer\InlineRendererInterface;
/**
* This is a somewhat clone of the League\CommonMark\Extension\Strikethrough\StrikethroughRender
* class but modified slightly to use <s> HTML tags instead of <del> in order to
* match front-end markdown-it rendering.
*/
class CustomStrikethroughRenderer implements InlineRendererInterface
{
public function render(AbstractInline $inline, ElementRendererInterface $htmlRenderer)
{
if (!($inline instanceof Strikethrough)) {
throw new \InvalidArgumentException('Incompatible inline type: ' . get_class($inline));
}
return new HtmlElement('s', $inline->getData('attributes', []), $htmlRenderer->renderInlines($inline->children()));
}
}

View File

@@ -1,10 +1,14 @@
<?php namespace BookStack\Entities\Managers;
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Page;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use DOMDocument;
use DOMElement;
use DOMNodeList;
use DOMXPath;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class PageContent
{
@@ -26,6 +30,31 @@ class PageContent
{
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
$this->page->markdown = '';
}
/**
* Update the content of the page with new provided Markdown content.
*/
public function setNewMarkdown(string $markdown)
{
$this->page->markdown = $markdown;
$html = $this->markdownToHtml($markdown);
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
}
/**
* Convert the given Markdown content to a HTML string.
*/
protected function markdownToHtml(string $markdown): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$converter = new CommonMarkConverter([], $environment);
return $converter->convertToHtml($markdown);
}
/**
@@ -44,18 +73,24 @@ class PageContent
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes
$idMap = [];
foreach ($childNodes as $index => $childNode) {
$this->setUniqueId($childNode, $idMap);
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap);
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Generate inner html as a string
@@ -67,23 +102,34 @@ class PageContent
return $html;
}
/**
* Update the all links to the $old location to instead point to $new.
*/
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
}
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element
* @param array $idMap
* Returns a pair of strings in the format [old_id, new_id]
*/
protected function setUniqueId($element, array &$idMap)
protected function setUniqueId(\DOMNode $element, array &$idMap): array
{
if (get_class($element) !== 'DOMElement') {
return;
return ['', ''];
}
// Overwrite id if not a BookStack custom id
// Stop if there's an existing valid id that has not already been used.
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return;
return [$existingId, $existingId];
}
// Create an unique id for the element
@@ -100,6 +146,7 @@ class PageContent
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
return [$existingId, $newId];
}
/**
@@ -108,7 +155,7 @@ class PageContent
protected function toPlainText(): string
{
$html = $this->render(true);
return strip_tags($html);
return html_entity_decode(strip_tags($html));
}
/**
@@ -279,6 +326,24 @@ class PageContent
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove clickable links to JavaScript URI
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
foreach ($badLinks as $badLink) {
$badLink->parentNode->removeChild($badLink);
}
// Remove forms with calls to JavaScript URI
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
foreach ($badForms as $badForm) {
$badForm->parentNode->removeChild($badForm);
}
// Remove meta tag to prevent external redirects
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
foreach ($metaTags as $metaTag) {
$metaTag->parentNode->removeChild($metaTag);
}
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
foreach ($badIframes as $badIframe) {

View File

@@ -1,7 +1,7 @@
<?php namespace BookStack\Entities\Managers;
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Models\PageRevision;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;

View File

@@ -0,0 +1,68 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PermissionsUpdater
{
/**
* Update an entities permissions from a permission form submit request.
*/
public function updateFromPermissionsForm(Entity $entity, Request $request)
{
$restricted = $request->get('restricted') === 'true';
$permissions = $request->get('restrictions', null);
$ownerId = $request->get('owned_by', null);
$entity->restricted = $restricted;
$entity->permissions()->delete();
if (!is_null($permissions)) {
$entityPermissionData = $this->formatPermissionsFromRequestToEntityPermissions($permissions);
$entity->permissions()->createMany($entityPermissionData);
}
if (!is_null($ownerId)) {
$this->updateOwnerFromId($entity, intval($ownerId));
}
$entity->save();
$entity->rebuildPermissions();
Activity::addForEntity($entity, ActivityType::PERMISSIONS_UPDATE);
}
/**
* Update the owner of the given entity.
* Checks the user exists in the system first.
* Does not save the model, just updates it.
*/
protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
{
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {
$entity->owned_by = $newOwner->id;
}
}
/**
* Format permissions provided from a permission form to be
* EntityPermission data.
*/
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): Collection
{
return collect($permissions)->flatMap(function ($restrictions, $roleId) {
return collect($restrictions)->keys()->map(function ($action) use ($roleId) {
return [
'role_id' => $roleId,
'action' => strtolower($action),
] ;
});
});
}
}

View File

@@ -0,0 +1,120 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\SearchTerm;
use Illuminate\Support\Collection;
class SearchIndex
{
/**
* @var SearchTerm
*/
protected $searchTerm;
/**
* @var EntityProvider
*/
protected $entityProvider;
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider)
{
$this->searchTerm = $searchTerm;
$this->entityProvider = $entityProvider;
}
/**
* Index the given entity.
*/
public function indexEntity(Entity $entity)
{
$this->deleteEntityTerms($entity);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
$terms = array_merge($nameTerms, $bodyTerms);
foreach ($terms as $index => $term) {
$terms[$index]['entity_type'] = $entity->getMorphClass();
$terms[$index]['entity_id'] = $entity->id;
}
$this->searchTerm->newQuery()->insert($terms);
}
/**
* Index multiple Entities at once
* @param Entity[] $entities
*/
protected function indexEntities(array $entities)
{
$terms = [];
foreach ($entities as $entity) {
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
$term['entity_id'] = $entity->id;
$term['entity_type'] = $entity->getMorphClass();
$terms[] = $term;
}
}
$chunkedTerms = array_chunk($terms, 500);
foreach ($chunkedTerms as $termChunk) {
$this->searchTerm->newQuery()->insert($termChunk);
}
}
/**
* Delete and re-index the terms for all entities in the system.
*/
public function indexAllEntities()
{
$this->searchTerm->newQuery()->truncate();
foreach ($this->entityProvider->all() as $entityModel) {
$selectFields = ['id', 'name', $entityModel->textField];
$entityModel->newQuery()
->withTrashed()
->select($selectFields)
->chunk(1000, function (Collection $entities) {
$this->indexEntities($entities->all());
});
}
}
/**
* Delete related Entity search terms.
*/
public function deleteEntityTerms(Entity $entity)
{
$entity->searchTerms()->delete();
}
/**
* Create a scored term array from the given text.
*/
protected function generateTermArrayFromText(string $text, int $scoreAdjustment = 1): array
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) {
$tokenMap[$token] = 0;
}
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
foreach ($tokenMap as $token => $count) {
$terms[] = [
'term' => $token,
'score' => $count * $scoreAdjustment
];
}
return $terms;
}
}

View File

@@ -0,0 +1,141 @@
<?php namespace BookStack\Entities\Tools;
use Illuminate\Http\Request;
class SearchOptions
{
/**
* @var array
*/
public $searches = [];
/**
* @var array
*/
public $exacts = [];
/**
* @var array
*/
public $tags = [];
/**
* @var array
*/
public $filters = [];
/**
* Create a new instance from a search string.
*/
public static function fromString(string $search): SearchOptions
{
$decoded = static::decode($search);
$instance = new static();
foreach ($decoded as $type => $value) {
$instance->$type = $value;
}
return $instance;
}
/**
* Create a new instance from a request.
* Will look for a classic string term and use that
* Otherwise we'll use the details from an advanced search form.
*/
public static function fromRequest(Request $request): SearchOptions
{
if (!$request->has('search') && !$request->has('term')) {
return static::fromString('');
}
if ($request->has('term')) {
return static::fromString($request->get('term'));
}
$instance = new static();
$inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
$instance->searches = explode(' ', $inputs['search'] ?? []);
$instance->exacts = array_filter($inputs['exact'] ?? []);
$instance->tags = array_filter($inputs['tags'] ?? []);
foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
if (empty($filterVal)) {
continue;
}
$instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
}
if (isset($inputs['types']) && count($inputs['types']) < 4) {
$instance->filters['type'] = implode('|', $inputs['types']);
}
return $instance;
}
/**
* Decode a search string into an array of terms.
*/
protected static function decode(string $searchString): array
{
$terms = [
'searches' => [],
'exacts' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exacts' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') {
$terms['searches'][] = $searchTerm;
}
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Encode this instance to a search string.
*/
public function toString(): string
{
$string = implode(' ', $this->searches ?? []);
foreach ($this->exacts as $term) {
$string .= ' "' . $term . '"';
}
foreach ($this->tags as $term) {
$string .= " [{$term}]";
}
foreach ($this->filters as $filterName => $filterVal) {
$string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
}
return $string;
}
}

View File

@@ -1,6 +1,8 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Tools;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder;
@@ -8,12 +10,8 @@ use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
class SearchService
class SearchRunner
{
/**
* @var SearchTerm
*/
protected $searchTerm;
/**
* @var EntityProvider
@@ -37,49 +35,28 @@ class SearchService
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
/**
* SearchService constructor.
* @param SearchTerm $searchTerm
* @param EntityProvider $entityProvider
* @param Connection $db
* @param PermissionService $permissionService
*/
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
public function __construct(EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
{
$this->searchTerm = $searchTerm;
$this->entityProvider = $entityProvider;
$this->db = $db;
$this->permissionService = $permissionService;
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Search all entities in the system.
* @param string $searchString
* @param string $entityType
* @param int $page
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @param string $action
* @return array[int, Collection];
* The provided count is for each entity to search,
* Total returned could can be larger and not guaranteed.
*/
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entityProvider->all());
$entityTypesToSearch = $entityTypes;
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
} else if (isset($terms['filters']['type'])) {
$entityTypesToSearch = explode('|', $terms['filters']['type']);
} else if (isset($searchOpts->filters['type'])) {
$entityTypesToSearch = explode('|', $searchOpts->filters['type']);
}
$results = collect();
@@ -90,8 +67,8 @@ class SearchService
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
$search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true);
if ($entityTotal > $page * $count) {
$hasMore = true;
}
@@ -103,60 +80,51 @@ class SearchService
'total' => $total,
'count' => count($results),
'has_more' => $hasMore,
'results' => $results->sortByDesc('score')->values()
'results' => $results->sortByDesc('score')->values(),
];
}
/**
* Search a book for entities
* @param integer $bookId
* @param string $searchString
* @return Collection
*/
public function searchBook($bookId, $searchString)
public function searchBook(int $bookId, string $searchString): Collection
{
$terms = $this->parseSearchString($searchString);
$opts = SearchOptions::fromString($searchString);
$entityTypes = ['page', 'chapter'];
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
$entityTypesToSearch = isset($opts->filters['type']) ? explode('|', $opts->filters['type']) : $entityTypes;
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
return $results->sortByDesc('score')->take(20);
}
/**
* Search a book for entities
* @param integer $chapterId
* @param string $searchString
* @return Collection
* Search a chapter for entities
*/
public function searchChapter($chapterId, $searchString)
public function searchChapter(int $chapterId, string $searchString): Collection
{
$terms = $this->parseSearchString($searchString);
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
$opts = SearchOptions::fromString($searchString);
$pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/**
* Search across a particular entity type.
* @param array $terms
* @param string $entityType
* @param int $page
* @param int $count
* @param string $action
* @param bool $getCount Return the total count of the search
* Setting getCount = true will return the total
* matching instead of the items themselves.
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
protected function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
if ($getCount) {
return $query->count();
}
@@ -167,50 +135,43 @@ class SearchService
/**
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @param string $action
* @return EloquentBuilder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder
{
$entity = $this->entityProvider->get($entityType);
$entitySelect = $entity->newQuery();
// Handle normal search terms
if (count($terms['search']) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
if (count($searchOpts->searches) > 0) {
$rawScoreSum = $this->db->raw('SUM(score) as score');
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', $rawScoreSum);
$subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) {
$subQuery->where(function (Builder $query) use ($searchOpts) {
foreach ($searchOpts->searches as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
$entitySelect->join(\DB::raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$entitySelect->join($this->db->raw('(' . $subQuery->toSql() . ') as s'), function (JoinClause $join) {
$join->on('id', '=', 'entity_id');
})->selectRaw($entity->getTable().'.*, s.score')->orderBy('score', 'desc');
$entitySelect->mergeBindings($subQuery);
}
// Handle exact term matching
if (count($terms['exact']) > 0) {
$entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) {
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
});
}
foreach ($searchOpts->exacts as $inputTerm) {
$entitySelect->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
});
}
// Handle tag searches
foreach ($terms['tags'] as $inputTerm) {
foreach ($searchOpts->tags as $inputTerm) {
$this->applyTagSearch($entitySelect, $inputTerm);
}
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
foreach ($searchOpts->filters as $filterTerm => $filterValue) {
$functionName = Str::camel('filter_' . $filterTerm);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
@@ -220,60 +181,10 @@ class SearchService
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
/**
* Parse a search string into components.
* @param $searchString
* @return array
*/
protected function parseSearchString($searchString)
{
$terms = [
'search' => [],
'exact' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exact' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') {
$terms['search'][] = $searchTerm;
}
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
protected function getRegexEscapedOperators(): string
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
@@ -284,11 +195,8 @@ class SearchService
/**
* Apply a tag search term onto a entity query.
* @param EloquentBuilder $query
* @param string $tagTerm
* @return mixed
*/
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder
{
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
@@ -316,103 +224,6 @@ class SearchService
return $query;
}
/**
* Index the given entity.
* @param Entity $entity
*/
public function indexEntity(Entity $entity)
{
$this->deleteEntityTerms($entity);
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
$terms = array_merge($nameTerms, $bodyTerms);
foreach ($terms as $index => $term) {
$terms[$index]['entity_type'] = $entity->getMorphClass();
$terms[$index]['entity_id'] = $entity->id;
}
$this->searchTerm->newQuery()->insert($terms);
}
/**
* Index multiple Entities at once
* @param \BookStack\Entities\Entity[] $entities
*/
protected function indexEntities($entities)
{
$terms = [];
foreach ($entities as $entity) {
$nameTerms = $this->generateTermArrayFromText($entity->name, 5 * $entity->searchFactor);
$bodyTerms = $this->generateTermArrayFromText($entity->getText(), 1 * $entity->searchFactor);
foreach (array_merge($nameTerms, $bodyTerms) as $term) {
$term['entity_id'] = $entity->id;
$term['entity_type'] = $entity->getMorphClass();
$terms[] = $term;
}
}
$chunkedTerms = array_chunk($terms, 500);
foreach ($chunkedTerms as $termChunk) {
$this->searchTerm->newQuery()->insert($termChunk);
}
}
/**
* Delete and re-index the terms for all entities in the system.
*/
public function indexAllEntities()
{
$this->searchTerm->truncate();
foreach ($this->entityProvider->all() as $entityModel) {
$selectFields = ['id', 'name', $entityModel->textField];
$entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
$this->indexEntities($entities);
});
}
}
/**
* Delete related Entity search terms.
* @param Entity $entity
*/
public function deleteEntityTerms(Entity $entity)
{
$entity->searchTerms()->delete();
}
/**
* Create a scored term array from the given text.
* @param $text
* @param float|int $scoreAdjustment
* @return array
*/
protected function generateTermArrayFromText($text, $scoreAdjustment = 1)
{
$tokenMap = []; // {TextToken => OccurrenceCount}
$splitChars = " \n\t.,!?:;()[]{}<>`'\"";
$token = strtok($text, $splitChars);
while ($token !== false) {
if (!isset($tokenMap[$token])) {
$tokenMap[$token] = 0;
}
$tokenMap[$token]++;
$token = strtok($splitChars);
}
$terms = [];
foreach ($tokenMap as $token => $count) {
$terms[] = [
'term' => $token,
'score' => $count * $scoreAdjustment
];
}
return $terms;
}
/**
* Custom entity search filters
*/

View File

@@ -1,29 +1,18 @@
<?php namespace BookStack\Entities\Managers;
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use Illuminate\Session\Store;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
class EntityContext
class ShelfContext
{
protected $session;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
*/
public function __construct(Store $session)
{
$this->session = $session;
}
/**
* Get the current bookshelf context for the given book.
*/
public function getContextualShelfForBook(Book $book): ?Bookshelf
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
$contextBookshelfId = session()->get($this->KEY_SHELF_CONTEXT_ID, null);
if (!is_int($contextBookshelfId)) {
return null;
@@ -37,11 +26,10 @@ class EntityContext
/**
* Store the current contextual shelf ID.
* @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
session()->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
@@ -49,6 +37,6 @@ class EntityContext
*/
public function clearShelfContext()
{
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
session()->forget($this->KEY_SHELF_CONTEXT_ID);
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use Illuminate\Support\Collection;
class SiblingFetcher
{
/**
* Search among the siblings of the entity of given type and id.
*/
public function fetch(string $entityType, int $entityId): Collection
{
$entity = (new EntityProvider)->get($entityType)->visible()->findOrFail($entityId);
$entities = [];
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
$entities = $entity->chapter->getVisiblePages();
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
$entities = $entity->book->getDirectChildren();
}
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity->isA('book')) {
$contextShelf = (new ShelfContext)->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $contextShelf->visibleBooks()->get();
} else {
$entities = Book::visible()->get();
}
}
// Shelve
if ($entity->isA('bookshelf')) {
$entities = Bookshelf::visible()->get();
}
return $entities;
}
}

View File

@@ -0,0 +1,52 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Entity;
use Illuminate\Support\Str;
class SlugGenerator
{
/**
* Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item.
*/
public function generate(Entity $entity): string
{
$slug = $this->formatNameAsSlug($entity->name);
while ($this->slugInUse($slug, $entity)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
}
return $slug;
}
/**
* Format a name as a url slug.
*/
protected function formatNameAsSlug(string $name): string
{
$slug = Str::slug($name);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}
return $slug;
}
/**
* Check if a slug is already in-use for this
* type of model within the same parent.
*/
protected function slugInUse(string $slug, Entity $entity): bool
{
$query = $entity->newQuery()->where('slug', '=', $slug);
if ($entity instanceof BookChild) {
$query->where('book_id', '=', $entity->book_id);
}
if ($entity->id) {
$query->where('id', '!=', $entity->id);
}
return $query->count() > 0;
}
}

View File

@@ -0,0 +1,326 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\HasCoverImage;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\ImageService;
use Exception;
use Illuminate\Support\Carbon;
class TrashCan
{
/**
* Send a shelf to the recycle bin.
*/
public function softDestroyShelf(Bookshelf $shelf)
{
Deletion::createForEntity($shelf);
$shelf->delete();
}
/**
* Send a book to the recycle bin.
* @throws Exception
*/
public function softDestroyBook(Book $book)
{
Deletion::createForEntity($book);
foreach ($book->pages as $page) {
$this->softDestroyPage($page, false);
}
foreach ($book->chapters as $chapter) {
$this->softDestroyChapter($chapter, false);
}
$book->delete();
}
/**
* Send a chapter to the recycle bin.
* @throws Exception
*/
public function softDestroyChapter(Chapter $chapter, bool $recordDelete = true)
{
if ($recordDelete) {
Deletion::createForEntity($chapter);
}
if (count($chapter->pages) > 0) {
foreach ($chapter->pages as $page) {
$this->softDestroyPage($page, false);
}
}
$chapter->delete();
}
/**
* Send a page to the recycle bin.
* @throws Exception
*/
public function softDestroyPage(Page $page, bool $recordDelete = true)
{
if ($recordDelete) {
Deletion::createForEntity($page);
}
// Check if set as custom homepage & remove setting if not used or throw error if active
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
setting()->remove('app-homepage');
}
$page->delete();
}
/**
* Remove a bookshelf from the system.
* @throws Exception
*/
protected function destroyShelf(Bookshelf $shelf): int
{
$this->destroyCommonRelations($shelf);
$shelf->forceDelete();
return 1;
}
/**
* Remove a book from the system.
* Destroys any child chapters and pages.
* @throws Exception
*/
protected function destroyBook(Book $book): int
{
$count = 0;
$pages = $book->pages()->withTrashed()->get();
foreach ($pages as $page) {
$this->destroyPage($page);
$count++;
}
$chapters = $book->chapters()->withTrashed()->get();
foreach ($chapters as $chapter) {
$this->destroyChapter($chapter);
$count++;
}
$this->destroyCommonRelations($book);
$book->forceDelete();
return $count + 1;
}
/**
* Remove a chapter from the system.
* Destroys all pages within.
* @throws Exception
*/
protected function destroyChapter(Chapter $chapter): int
{
$count = 0;
$pages = $chapter->pages()->withTrashed()->get();
if (count($pages)) {
foreach ($pages as $page) {
$this->destroyPage($page);
$count++;
}
}
$this->destroyCommonRelations($chapter);
$chapter->forceDelete();
return $count + 1;
}
/**
* Remove a page from the system.
* @throws Exception
*/
protected function destroyPage(Page $page): int
{
$this->destroyCommonRelations($page);
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
foreach ($page->attachments as $attachment) {
$attachmentService->deleteFile($attachment);
}
$page->forceDelete();
return 1;
}
/**
* Get the total counts of those that have been trashed
* but not yet fully deleted (In recycle bin).
*/
public function getTrashedCounts(): array
{
$counts = [];
/** @var Entity $instance */
foreach ((new EntityProvider)->all() as $key => $instance) {
$counts[$key] = $instance->newQuery()->onlyTrashed()->count();
}
return $counts;
}
/**
* Destroy all items that have pending deletions.
* @throws Exception
*/
public function empty(): int
{
$deletions = Deletion::all();
$deleteCount = 0;
foreach ($deletions as $deletion) {
$deleteCount += $this->destroyFromDeletion($deletion);
}
return $deleteCount;
}
/**
* Destroy an element from the given deletion model.
* @throws Exception
*/
public function destroyFromDeletion(Deletion $deletion): int
{
// We directly load the deletable element here just to ensure it still
// exists in the event it has already been destroyed during this request.
$entity = $deletion->deletable()->first();
$count = 0;
if ($entity) {
$count = $this->destroyEntity($deletion->deletable);
}
$deletion->delete();
return $count;
}
/**
* Restore the content within the given deletion.
* @throws Exception
*/
public function restoreFromDeletion(Deletion $deletion): int
{
$shouldRestore = true;
$restoreCount = 0;
$parent = $deletion->deletable->getParent();
if ($parent && $parent->trashed()) {
$shouldRestore = false;
}
if ($shouldRestore) {
$restoreCount = $this->restoreEntity($deletion->deletable);
}
$deletion->delete();
return $restoreCount;
}
/**
* Automatically clear old content from the recycle bin
* depending on the configured lifetime.
* Returns the total number of deleted elements.
* @throws Exception
*/
public function autoClearOld(): int
{
$lifetime = intval(config('app.recycle_bin_lifetime'));
if ($lifetime < 0) {
return 0;
}
$clearBeforeDate = Carbon::now()->addSeconds(10)->subDays($lifetime);
$deleteCount = 0;
$deletionsToRemove = Deletion::query()->where('created_at', '<', $clearBeforeDate)->get();
foreach ($deletionsToRemove as $deletion) {
$deleteCount += $this->destroyFromDeletion($deletion);
}
return $deleteCount;
}
/**
* Restore an entity so it is essentially un-deleted.
* Deletions on restored child elements will be removed during this restoration.
*/
protected function restoreEntity(Entity $entity): int
{
$count = 1;
$entity->restore();
$restoreAction = function ($entity) use (&$count) {
if ($entity->deletions_count > 0) {
$entity->deletions()->delete();
}
$entity->restore();
$count++;
};
if ($entity instanceof Chapter || $entity instanceof Book) {
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
if ($entity instanceof Book) {
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
return $count;
}
/**
* Destroy the given entity.
* @throws Exception
*/
protected function destroyEntity(Entity $entity): int
{
if ($entity instanceof Page) {
return $this->destroyPage($entity);
}
if ($entity instanceof Chapter) {
return $this->destroyChapter($entity);
}
if ($entity instanceof Book) {
return $this->destroyBook($entity);
}
if ($entity instanceof Bookshelf) {
return $this->destroyShelf($entity);
}
}
/**
* Update entity relations to remove or update outstanding connections.
*/
protected function destroyCommonRelations(Entity $entity)
{
Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
$entity->deletions()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);
$imageService->destroy($entity->cover);
}
}
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -26,6 +25,7 @@ class Handler extends ExceptionHandler
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
NotFoundException::class,
];
/**

View File

@@ -5,7 +5,7 @@ use BookStack\Http\Controllers\Controller;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\JsonResponse;
class ApiController extends Controller
abstract class ApiController extends Controller
{
protected $rules = [];

View File

@@ -1,8 +1,6 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Api\ApiDocsGenerator;
use Cache;
use Illuminate\Support\Collection;
class ApiDocsController extends ApiController
{
@@ -12,7 +10,8 @@ class ApiDocsController extends ApiController
*/
public function display()
{
$docs = $this->getDocs();
$docs = ApiDocsGenerator::generateConsideringCache();
$this->setPageTitle(trans('settings.users_api_tokens_docs'));
return view('api-docs.index', [
'docs' => $docs,
]);
@@ -21,27 +20,10 @@ class ApiDocsController extends ApiController
/**
* Show a JSON view of the API docs data.
*/
public function json() {
$docs = $this->getDocs();
public function json()
{
$docs = ApiDocsGenerator::generateConsideringCache();
return response()->json($docs);
}
/**
* Get the base docs data.
* Checks and uses the system cache for quick re-fetching.
*/
protected function getDocs(): Collection
{
$appVersion = trim(file_get_contents(base_path('version')));
$cacheKey = 'api-docs::' . $appVersion;
if (Cache::has($cacheKey) && config('app.env') === 'production') {
$docs = Cache::get($cacheKey);
} else {
$docs = (new ApiDocsGenerator())->generate();
Cache::put($cacheKey, $docs, 60*24);
}
return $docs;
}
}

View File

@@ -1,14 +1,13 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Book;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BooksApiController extends ApiController
class BookApiController extends ApiController
{
protected $bookRepo;
@@ -17,16 +16,15 @@ class BooksApiController extends ApiController
'create' => [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
'update' => [
'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
];
/**
* BooksApiController constructor.
*/
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
@@ -39,7 +37,7 @@ class BooksApiController extends ApiController
{
$books = Book::visible();
return $this->apiListingResponse($books, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
]);
}
@@ -53,8 +51,6 @@ class BooksApiController extends ApiController
$requestData = $this->validate($request, $this->rules['create']);
$book = $this->bookRepo->create($requestData);
Activity::add($book, 'book_create', $book->id);
return response()->json($book);
}
@@ -63,7 +59,7 @@ class BooksApiController extends ApiController
*/
public function read(string $id)
{
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy'])->findOrFail($id);
$book = Book::visible()->with(['tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy'])->findOrFail($id);
return response()->json($book);
}
@@ -78,15 +74,14 @@ class BooksApiController extends ApiController
$requestData = $this->validate($request, $this->rules['update']);
$book = $this->bookRepo->update($book, $requestData);
Activity::add($book, 'book_update', $book->id);
return response()->json($book);
}
/**
* Delete a single book from the system.
* @throws NotifyException
* @throws BindingResolutionException
* Delete a single book.
* This will typically send the book to the recycle bin.
* @throws \Exception
*/
public function delete(string $id)
{
@@ -94,8 +89,6 @@ class BooksApiController extends ApiController
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
Activity::addMessage('book_delete', $book->name);
return response('', 204);
}
}

View File

@@ -1,24 +1,16 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Book;
use BookStack\Entities\ExportService;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\ExportFormatter;
use Throwable;
class BooksExportApiController extends ApiController
class BookExportApiController extends ApiController
{
protected $exportFormatter;
protected $bookRepo;
protected $exportService;
/**
* BookExportController constructor.
*/
public function __construct(BookRepo $bookRepo, ExportService $exportService)
public function __construct(ExportFormatter $exportFormatter)
{
$this->bookRepo = $bookRepo;
$this->exportService = $exportService;
parent::__construct();
$this->exportFormatter = $exportFormatter;
}
/**
@@ -28,7 +20,7 @@ class BooksExportApiController extends ApiController
public function exportPdf(int $id)
{
$book = Book::visible()->findOrFail($id);
$pdfContent = $this->exportService->bookToPdf($book);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
}
@@ -39,7 +31,7 @@ class BooksExportApiController extends ApiController
public function exportHtml(int $id)
{
$book = Book::visible()->findOrFail($id);
$htmlContent = $this->exportService->bookToContainedHtml($book);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->downloadResponse($htmlContent, $book->slug . '.html');
}
@@ -49,7 +41,7 @@ class BooksExportApiController extends ApiController
public function exportPlainText(int $id)
{
$book = Book::visible()->findOrFail($id);
$textContent = $this->exportService->bookToPlainText($book);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->downloadResponse($textContent, $book->slug . '.txt');
}
}

View File

@@ -1,8 +1,7 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Facades\Activity;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Models\Bookshelf;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
@@ -31,7 +30,6 @@ class BookshelfApiController extends ApiController
/**
* BookshelfApiController constructor.
* @param BookshelfRepo $bookshelfRepo
*/
public function __construct(BookshelfRepo $bookshelfRepo)
{
@@ -45,7 +43,7 @@ class BookshelfApiController extends ApiController
{
$shelves = Bookshelf::visible();
return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by', 'image_id',
]);
}
@@ -63,7 +61,6 @@ class BookshelfApiController extends ApiController
$bookIds = $request->get('books', []);
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
Activity::add($shelf, 'bookshelf_create', $shelf->id);
return response()->json($shelf);
}
@@ -73,7 +70,7 @@ class BookshelfApiController extends ApiController
public function read(string $id)
{
$shelf = Bookshelf::visible()->with([
'tags', 'cover', 'createdBy', 'updatedBy',
'tags', 'cover', 'createdBy', 'updatedBy', 'ownedBy',
'books' => function (BelongsToMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}
@@ -94,19 +91,17 @@ class BookshelfApiController extends ApiController
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules['update']);
$bookIds = $request->get('books', null);
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
Activity::add($shelf, 'bookshelf_update', $shelf->id);
return response()->json($shelf);
}
/**
* Delete a single shelf from the system.
* Delete a single shelf.
* This will typically send the shelf to the recycle bin.
* @throws Exception
*/
public function delete(string $id)
@@ -115,8 +110,6 @@ class BookshelfApiController extends ApiController
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);
Activity::addMessage('bookshelf_delete', $shelf->name);
return response('', 204);
}
}

View File

@@ -0,0 +1,100 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Facades\Activity;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected $chapterRepo;
protected $rules = [
'create' => [
'book_id' => 'required|integer',
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
'update' => [
'book_id' => 'integer',
'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
];
/**
* ChapterController constructor.
*/
public function __construct(ChapterRepo $chapterRepo)
{
$this->chapterRepo = $chapterRepo;
}
/**
* Get a listing of chapters visible to the user.
*/
public function list()
{
$chapters = Chapter::visible();
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new chapter in the system.
*/
public function create(Request $request)
{
$this->validate($request, $this->rules['create']);
$bookId = $request->get('book_id');
$book = Book::visible()->findOrFail($bookId);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
return response()->json($chapter->load(['tags']));
}
/**
* View the details of a single chapter.
*/
public function read(string $id)
{
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}])->findOrFail($id);
return response()->json($chapter);
}
/**
* Update the details of a single chapter.
*/
public function update(Request $request, string $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-update', $chapter);
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
return response()->json($updatedChapter->load(['tags']));
}
/**
* Delete a chapter.
* This will typically send the chapter to the recycle bin.
*/
public function delete(string $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
return response('', 204);
}
}

View File

@@ -0,0 +1,51 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
class ChapterExportApiController extends ApiController
{
protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
}
/**
* Export a chapter as a PDF file.
* @throws Throwable
*/
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
}
/**
* Export a chapter as a contained HTML file.
* @throws Throwable
*/
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
}
/**
* Export a chapter as a plain text file.
*/
public function exportPlainText(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$textContent = $this->exportFormatter->chapterToPlainText($chapter);
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected $pageRepo;
protected $rules = [
'create' => [
'book_id' => 'required_without:chapter_id|integer',
'chapter_id' => 'required_without:book_id|integer',
'name' => 'required|string|max:255',
'html' => 'required_without:markdown|string',
'markdown' => 'required_without:html|string',
'tags' => 'array',
],
'update' => [
'book_id' => 'required|integer',
'chapter_id' => 'required|integer',
'name' => 'string|min:1|max:255',
'html' => 'string',
'markdown' => 'string',
'tags' => 'array',
],
];
public function __construct(PageRepo $pageRepo)
{
$this->pageRepo = $pageRepo;
}
/**
* Get a listing of pages visible to the user.
*/
public function list()
{
$pages = Page::visible();
return $this->apiListingResponse($pages, [
'id', 'book_id', 'chapter_id', 'name', 'slug', 'priority',
'draft', 'template',
'created_at', 'updated_at',
'created_by', 'updated_by', 'owned_by',
]);
}
/**
* Create a new page in the system.
*
* The ID of a parent book or chapter is required to indicate
* where this page should be located.
*
* Any HTML content provided should be kept to a single-block depth of plain HTML
* elements to remain compatible with the BookStack front-end and editors.
*/
public function create(Request $request)
{
$this->validate($request, $this->rules['create']);
if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
} else {
$parent = Book::visible()->findOrFail($request->get('book_id'));
}
$this->checkOwnablePermission('page-create', $parent);
$draft = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
return response()->json($draft->forJsonDisplay());
}
/**
* View the details of a single page.
*
* Pages will always have HTML content. They may have markdown content
* if the markdown editor was used to last update the page.
*/
public function read(string $id)
{
$page = $this->pageRepo->getById($id, []);
return response()->json($page->forJsonDisplay());
}
/**
* Update the details of a single page.
*
* See the 'create' action for details on the provided HTML/Markdown.
* Providing a 'book_id' or 'chapter_id' property will essentially move
* the page into that parent element if you have permissions to do so.
*/
public function update(Request $request, string $id)
{
$page = $this->pageRepo->getById($id, []);
$this->checkOwnablePermission('page-update', $page);
$parent = null;
if ($request->has('chapter_id')) {
$parent = Chapter::visible()->findOrFail($request->get('chapter_id'));
} else if ($request->has('book_id')) {
$parent = Book::visible()->findOrFail($request->get('book_id'));
}
if ($parent && !$parent->matches($page->getParent())) {
$this->checkOwnablePermission('page-delete', $page);
try {
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
return $this->jsonError(trans('errors.selected_book_chapter_not_found'));
}
}
$updatedPage = $this->pageRepo->update($page, $request->all());
return response()->json($updatedPage->forJsonDisplay());
}
/**
* Delete a page.
* This will typically send the page to the recycle bin.
*/
public function delete(string $id)
{
$page = $this->pageRepo->getById($id, []);
$this->checkOwnablePermission('page-delete', $page);
$this->pageRepo->destroy($page);
return response('', 204);
}
}

View File

@@ -0,0 +1,47 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\ExportFormatter;
use Throwable;
class PageExportApiController extends ApiController
{
protected $exportFormatter;
public function __construct(ExportFormatter $exportFormatter)
{
$this->exportFormatter = $exportFormatter;
}
/**
* Export a page as a PDF file.
* @throws Throwable
*/
public function exportPdf(int $id)
{
$page = Page::visible()->findOrFail($id);
$pdfContent = $this->exportFormatter->pageToPdf($page);
return $this->downloadResponse($pdfContent, $page->slug . '.pdf');
}
/**
* Export a page as a contained HTML file.
* @throws Throwable
*/
public function exportHtml(int $id)
{
$page = Page::visible()->findOrFail($id);
$htmlContent = $this->exportFormatter->pageToContainedHtml($page);
return $this->downloadResponse($htmlContent, $page->slug . '.html');
}
/**
* Export a page as a plain text file.
*/
public function exportPlainText(int $id)
{
$page = Page::visible()->findOrFail($id);
$textContent = $this->exportFormatter->pageToPlainText($page);
return $this->downloadResponse($textContent, $page->slug . '.txt');
}
}

View File

@@ -8,6 +8,7 @@ use BookStack\Uploads\AttachmentService;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
@@ -24,7 +25,6 @@ class AttachmentController extends Controller
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo;
parent::__construct();
}
@@ -60,25 +60,17 @@ class AttachmentController extends Controller
/**
* Update an uploaded attachment.
* @throws ValidationException
* @throws NotFoundException
*/
public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$uploadedFile = $request->file('file');
@@ -92,57 +84,87 @@ class AttachmentController extends Controller
}
/**
* Update the details of an existing file.
* @throws ValidationException
* @throws NotFoundException
* Get the update form for an attachment.
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function update(Request $request, $attachmentId)
public function getUpdateForm(string $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Update the details of an existing file.
*/
public function update(Request $request, string $attachmentId)
{
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
'attachment_edit_url' => 'string|min:1|max:255|safe_url'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
'attachment' => $attachment,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return response()->json($attachment);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
$attachment = $this->attachmentService->updateFile($attachment, [
'name' => $request->get('attachment_edit_name'),
'link' => $request->get('attachment_edit_url'),
]);
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Attach a link to a page.
* @throws ValidationException
* @throws NotFoundException
*/
public function attachLink(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('attachment_link_uploaded_to');
try {
$this->validate($request, [
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
'attachment_link_name' => 'required|string|min:1|max:255',
'attachment_link_url' => 'required|string|min:1|max:255|safe_url'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
'pageId' => $pageId,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$attachmentName = $request->get('name');
$link = $request->get('link');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
return response()->json($attachment);
return view('attachments.manager-link-form', [
'pageId' => $pageId,
]);
}
/**
@@ -152,7 +174,9 @@ class AttachmentController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
return view('attachments.manager-list', [
'attachments' => $page->attachments->all(),
]);
}
/**
@@ -163,14 +187,13 @@ class AttachmentController extends Controller
public function sortForPage(Request $request, int $pageId)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
'order' => 'required|array',
]);
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
$attachmentOrder = $request->get('order');
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
return response()->json(['message' => trans('entities.attachments_order_updated')]);
}
@@ -179,7 +202,7 @@ class AttachmentController extends Controller
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function get(int $attachmentId)
public function get(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
try {
@@ -200,11 +223,9 @@ class AttachmentController extends Controller
/**
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
* @throws Exception
*/
public function delete(int $attachmentId)
public function delete(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);

View File

@@ -0,0 +1,56 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Actions\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AuditLogController extends Controller
{
public function index(Request $request)
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->get('order', 'desc'),
'event' => $request->get('event', ''),
'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
];
$query = Activity::query()
->with([
'entity' => function ($query) {
$query->withTrashed();
},
'user'
])
->orderBy($listDetails['sort'], $listDetails['order']);
if ($listDetails['event']) {
$query->where('type', '=', $listDetails['event']);
}
if ($listDetails['date_from']) {
$query->where('created_at', '>=', $listDetails['date_from']);
}
if ($listDetails['date_to']) {
$query->where('created_at', '<=', $listDetails['date_to']);
}
$activities = $query->paginate(100);
$activities->appends($listDetails);
$types = DB::table('activities')->select('type')->distinct()->pluck('type');
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
'listDetails' => $listDetails,
'activityTypes' => $types,
]);
}
}

View File

@@ -21,15 +21,11 @@ class ConfirmEmailController extends Controller
/**
* Create a new controller instance.
*
* @param EmailConfirmationService $emailConfirmationService
* @param UserRepo $userRepo
*/
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
{
$this->emailConfirmationService = $emailConfirmationService;
$this->userRepo = $userRepo;
parent::__construct();
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
@@ -31,7 +32,6 @@ class ForgotPasswordController extends Controller
{
$this->middleware('guest');
$this->middleware('guard:standard');
parent::__construct();
}
@@ -52,6 +52,10 @@ class ForgotPasswordController extends Controller
$request->only('email')
);
if ($response === Password::RESET_LINK_SENT) {
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET, $request->get('email'));
}
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
$this->showSuccessNotification($message);

View File

@@ -2,10 +2,11 @@
namespace BookStack\Http\Controllers\Auth;
use Activity;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
@@ -45,7 +46,6 @@ class LoginController extends Controller
$this->socialAuthService = $socialAuthService;
$this->redirectPath = url('/');
$this->redirectAfterLogout = url('/login');
parent::__construct();
}
public function username()
@@ -76,9 +76,13 @@ class LoginController extends Controller
]);
}
// Store the previous location for redirect after login
$previous = url()->previous('');
if (setting('app-public') && $previous && $previous !== url('/login')) {
redirect()->setIntendedUrl($previous);
if ($previous && $previous !== url('/login') && setting('app-public')) {
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
if ($isPreviousFromInstance) {
redirect()->setIntendedUrl($previous);
}
}
return view('auth.login', [
@@ -98,6 +102,7 @@ class LoginController extends Controller
public function login(Request $request)
{
$this->validateLogin($request);
$username = $request->get($this->username());
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
@@ -106,6 +111,7 @@ class LoginController extends Controller
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
Activity::logFailedLogin($username);
return $this->sendLockoutResponse($request);
}
@@ -114,6 +120,7 @@ class LoginController extends Controller
return $this->sendLoginResponse($request);
}
} catch (LoginAttemptException $exception) {
Activity::logFailedLogin($username);
return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
@@ -122,6 +129,7 @@ class LoginController extends Controller
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
Activity::logFailedLogin($username);
return $this->sendFailedLoginResponse($request);
}
@@ -142,6 +150,7 @@ class LoginController extends Controller
}
}
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
return redirect()->intended($this->redirectPath());
}

View File

@@ -51,7 +51,6 @@ class RegisterController extends Controller
$this->redirectTo = url('/');
$this->redirectPath = url('/');
parent::__construct();
}
/**

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
@@ -33,7 +34,6 @@ class ResetPasswordController extends Controller
{
$this->middleware('guest');
$this->middleware('guard:standard');
parent::__construct();
}
/**
@@ -47,6 +47,7 @@ class ResetPasswordController extends Controller
{
$message = trans('auth.reset_password_success');
$this->showSuccessNotification($message);
$this->logActivity(ActivityType::AUTH_PASSWORD_RESET_UPDATE, user());
return redirect($this->redirectPath())
->with('status', trans($response));
}

View File

@@ -15,7 +15,6 @@ class Saml2Controller extends Controller
*/
public function __construct(Saml2Service $samlService)
{
parent::__construct();
$this->samlService = $samlService;
$this->middleware('guard:saml2');
}

View File

@@ -27,8 +27,6 @@ class UserInviteController extends Controller
$this->inviteService = $inviteService;
$this->userRepo = $userRepo;
parent::__construct();
}
/**

View File

@@ -1,12 +1,13 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Entities\Managers\BookContents;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\Managers\EntityContext;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotifyException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -18,14 +19,10 @@ class BookController extends Controller
protected $bookRepo;
protected $entityContextManager;
/**
* BookController constructor.
*/
public function __construct(EntityContext $entityContextManager, BookRepo $bookRepo)
public function __construct(ShelfContext $entityContextManager, BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
@@ -97,11 +94,10 @@ class BookController extends Controller
$book = $this->bookRepo->create($request->all());
$this->bookRepo->updateCoverImage($book, $request->file('image', null));
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
$bookshelf->appendBook($book);
Activity::add($bookshelf, 'bookshelf_update');
Activity::addForEntity($bookshelf, ActivityType::BOOKSHELF_UPDATE);
}
return redirect($book->getUrl());
@@ -162,8 +158,6 @@ class BookController extends Controller
$resetCover = $request->has('image_reset');
$this->bookRepo->updateCoverImage($book, $request->file('image', null), $resetCover);
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -181,14 +175,12 @@ class BookController extends Controller
/**
* Remove the specified book from the system.
* @throws Throwable
* @throws NotifyException
*/
public function destroy(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', $book->name);
$this->bookRepo->destroy($book);
return redirect('/books');
@@ -211,14 +203,12 @@ class BookController extends Controller
* Set the restrictions for this book.
* @throws Throwable
*/
public function permissions(Request $request, string $bookSlug)
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$restricted = $request->get('restricted') === 'true';
$permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
$this->bookRepo->updatePermissions($book, $restricted, $permissions);
$permissionsUpdater->updateFromPermissionsForm($book, $request);
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
return redirect($book->getUrl());

View File

@@ -2,7 +2,7 @@
namespace BookStack\Http\Controllers;
use BookStack\Entities\ExportService;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
@@ -10,16 +10,15 @@ class BookExportController extends Controller
{
protected $bookRepo;
protected $exportService;
protected $exportFormatter;
/**
* BookExportController constructor.
*/
public function __construct(BookRepo $bookRepo, ExportService $exportService)
public function __construct(BookRepo $bookRepo, ExportFormatter $exportFormatter)
{
$this->bookRepo = $bookRepo;
$this->exportService = $exportService;
parent::__construct();
$this->exportFormatter = $exportFormatter;
}
/**
@@ -29,7 +28,7 @@ class BookExportController extends Controller
public function pdf(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$pdfContent = $this->exportService->bookToPdf($book);
$pdfContent = $this->exportFormatter->bookToPdf($book);
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
}
@@ -40,7 +39,7 @@ class BookExportController extends Controller
public function html(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$htmlContent = $this->exportService->bookToContainedHtml($book);
$htmlContent = $this->exportFormatter->bookToContainedHtml($book);
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
}
@@ -50,7 +49,7 @@ class BookExportController extends Controller
public function plainText(string $bookSlug)
{
$book = $this->bookRepo->getBySlug($bookSlug);
$textContent = $this->exportService->bookToPlainText($book);
$textContent = $this->exportFormatter->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
}

View File

@@ -2,8 +2,9 @@
namespace BookStack\Http\Controllers;
use BookStack\Entities\Book;
use BookStack\Entities\Managers\BookContents;
use BookStack\Actions\ActivityType;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Exceptions\SortOperationException;
use BookStack\Facades\Activity;
@@ -14,14 +15,9 @@ class BookSortController extends Controller
protected $bookRepo;
/**
* BookSortController constructor.
* @param $bookRepo
*/
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
parent::__construct();
}
/**
@@ -74,7 +70,7 @@ class BookSortController extends Controller
// Rebuild permissions and add activity for involved books.
$booksInvolved->each(function (Book $book) {
Activity::add($book, 'book_sort', $book->id);
Activity::addForEntity($book, ActivityType::BOOK_SORT);
});
return redirect($book->getUrl());

View File

@@ -1,8 +1,9 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Entities\Book;
use BookStack\Entities\Managers\EntityContext;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
@@ -19,15 +20,11 @@ class BookshelfController extends Controller
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
*/
public function __construct(BookshelfRepo $bookshelfRepo, EntityContext $entityContextManager, ImageRepo $imageRepo)
public function __construct(BookshelfRepo $bookshelfRepo, ShelfContext $entityContextManager, ImageRepo $imageRepo)
{
$this->bookshelfRepo = $bookshelfRepo;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
@@ -92,7 +89,6 @@ class BookshelfController extends Controller
$shelf = $this->bookshelfRepo->create($request->all(), $bookIds);
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null));
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
@@ -156,7 +152,6 @@ class BookshelfController extends Controller
$shelf = $this->bookshelfRepo->update($shelf, $request->all(), $bookIds);
$resetCover = $request->has('image_reset');
$this->bookshelfRepo->updateCoverImage($shelf, $request->file('image', null), $resetCover);
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
}
@@ -182,7 +177,6 @@ class BookshelfController extends Controller
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
Activity::addMessage('bookshelf_delete', $shelf->name);
$this->bookshelfRepo->destroy($shelf);
return redirect('/shelves');
@@ -204,14 +198,12 @@ class BookshelfController extends Controller
/**
* Set the permissions for this bookshelf.
*/
public function permissions(Request $request, string $slug)
public function permissions(Request $request, PermissionsUpdater $permissionsUpdater, string $slug)
{
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$restricted = $request->get('restricted') === 'true';
$permissions = $request->filled('restrictions') ? collect($request->get('restrictions')) : null;
$this->bookshelfRepo->updatePermissions($shelf, $restricted, $permissions);
$permissionsUpdater->updateFromPermissionsForm($shelf, $request);
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
return redirect($shelf->getUrl());

Some files were not shown because too many files have changed in this diff Show More