Compare commits

...

315 Commits

Author SHA1 Message Date
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
c7322a71f7 Added theme add social driver redirect configuration callback
Allows someone using the theme system to configure the social driver
before a redirect action occurs, by passing a callback as an additional
param to the theme 'addSocialDriver' method.
2021-05-24 12:55:45 +01:00
Dan Brown
2c3523f6a1 Updated image permission setting logic
To ensure thhat the visibility is still set on local storage options
since the previous recent changes could cause problems where in
scenarios where the server user could not read images uploaded by the
php process user.

Closes #2758
2021-05-24 12:09:28 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
7a059a5e90 Updated translator attribution before release v21.04.5 2021-05-15 17:54:57 +01:00
Dan Brown
e5fc104aff New Crowdin updates (#2737)
* New translations errors.php (Italian)

* New translations errors.php (Slovak)

* New translations errors.php (Norwegian Bokmal)

* New translations errors.php (Bosnian)

* New translations errors.php (Latvian)

* New translations errors.php (Spanish, Argentina)

* New translations errors.php (Persian)

* New translations errors.php (Indonesian)

* New translations errors.php (Portuguese, Brazilian)

* New translations errors.php (Vietnamese)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (Ukrainian)

* New translations errors.php (Turkish)

* New translations errors.php (Swedish)

* New translations errors.php (Slovenian)

* New translations errors.php (Russian)

* New translations errors.php (French)

* New translations errors.php (Portuguese)

* New translations errors.php (Polish)

* New translations errors.php (Dutch)

* New translations errors.php (Korean)

* New translations errors.php (Japanese)

* New translations errors.php (Hungarian)

* New translations errors.php (Hebrew)

* New translations errors.php (German)

* New translations errors.php (Danish)

* New translations errors.php (Czech)

* New translations errors.php (Catalan)

* New translations errors.php (Bulgarian)

* New translations errors.php (Arabic)

* New translations errors.php (Spanish)

* New translations errors.php (German Informal)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (French)

* New translations common.php (French)

* New translations errors.php (Spanish, Argentina)

* New translations common.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations activities.php (Arabic)

* New translations auth.php (Arabic)

* New translations entities.php (Arabic)

* New translations auth.php (Arabic)

* New translations components.php (Arabic)

* New translations entities.php (Arabic)

* New translations errors.php (Russian)

* New translations common.php (Portuguese)

* New translations errors.php (Portuguese)
2021-05-15 17:50:02 +01:00
Dan Brown
d0ed165630 Merge pull request #2735 from dopyrory3/table_column_fix
Fix table width styling on pages rendered in markdown
2021-05-15 17:48:27 +01:00
Dan Brown
68ef6a842f Fixed issue thrown upon empty markdown content save
Closes #2741
2021-05-15 17:33:53 +01:00
Dan Brown
c1f070a136 Handle acl set of images differently for s3 and s3-like
Related to #2739
2021-05-15 17:25:51 +01:00
Dan Brown
c2cc1ec5e5 Adjusted dompdf font path to writable folder
Related to #2746
2021-05-15 12:19:36 +01:00
Rory Maher
386925ad8e Apply column fix to all tables 2021-05-10 12:11:28 +01:00
Rory Maher
243c1db408 Revert "Fix table width style"
This reverts commit b010d2663d.
2021-05-10 12:10:02 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
9e7bcacf8c Moved NotifyException render work from handler to exception
As continued from last commit.
2021-05-08 19:00:09 +01:00
Dan Brown
7be7d7d1e7 Updated not-found image path handling to have better ux
Added test to cover.
Started refactoring some of the app error handling in
the process of this.

Fixes #2696
2021-05-08 18:49:58 +01:00
Dan Brown
04c1d0e071 Updated translators before v21.04.4 release 2021-05-08 17:56:35 +01:00
Dan Brown
ab62e0f75b Merge pull request #2716 from Jokuna/master
Update Korean translation
2021-05-08 17:53:02 +01:00
Dan Brown
d85f99c87c New Crowdin updates (#2719)
* New translations entities.php (Dutch)

* New translations components.php (Italian)

* New translations entities.php (Italian)

* New translations entities.php (Italian)

* New translations errors.php (Italian)

* New translations passwords.php (Italian)

* New translations settings.php (Italian)

* New translations validation.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Indonesian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Portuguese)

* New translations common.php (Arabic)

* New translations common.php (Arabic)

* New translations entities.php (Arabic)

* New translations entities.php (Arabic)

* New translations settings.php (Italian)
2021-05-08 17:52:32 +01:00
Dan Brown
c42b6aece9 Updated composer deps again and run npm audit fix 2021-05-08 17:50:28 +01:00
Dan Brown
7f8f3080c5 Removed php8-only 'mixed' type from test method 2021-05-08 13:23:28 +01:00
Dan Brown
9cf4191079 Reviewed and updated SAML2 authncontext option
Added tests to cover.
Changed default to align with existing default.
Added env option parsing.
For #1998
2021-05-08 13:07:25 +01:00
Dan Brown
b8e2d75014 Merge branch 'ivir-authncontext' of https://github.com/ivir/BookStack into ivir-ivir-authncontext 2021-05-08 12:13:27 +01:00
Dan Brown
f522f16526 Fixed SAML login button alignment 2021-05-08 11:49:18 +01:00
Rory Maher
b010d2663d Fix table width style
Tables generated by the markdown renderer don't honour the max-width property without applying word-break styling to the td elements
2021-05-06 13:23:38 +01:00
Dan Brown
a083ceaf44 Fixed item export with deleted creator/updated
Added test to cover.
Fixes #2733
2021-05-05 22:52:08 +01:00
Dan Brown
95798a2eba Standardised export views with base layout, Reduced included export styles
Related to #2666
2021-05-04 23:15:05 +01:00
Dan Brown
43b6633183 Filtered scripts in custom HTML head for exports
Since it appeared to cause problems in some scenarios.
Related to #2490
2021-05-03 23:59:52 +01:00
Dan Brown
c50ac022a8 Updated composer deps 2021-05-03 22:32:19 +01:00
Dan Brown
a3d36237e2 Fixed white borders on layout tabs on ios
Closes #2728
2021-05-03 22:28:25 +01:00
Jokuna
a2be61f26d [Fix] app_footer_links_desc 2021-04-29 15:06:58 +09:00
Jokuna
79f5b579d7 [Fix] maint_delete_images_only_in_revisions better 2021-04-29 14:49:46 +09:00
Jokuna
66ecee1e26 [Fix] maint_delete_images_only_in_revisions 2021-04-29 13:54:24 +09:00
Jokuna
02e86ea18f [Fix] app_footer_links_desc 2021-04-29 13:44:50 +09:00
Jokuna
723dbe1da7 [Fix] korean 2021-04-29 13:43:10 +09:00
Jokuna
65fe89441f fix pages_revisions_resotred_from 2021-04-29 13:39:11 +09:00
Jokuna
2093122ac5 Korean translation
resources/lang/ko/settings.php
2021-04-29 12:58:53 +09:00
Jokuna
ab584c93bc Korean translations
activities.php
common.php
entities.php
validation.php
2021-04-29 00:11:01 +09:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
fc504a3d2c Updated translator attribution before release v21.04.3 2021-04-27 22:00:51 +01:00
Dan Brown
dd805503fb New Crowdin updates (#2695)
* New translations settings.php (Japanese)

* New translations settings.php (Japanese)

* New translations common.php (Latvian)

* New translations common.php (Russian)

* New translations settings.php (Dutch)

* New translations common.php (Dutch)

* New translations settings.php (Dutch)

* New translations entities.php (Dutch)

* New translations validation.php (Dutch)

* New translations activities.php (Dutch)

* New translations common.php (German)

* New translations common.php (Dutch)

* New translations common.php (German Informal)

* New translations activities.php (Dutch)

* New translations entities.php (German)

* New translations settings.php (German)

* New translations auth.php (Dutch)

* New translations components.php (Dutch)

* New translations common.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (German Informal)

* New translations common.php (Catalan)

* New translations common.php (Catalan)

* New translations passwords.php (Catalan)

* New translations validation.php (Catalan)

* New translations validation.php (Catalan)

* New translations auth.php (Catalan)

* New translations common.php (Italian)

* New translations activities.php (Italian)

* New translations common.php (Italian)
2021-04-27 21:58:09 +01:00
Dan Brown
f24336f77a Updated mobile content tabs to respect dark mode 2021-04-27 21:55:33 +01:00
Dan Brown
aa6a752e38 Implemented custom select controls because apple hates web developers
They'd rather keep pushing their 2007 era strange form control styles
even though they're horribly outdated, ugly and hard to style. The
only way to override is a full nuking of the default styles, which means
we have to then implement the frigging arrow icon using hacks which would
then conflict with all other sensible browsers so we have to nuke their
styles aswell to ensure some stupid backgroud hack is used everywhere.

I bet apple don't even use their shite default control styles and nuke
them also, Lets see. Yup, First thing I see on the top of their homepage
is a locale select dropdown custom built from about 10 HTML elements. FML

For #2709
2021-04-27 21:36:08 +01:00
Dan Brown
83b576eb19 Prevented "Recently Viewed" homepage list showing non-user-viewed items
Triggered when the user has no/limited views. Added a test to cover.
Closes #2703
2021-04-27 21:05:01 +01:00
Dan Brown
c4e31a0d5e Updated hard-coded string lengths for indexed columns
Since this is what's causing issues for people during migration due to max
key lengths.
Related to #2710.
2021-04-27 20:53:22 +01:00
Dan Brown
f8cdd6e80d Reduced calls for s3-based uploads
Combined the public ACL update into the put operation.
2021-04-27 20:36:42 +01:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
140aed3586 Updated translator attribution before release v21.04.2 2021-04-20 22:36:21 +01:00
Dan Brown
cf87b78636 New Crowdin updates (#2691)
* New translations common.php (Spanish)

* New translations common.php (Danish)

* New translations auth.php (Danish)

* New translations components.php (Danish)

* New translations entities.php (Danish)

* New translations entities.php (Danish)

* New translations settings.php (Danish)

* New translations common.php (Chinese Simplified)

* New translations auth.php (Chinese Simplified)

* New translations settings.php (Danish)

* New translations settings.php (Danish)

* New translations activities.php (Danish)

* New translations validation.php (Danish)

* New translations common.php (Danish)

* New translations auth.php (Danish)

* New translations activities.php (Danish)
2021-04-20 22:14:37 +01:00
Dan Brown
ec827da5a5 Updated public view test case to be more reliable
Was failing due to either common name or view share being
sticky across requests.
2021-04-20 22:14:01 +01:00
Dan Brown
20528a2442 Fixed error thrown when owner existed but the creator did not
Added test to cover.
For #2687
2021-04-20 21:04:38 +01:00
Dan Brown
78886b1e67 Updated version and assets for release v21.04.1 2021-04-19 22:26:19 +01:00
Dan Brown
d9debaf032 Merge branch 'master' into release 2021-04-19 22:25:29 +01:00
Dan Brown
b3c47649b4 New Crowdin updates (#2672)
* New translations common.php (French)

* New translations entities.php (French)

* New translations settings.php (Russian)

* New translations settings.php (Ukrainian)

* New translations common.php (Spanish, Argentina)

* New translations common.php (Spanish)

* New translations entities.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations common.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations common.php (Chinese Simplified)

* New translations common.php (Polish)

* New translations common.php (Norwegian Bokmal)

* New translations common.php (Bosnian)

* New translations common.php (Latvian)

* New translations common.php (Persian)

* New translations common.php (Indonesian)

* New translations common.php (Vietnamese)

* New translations common.php (Chinese Traditional)

* New translations common.php (Ukrainian)

* New translations common.php (Turkish)

* New translations common.php (Swedish)

* New translations common.php (Slovenian)

* New translations common.php (Slovak)

* New translations common.php (Russian)

* New translations common.php (Portuguese)

* New translations common.php (Dutch)

* New translations common.php (French)

* New translations common.php (Korean)

* New translations common.php (Japanese)

* New translations common.php (Italian)

* New translations common.php (Hungarian)

* New translations common.php (Hebrew)

* New translations common.php (German)

* New translations common.php (Danish)

* New translations common.php (Czech)

* New translations common.php (Catalan)

* New translations common.php (Bulgarian)

* New translations common.php (Arabic)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Spanish)

* New translations common.php (Spanish, Argentina)

* New translations common.php (German Informal)
2021-04-19 22:04:05 +01:00
Dan Brown
70be28d22c Updated tinymce code block handling to help prevent breaking history states
Only used an undo transaction on startup and added a small delay
to codeMirror parsing on SetContent's to help avoid
the rendering activities getting caught in undoManager states.
Seemed to improve things a lot in Firefox & chrome on my dev machine.

For #2602
2021-04-19 22:00:33 +01:00
Dan Brown
9df4dee1b2 Improved header element accessibility when at mobile sizes
Intended to fix issues raised in #2681.
Changes up the tri-layout tabs, and the main header menu toggle,
to be buttons while adding better text and keyboard controls.

Updated the component format of a few elements along the way.
2021-04-19 21:41:13 +01:00
Dan Brown
60ffe6a993 Updated packages and added better upload failure logging
To fix #2689
Updates all packages but mainly focused on aws-sdk
2021-04-19 20:16:49 +01:00
Dan Brown
0c880def5e Fixed response JSON detection when charset existed
Fixes #2684
2021-04-18 22:12:26 +01:00
Dan Brown
d4360d6347 Updated version and assets for release v21.04 2021-04-09 21:18:32 +01:00
Dan Brown
175b1785c0 Merge branch 'master' into release 2021-04-09 21:18:09 +01:00
Dan Brown
e4660a5ba2 Aligned facade accessor 2021-04-09 21:03:02 +01:00
Dan Brown
e2fa6d83c6 Removed some unused sass variables 2021-04-08 22:33:36 +01:00
Dan Brown
a0d32e7b88 Updated translator contribution list 2021-04-07 21:56:30 +01:00
Dan Brown
ced15b64a8 New Crowdin updates (#2621)
* New translations common.php (Russian)

* New translations settings.php (Russian)

* New translations activities.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations settings.php (French)

* New translations activities.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations activities.php (Polish)

* New translations common.php (Polish)

* New translations entities.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations validation.php (Polish)

* New translations settings.php (Latvian)

* New translations settings.php (Latvian)

* New translations settings.php (Latvian)

* New translations passwords.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations validation.php (Latvian)

* New translations validation.php (Polish)

* New translations validation.php (German Informal)

* New translations validation.php (Norwegian Bokmal)

* New translations validation.php (Spanish, Argentina)

* New translations validation.php (Persian)

* New translations validation.php (Portuguese, Brazilian)

* New translations validation.php (Vietnamese)

* New translations validation.php (Chinese Traditional)

* New translations validation.php (Chinese Simplified)

* New translations validation.php (Ukrainian)

* New translations validation.php (Turkish)

* New translations validation.php (Swedish)

* New translations validation.php (Slovenian)

* New translations validation.php (Slovak)

* New translations validation.php (Russian)

* New translations validation.php (Dutch)

* New translations validation.php (Portuguese)

* New translations validation.php (Korean)

* New translations validation.php (Japanese)

* New translations validation.php (Italian)

* New translations validation.php (Hungarian)

* New translations validation.php (Hebrew)

* New translations validation.php (German)

* New translations validation.php (Danish)

* New translations validation.php (Czech)

* New translations validation.php (Bulgarian)

* New translations validation.php (Arabic)

* New translations validation.php (Spanish)

* New translations validation.php (French)

* New translations validation.php (Bosnian)

* New translations validation.php (Indonesian)

* New translations validation.php (Catalan)

* New translations entities.php (Latvian)

* New translations entities.php (Polish)

* New translations entities.php (German Informal)

* New translations entities.php (Norwegian Bokmal)

* 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 (Russian)

* New translations entities.php (Dutch)

* New translations entities.php (Portuguese)

* 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 (French)

* New translations entities.php (Bosnian)

* New translations entities.php (Indonesian)

* New translations entities.php (Catalan)

* New translations entities.php (Spanish)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations entities.php (Portuguese)

* New translations entities.php (Latvian)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations common.php (German)

* New translations common.php (Japanese)

* New translations common.php (Chinese Simplified)

* New translations common.php (Ukrainian)

* New translations common.php (Turkish)

* New translations common.php (Swedish)

* New translations common.php (Slovenian)

* New translations common.php (Slovak)

* New translations common.php (Russian)

* New translations common.php (Portuguese)

* New translations common.php (Dutch)

* New translations common.php (Korean)

* New translations common.php (Polish)

* New translations common.php (Italian)

* New translations common.php (Arabic)

* New translations common.php (Hungarian)

* New translations common.php (French)

* New translations common.php (Spanish)

* New translations common.php (Catalan)

* New translations common.php (Bulgarian)

* New translations common.php (Czech)

* New translations common.php (Danish)

* New translations common.php (Hebrew)

* New translations common.php (Bosnian)

* New translations common.php (Chinese Traditional)

* New translations common.php (Vietnamese)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Persian)

* New translations common.php (Spanish, Argentina)

* New translations common.php (Latvian)

* New translations common.php (Norwegian Bokmal)

* New translations common.php (German Informal)

* New translations common.php (Indonesian)

* New translations common.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations common.php (Portuguese)

* New translations entities.php (Latvian)

* New translations common.php (Latvian)

* New translations settings.php (Portuguese)

* New translations common.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Polish)

* New translations common.php (Ukrainian)

* New translations entities.php (Ukrainian)

* New translations settings.php (Ukrainian)

* New translations settings.php (Russian)

* New translations settings.php (Russian)

* New translations common.php (Chinese Simplified)
2021-04-07 21:49:45 +01:00
Dan Brown
f0723b6ee7 Fixed social button icon/text misalignment 2021-04-06 22:00:07 +01:00
Dan Brown
2162da3a14 Updated project npm deps 2021-04-06 21:55:49 +01:00
Dan Brown
f02cfd8271 Removed mentions of 'mail' mail driver
Closes #2657
2021-03-27 15:56:36 +00:00
Dan Brown
b6a0f9f069 Create config.yml
Totally not discovered/copied from viewing the linuxserver.io options (https://github.com/linuxserver/docker-bookstack/blob/master/.github/ISSUE_TEMPLATE/config.yml)
2021-03-25 21:52:32 +00:00
Dan Brown
5c9c1d1a4b Updated shelf sort to allow default sort, added testing
Done during review of #2515
2021-03-21 23:06:15 +00:00
Dan Brown
ab4c5a55b8 Merge branch 'feature/sort-shelf-books' of git://github.com/guillaumehanotel/BookStack into guillaumehanotel-feature/sort-shelf-books 2021-03-21 21:52:39 +00:00
Dan Brown
43c2fc3c37 Updated dev-docker setup to not alter phpunit.xml
Tested on my machine via fresh dev instance with tests passing.
May need old users to drop their old volume data.
2021-03-21 17:42:10 +00:00
Dan Brown
371033a0f2 Merge branch 'master' into docker-tests 2021-03-21 16:49:22 +00:00
Dan Brown
06706a2d9c Added user filter to audit log
Included testing to cover.
Closes #2472
2021-03-21 15:04:32 +00:00
Dan Brown
c548c06086 Updated dev-docker setup
Removed extension installs for already installed things.
Removed tidy build bits.
Ensured it ran via quick build and test
2021-03-20 16:44:47 +00:00
Dan Brown
8e5067ee91 Performed fixes for failing tests on php8
- Commands that run a truncate DB action failed due to messing up the
  test transations so we mnaully work around that now to ensure a
transaction exists for the test to cleanup afterwards.
- Updated dompdf lib version
2021-03-20 16:25:02 +00:00
Dan Brown
0310b8614e Updated GH actions to use strings for php versions
Looked like 8.0 was being converted to 8
2021-03-20 15:40:08 +00:00
Dan Brown
829fecd338 Updated app to PHP7.3 min supported version, For php8 support
- Updated remaining dependancies
- Upped min versions used
- Updated GH actions to drop 7.2 and include 8.0
- Updated phpunit & tests to 9.x
2021-03-20 15:35:39 +00:00
Dan Brown
44a293f051 Fleshed out and checked over theme system docs 2021-03-20 15:09:17 +00:00
Dan Brown
a92c35a7ac Worked on theme system documentation 2021-03-19 23:06:50 +00:00
Dan Brown
691db40a33 Added login/register theme events 2021-03-19 21:54:50 +00:00
Dan Brown
2ae89f2c32 Added the possibility of social provider extension via theme
Also started docs page
2021-03-19 16:22:47 +00:00
Dan Brown
9d37af9453 Added web-middleware based theme events 2021-03-17 12:56:56 +00:00
Dan Brown
a5d2a26fcc Added testing for the back-end theme system done so far 2021-03-16 17:55:19 +00:00
Dan Brown
c61c3bc608 Started backend theme system
Allows customization of back-end components via event-driven handling
from the theme folder.
2021-03-16 17:14:03 +00:00
Dan Brown
1420f239fc Made session cookie path dynamic based on APP_URL 2021-03-16 13:03:07 +00:00
Dan Brown
3d0e1bc9db Merge branch 'master' of git://github.com/ckleemann/BookStack into ckleemann-master 2021-03-16 12:45:12 +00:00
Dan Brown
71ccb90ef4 Amended owned by search filter to use slugs 2021-03-15 18:27:03 +00:00
Dan Brown
c8564b7792 Merge branch 'search-owned-by-me' of git://github.com/benediktvolke/BookStack into benediktvolke-search-owned-by-me 2021-03-15 18:21:09 +00:00
Dan Brown
215c69acb2 Merge image name cleaning functions
Updated testing for changes and to check existing of new expected file
name.
Related to #2611
2021-03-14 23:20:21 +00:00
Dan Brown
c1f67372a7 Merge branch 'master' of git://github.com/webfoersterei/BookStack into webfoersterei-master 2021-03-14 22:55:30 +00:00
Dan Brown
b929c0adbb Performed further cleanup in permission service 2021-03-14 20:32:33 +00:00
Dan Brown
1e5951a75f Done a refactor pass on PermissionService
Could do with splitting out into seperate query/build classess really.
Closes #2633.
2021-03-14 19:52:07 +00:00
Dan Brown
a644f64c6b Merge branch 'v0.31.x' 2021-03-13 15:37:44 +00:00
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
339d4ec355 Fixed misalignment of page and chapter parent book
Could occur when a chapter was moved with deleted pages.
Fixes #2632
2021-03-13 15:18:37 +00:00
Dan Brown
615038ac6d Merge pull request #2626 from BookStackApp/2525_add_user_slugs
User slugs
2021-03-10 23:12:25 +00:00
Dan Brown
3c57cbc567 Updated testing for user slugs 2021-03-10 23:04:18 +00:00
Dan Brown
da929d5edc Updates search to use user slugs 2021-03-10 22:51:18 +00:00
Dan Brown
124c4d0778 Updated register paths to include user slugs 2021-03-10 22:37:53 +00:00
Dan Brown
19d79b6a0f Started rolling out user slugs to model and core controllers 2021-03-09 23:06:12 +00:00
Dan Brown
3a9caea846 Started work on user slugs
Related to #2525
2021-03-08 22:34:22 +00:00
Dan Brown
34e6098687 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-03-07 22:24:41 +00:00
Dan Brown
98a1e57ba9 Ran phpcbf and updated phpcs.xml 2021-03-07 22:24:05 +00:00
Dan Brown
f31cdf7bff New Crowdin updates (#2620)
* New translations settings.php (Norwegian Bokmal)

* New translations auth.php (Catalan)

* New translations settings.php (Catalan)

* New translations entities.php (Catalan)

* New translations settings.php (German Informal)

* New translations settings.php (Bosnian)

* New translations settings.php (Spanish)

* New translations settings.php (French)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (Czech)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Slovenian)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* 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 (Slovak)

* New translations settings.php (Danish)

* New translations settings.php (Russian)

* New translations settings.php (Polish)

* 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 auth.php (German)

* New translations auth.php (Spanish)

* New translations settings.php (Portuguese)

* New translations settings.php (German)

* New translations settings.php (Latvian)

* New translations settings.php (Vietnamese)

* New translations activities.php (French)

* New translations settings.php (Indonesian)

* New translations entities.php (French)

* New translations components.php (German Informal)
2021-03-07 17:41:58 +00:00
Dan Brown
9a3e1490ff Merge branch 'Ereza-master' 2021-03-07 17:25:27 +00:00
Dan Brown
1f2fd58e28 Merge branch 'master' of git://github.com/Ereza/BookStack into Ereza-master 2021-03-07 17:25:07 +00:00
Dan Brown
bcf3a0f677 Merge pull request #2616 from geins/master
Fixed german formal translation
2021-03-07 17:20:07 +00:00
Dan Brown
f40dedb451 Merge branch 'master' into master 2021-03-07 17:19:55 +00:00
Dan Brown
266f6846b5 Merge pull request #2609 from arcoai/fix/user-invite-email-subject-spanish-translation
Fixed user invite email subject in spanish translation (#2608)
2021-03-07 17:18:07 +00:00
Dan Brown
49ca9a9db8 Merge pull request #2533 from benediktvolke/fix-german-language
Fix German translation string
2021-03-07 17:15:40 +00:00
Dan Brown
e7a7d8cc1d Merge pull request #2513 from Baptistou/patch-1
Fix French translations
2021-03-07 17:14:57 +00:00
Dan Brown
34c2da4ab1 New Crowdin updates (#2618)
* New translations settings.php (Russian)

* 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 (Portuguese, Brazilian)

* 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 (Danish)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Persian)

* New translations settings.php (Bulgarian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (German Informal)

* New translations settings.php (Norwegian Bokmal)

* New translations settings.php (Bosnian)

* New translations settings.php (Czech)

* New translations settings.php (Arabic)

* New translations settings.php (Spanish)

* New translations settings.php (French)

* New translations settings.php (Portuguese)

* New translations settings.php (German)

* New translations settings.php (Latvian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Indonesian)

* New translations common.php (German Informal)

* New translations common.php (Spanish, Argentina)
2021-03-07 17:11:18 +00:00
Dan Brown
7497203014 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-03-07 17:03:21 +00:00
Dan Brown
d731a4f695 Updated language lists with Bosnian, Indonesian, Latvian & Portuguese 2021-03-07 17:02:28 +00:00
Dan Brown
03f6be6d0e New Crowdin updates (#2501)
* New translations activities.php (German Informal)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Polish)

* New translations common.php (Russian)

* New translations settings.php (Russian)

* New translations common.php (Slovak)

* New translations settings.php (Slovak)

* New translations common.php (Slovenian)

* New translations settings.php (Slovenian)

* New translations common.php (Swedish)

* New translations settings.php (Swedish)

* New translations common.php (Turkish)

* New translations common.php (Ukrainian)

* New translations settings.php (Dutch)

* New translations common.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations common.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations common.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations common.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations common.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations common.php (Norwegian Bokmal)

* New translations common.php (Polish)

* New translations common.php (Dutch)

* New translations settings.php (Norwegian Bokmal)

* New translations settings.php (Czech)

* New translations settings.php (German)

* New translations settings.php (German Informal)

* New translations common.php (French)

* New translations settings.php (French)

* New translations common.php (Spanish)

* New translations settings.php (Spanish)

* New translations common.php (Arabic)

* New translations settings.php (Arabic)

* New translations common.php (Bulgarian)

* New translations settings.php (Bulgarian)

* New translations common.php (Czech)

* New translations common.php (Danish)

* New translations settings.php (Korean)

* New translations settings.php (Danish)

* New translations common.php (German)

* New translations common.php (Hebrew)

* New translations settings.php (Hebrew)

* New translations common.php (Hungarian)

* New translations settings.php (Hungarian)

* New translations common.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Japanese)

* New translations settings.php (Japanese)

* New translations common.php (Korean)

* New translations common.php (German Informal)

* New translations common.php (Spanish)

* New translations settings.php (Spanish)

* New translations settings.php (Hebrew)

* New translations settings.php (Hebrew)

* New translations settings.php (Hebrew)

* New translations settings.php (Hebrew)

* New translations common.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations components.php (Hebrew)

* New translations passwords.php (Hebrew)

* New translations activities.php (Persian)

* New translations settings.php (Latvian)

* New translations settings.php (Bosnian)

* New translations passwords.php (Bosnian)

* New translations pagination.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations components.php (Bosnian)

* New translations common.php (Bosnian)

* New translations auth.php (Bosnian)

* New translations activities.php (Bosnian)

* New translations validation.php (Latvian)

* New translations passwords.php (Latvian)

* New translations common.php (Persian)

* New translations pagination.php (Latvian)

* New translations errors.php (Latvian)

* New translations entities.php (Latvian)

* New translations components.php (Latvian)

* New translations common.php (Latvian)

* New translations auth.php (Latvian)

* New translations activities.php (Latvian)

* New translations validation.php (Persian)

* New translations settings.php (Persian)

* New translations entities.php (Persian)

* New translations validation.php (Bosnian)

* New translations activities.php (Latvian)

* New translations common.php (Latvian)

* New translations common.php (Latvian)

* New translations passwords.php (Latvian)

* New translations auth.php (Latvian)

* New translations auth.php (Latvian)

* New translations activities.php (Bosnian)

* New translations activities.php (Bosnian)

* New translations components.php (Bosnian)

* New translations components.php (Bosnian)

* New translations activities.php (Latvian)

* New translations auth.php (Latvian)

* New translations activities.php (Latvian)

* New translations auth.php (Latvian)

* New translations pagination.php (Latvian)

* New translations passwords.php (Latvian)

* New translations auth.php (Latvian)

* New translations common.php (Latvian)

* New translations components.php (Latvian)

* New translations passwords.php (Latvian)

* New translations components.php (Latvian)

* New translations common.php (French)

* New translations settings.php (French)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations settings.php (Latvian)

* New translations errors.php (Latvian)

* New translations settings.php (Latvian)

* New translations common.php (Slovenian)

* New translations settings.php (Slovenian)

* New translations entities.php (Slovenian)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations errors.php (Latvian)

* New translations validation.php (Latvian)

* New translations validation.php (Latvian)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations errors.php (Latvian)

* New translations settings.php (Latvian)

* New translations activities.php (Portuguese, Brazilian)

* New translations common.php (Portuguese, Brazilian)

* New translations entities.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations validation.php (Portuguese, Brazilian)

* New translations validation.php (Latvian)

* New translations errors.php (Latvian)

* New translations settings.php (Latvian)

* New translations validation.php (Latvian)

* New translations common.php (German)

* New translations settings.php (German)

* New translations settings.php (Latvian)

* New translations validation.php (Latvian)

* New translations settings.php (Latvian)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations entities.php (Latvian)

* New translations errors.php (Latvian)

* New translations settings.php (Latvian)

* New translations activities.php (Portuguese)

* New translations auth.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations passwords.php (Indonesian)

* New translations pagination.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations components.php (Indonesian)

* New translations common.php (Indonesian)

* New translations activities.php (Indonesian)

* New translations auth.php (Portuguese)

* New translations validation.php (Portuguese)

* New translations settings.php (Portuguese)

* New translations passwords.php (Portuguese)

* New translations pagination.php (Portuguese)

* New translations errors.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations components.php (Portuguese)

* New translations common.php (Portuguese)

* New translations validation.php (Indonesian)

* New translations auth.php (Portuguese)

* New translations common.php (Portuguese)

* New translations components.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations errors.php (Portuguese)

* New translations components.php (Bosnian)

* New translations auth.php (Bosnian)

* New translations common.php (Bosnian)

* New translations pagination.php (Bosnian)

* New translations passwords.php (Bosnian)

* New translations auth.php (Bosnian)

* New translations errors.php (Portuguese)

* New translations errors.php (Portuguese)

* New translations pagination.php (Portuguese)

* New translations passwords.php (Portuguese)

* New translations settings.php (Portuguese)

* New translations validation.php (Portuguese)

* New translations settings.php (Portuguese)

* New translations settings.php (Portuguese)

* New translations activities.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations validation.php (Bosnian)

* New translations validation.php (Bosnian)

* New translations validation.php (Bosnian)

* New translations validation.php (Bosnian)

* New translations validation.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations errors.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations activities.php (Indonesian)

* New translations activities.php (Indonesian)

* New translations auth.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations auth.php (Indonesian)

* New translations auth.php (Indonesian)

* New translations common.php (Indonesian)

* New translations components.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations entities.php (Latvian)

* New translations errors.php (Latvian)

* New translations settings.php (Latvian)

* New translations entities.php (Indonesian)

* New translations settings.php (Latvian)

* New translations entities.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations entities.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations pagination.php (Indonesian)

* New translations passwords.php (Indonesian)

* New translations validation.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations validation.php (Indonesian)

* New translations common.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations validation.php (Indonesian)

* New translations entities.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations entities.php (Bosnian)

* New translations validation.php (Indonesian)

* New translations auth.php (Indonesian)

* New translations validation.php (Indonesian)
2021-03-07 16:44:28 +00:00
Dan Brown
938b5b4d1d Updated github CI actions for ubuntu 20.04
Squash of:
commit 6b7d305e776dcec2103b9e4486f3c57c674d046f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:25:43 2021 +0000

    Updated migrations action and added ldap

commit 139d87687d3d4d6c2368adb522932469419c848a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:18:55 2021 +0000

    Updated mysql user auth

commit 326d11e0d3b96bfb2e6ba446ada9cee479d3eb1a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:15:33 2021 +0000

    Moved extensions to right place

commit aaa1e159ccbf535292615e094bb58ce6488726df
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:13:22 2021 +0000

    Added php extensions

commit 3720324288c974d825ff2a63306a5fbf1c0478ab
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:07:37 2021 +0000

    Update gh ci branches list for testing

commit 4e3a302a5a4480b45d967f398abbe33883b6d0fb
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 7 16:06:19 2021 +0000

    Updated github ci to use ubuntu 20.04
2021-03-07 16:31:51 +00:00
geins
792b51b5dc Merge pull request #2 from geins/settings.php-german-patch
completed german translation of 4 english residues
2021-03-06 21:32:27 +01:00
geins
a57eae20b0 completed german translation of 4 english residues 2021-03-06 21:29:59 +01:00
geins
1ffe212d83 Merge pull request #1 from geins/geins-patch-1
Translation error in formal german vs informal
2021-03-06 20:30:06 +01:00
geins
e29c07b28d Translation error
Translation error: informal language in formal file Sie<->Du
2021-03-06 18:57:49 +01:00
Timo Förster
745d15d200 Allow uploads of files containing dots in filename. Closes BookStackApp/BookStack#2217 2021-03-04 22:27:20 +01:00
Arco Aplicaciones - David Blanco
05cfe74904 Fixed user invite email subject in spanish translation (#2608) 2021-03-04 00:07:17 +01:00
Dan Brown
4d4a57d1bf Converted some tests from BrowserKit, Updated shared helpers 2021-03-03 22:11:00 +00:00
Dan Brown
382f155f76 Better aligned handler with core laravel 2021-03-02 21:59:12 +00:00
Dan Brown
60030a774d Merge branch 'v0.31.x' 2021-03-02 21:43:30 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
26730e56ea Updated composer dependancies
Primarily to fix aws library for non-amazon use.
Related to #2603
2021-03-02 21:06:45 +00:00
Dan Brown
7b6f8cb902 Merge pull request #2591 from philjak/add_bookshelf_view_type_to_env
Adding APP_VIEWS_BOOKSHELF to .ENV
2021-02-25 22:00:03 +00:00
Philip
111835f402 Adding APP_VIEWS_BOOKSHELF to .ENV 2021-02-25 07:51:38 +01:00
ckleemann
3fc935d4bb Introduce an env variable for the Session Cookie Path 2021-02-20 14:25:28 +01:00
Benedikt Volke
b939785ece Add checkbox on search page 2021-02-14 11:40:38 +01:00
Benedikt Volke
723628cfcf Add translation string 2021-02-14 11:40:24 +01:00
Benedikt Volke
cf489453c9 Add test for new search tag 2021-02-14 11:40:02 +01:00
Benedikt Volke
6616065d82 Add filter method to search runner 2021-02-14 11:39:18 +01:00
Dan Brown
2df82dd870 Added padding to the bottom of the WYSIWYG editor
Also fixed weird affects from body now always being flex.
For #1075
2021-02-12 23:35:42 +00:00
Dan Brown
0ca8d7fc03 Updated books list view description to be limited by css
Instead of length limited
Related to #1222
2021-02-12 23:10:30 +00:00
Dan Brown
f36e6d9917 Updtd entity-selector for keyboard nav and new component system
For #2064
2021-02-12 22:10:37 +00:00
Dan Brown
6a4b020dd8 Removed user and revision links in export meta
Closes #2526
2021-02-12 20:58:01 +00:00
Dan Brown
b51ede2372 Updated php deps to avoid a couple of abandoned packages 2021-02-11 23:46:26 +00:00
Dan Brown
1a4797abc4 Updated update-url command to handle array values
Also added message to clear the cache after running.
For #2546
2021-02-11 23:14:37 +00:00
Dan Brown
c09300c06f Split command tests out to indavidual test files 2021-02-11 22:42:36 +00:00
Dan Brown
ae353bb3f4 Updated update-url command to look at setting values
For #2546
Need to consider new JSON-array based setting values.
2021-02-10 23:47:58 +00:00
Dan Brown
54f5bf9437 Aligned setting helper with new get method changes
Also removed old unsused facade that existed for settings.
2021-02-10 23:21:49 +00:00
Dan Brown
b0f4500c34 Added env option for setting dark mode default
Also allowed config-centralised default user settings for this change
and bought existing user-level view options into that default settings
system to be cleaner in code usage.

For #2081
2021-02-07 23:12:05 +00:00
Dan Brown
af032f8993 Tweaked LDAP TLS Implementation
- Moved the ldap function out to our separate service for easier
  testing.
- Added testing for the option.
- Moved tls_insecure part back up above connection start as found more
  reliable there.

Done a lot of real-connection testing during this review.
Used wireshare to ensure TLS connection does take place.
Found LDAP_TLS_INSECURE=false can action unreliably, restarting php-fpm
helped.
Tested both trusted and untrusted certificates.
2021-02-07 20:00:04 +00:00
Dan Brown
f177b02cae Merge branch 'master' of git://github.com/Body4/BookStack into Body4-master 2021-02-07 18:33:10 +00:00
Dan Brown
5323cb5224 Removed some old front-end md rendering elements
Also ensured revisions were not created more often than expected.
Summary field null check was triggering revision save even when empty
since it was still in request.

Related to #1846
2021-02-06 23:11:20 +00:00
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
a98fc71720 Updated composer deps again after merge 2021-02-06 14:22:55 +00:00
Dan Brown
9a05223e7d Merge branch 'v0.31.x' 2021-02-06 14:22:19 +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
d759f9c121 Merge branch 'master' of git://github.com/i4j5/BookStack into i4j5-master 2021-02-06 13:21:14 +00:00
Dan Brown
f25e585008 Moved sketchy file samples to base64 equivilents
Hides them from AV systems.
Done some test helper cleaning while at it.

Related to #1571
2021-02-06 00:16:27 +00:00
Dan Brown
4f96cd9164 Altered header to keep search box center
For #2310
2021-02-04 23:11:55 +00:00
Eduard Ereza Martínez
7893e8229f Add Catalan translation 2021-02-04 00:55:01 +01:00
Benedikt Volke
795ef67712 fix image delete confirm text 2021-02-03 13:00:08 +01:00
Aleksandr Sazhin
88f6d3f241 Update TrashCan.php
bookshelf
2021-02-03 10:03:54 +03:00
Dan Brown
8e87f01aa0 Added stats badge and league to attribution 2021-02-02 22:12:41 +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
Baptiste Thémine
f40c389a15 Fix French translations
- "supprimer" is a better translation than "écarter" for "discard"
- "actuel" is a better translation than "courant" for "current"
- "modifications" is a better translation than "changements" for change
- "journal des modifications" is a better translation than "journal des changements" for changelog
- The button "Remplir le journal des changements" for pages_edit_set_changelog caused a wrapping in the header, it is replaced by "Décrivez vos modifications"
2021-02-02 13:12:07 +01:00
Dan Brown
bc1e84325c Made codemirror editor load a lot more efficient
Brings down total editor load time from about 11s to 7s from testing in
4x reduced CPU speed in chrome.
About 1.5 seconds of that is editor init/page load.
Post editor-init/page-load time is now 60% of previous from testing.

Related to #2518
2021-01-31 16:26:54 +00:00
Abijeet
a0c605faae Docker: Fix PHP tests
This creates another mysql_testing database during db service setup

Replace server with env tags in phpunit.xml in order to force
override certain parameters when tests are run. See:
https://github.com/sebastianbergmann/phpunit/issues/2353 for more
information.

Rename primary developer Docker database from bookstack-test to
bookstack-dev. bookstack-test is used as the mysql_testing database
2021-01-31 18:54:24 +05:30
Abijeet
ba2033a8fb Docker: Fix permission with node service by adding user key
Fixes the following error:

glob error:
[Error: EACCES: permission denied, scandir '/root/.npm/_logs'] {
  errno: -13,
  code: 'EACCES',
  syscall: 'scandir',
  path: '/root/.npm/_logs'
}

On Linux, these lines ensure file ownership is set to your host
user/group
2021-01-31 16:57:30 +05:30
Guillaume Hanotel
a7848b916b Improve sorting Shelf Books 2021-01-31 04:28:25 +01:00
Dan Brown
44c41e9e4d Updated footer links to be a configurable list
Made so footer link ordering, names and urls can be set.
Cleaned up some of the setting-service and added support for array
setting types, which are cleaned on entry and stored as json with a new
type indicator column on the settings table for auto-decode.
Also added testing to cover this feature.

Related to #1973 and #854
2021-01-31 00:23:15 +00:00
Dan Brown
a663364223 Merge branch 'footer-links' of git://github.com/james-geiger/BookStack into james-geiger-footer-links 2021-01-30 22:03:16 +00:00
Dan Brown
72fda8f592 Merge pull request #2510 from BookStackApp/fix-docker-perm
Docker: Fix permission with node service by adding node as user
2021-01-30 21:51:00 +00:00
Abijeet
1aa9465611 Docker: Fix permission with node service by adding node as user
See: https://github.com/nodejs/docker-node/blob/master/docs/BestPractices.md#non-root-user

Fixes the following error:

glob error:
[Error: EACCES: permission denied, scandir '/root/.npm/_logs'] {
  errno: -13,
  code: 'EACCES',
  syscall: 'scandir',
  path: '/root/.npm/_logs'
}
2021-01-30 23:46:53 +05:30
Dan Brown
4d3194d784 Merge branch 'patch-1' of git://github.com/l1n/BookStack into l1n-patch-1 2021-01-30 17:15:23 +00:00
Dan Brown
5404f22bf9 Added codemirror refresh on details blog toggle
For #781
2021-01-30 17:04:30 +00:00
Dan Brown
ccb2cb5b7c Merge branch 'feature_add_add-button_to_home_view' of git://github.com/philjak/BookStack into philjak-feature_add_add-button_to_home_view 2021-01-30 16:40:13 +00:00
Guillaume Hanotel
26ba056302 Sort Books within Shelves 2021-01-29 08:02:18 +01:00
Dan Brown
0dac9c68f0 Changed how the cache is mocked in status test 2021-01-28 23:13:55 +00:00
Dan Brown
3df6c9ac05 Updated service provider reference, added phpunit env var 2021-01-28 22:46:15 +00:00
Dan Brown
2db081938f Updated deps, focused on new version of htmldiff 2021-01-28 22:04:19 +00:00
Baptiste Thémine
d979570227 Fix French translation for permissions_update 2021-01-27 13:40:58 +01:00
Dan Brown
7ba6962707 Removed lesser-used middleware and updated localization middleware
So that DB/User access is not explicitly enforced.
Same for GlobalViewData middleware although that was also just doubling
up on ways to access user/auth info.
Also cleaned up Localization Middleware doc blocks.
2021-01-17 13:41:43 +00:00
Dan Brown
6eda1c1fb2 Added status endpoint
For #2467
2021-01-17 13:21:57 +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
Nova
b8aabfffe8 Update form.blade.php 2021-01-13 12:45:18 -08:00
Nova
ac8e124d01 Update form.blade.php 2021-01-13 12:23:20 -08:00
Nova
857f8c2a95 Disable autocomplete on the change password field 2021-01-13 12:21:57 -08: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
9884cca00c Merge branch 'v0.30.x' 2020-12-17 21:47:59 +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
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
Boddy4
20f9a50cee LDAP: Added TLS support 2020-11-18 01:05:29 +01: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
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
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
Timo Schwarzer
a74d551bd6 Install composer dependencies in Docker entrypoint 2020-10-01 11:34:56 +02:00
Dan Brown
691027a522 Started implementation of recycle bin functionality 2020-09-27 23:24:33 +01:00
Jan Mareš
034478409e Add support Windows Authentication via SAML 2020-04-03 14:05:07 +02:00
James Geiger
fe438bdb45 Add footer element, styles, and associated settings 2020-03-18 22:28:06 -05:00
jakob
6acd958927 Add the "Create Shelf" resp. "Create Book" to the home view 2019-10-30 11:42:37 +01:00
643 changed files with 21032 additions and 7675 deletions

View File

@@ -51,7 +51,7 @@ DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
@@ -195,6 +195,7 @@ LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
LDAP_START_TLS=false
LDAP_TLS_INSECURE=false
LDAP_ID_ATTRIBUTE=uid
LDAP_EMAIL_ATTRIBUTE=mail
@@ -221,6 +222,7 @@ SAML2_IDP_x509=null
SAML2_ONELOGIN_OVERRIDES=null
SAML2_DUMP_USER_DETAILS=false
SAML2_AUTOLOAD_METADATA=false
SAML2_IDP_AUTHNCONTEXT=true
# SAML group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/
@@ -245,16 +247,29 @@ AVATAR_URL=
DRAWIO=true
# Default item listing view
# Used for public visitors and user's without a preference
# Can be 'list' or 'grid'
# Used for public visitors and user's without a preference.
# Can be 'list' or 'grid'.
APP_VIEWS_BOOKS=list
APP_VIEWS_BOOKSHELVES=grid
APP_VIEWS_BOOKSHELF=grid
# Use dark mode by default
# Will be overriden by any user/session preference.
APP_DEFAULT_DARK_MODE=false
# Page revision limit
# Number of page revisions to keep in the system before deleting old revisions.
# 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,6 +280,12 @@ 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

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.

9
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@@ -0,0 +1,9 @@
blank_issues_enabled: false
contact_links:
- name: Discord chat support
url: https://discord.gg/ztkBqR2
about: Realtime support / chat with the community and the team.
- name: Debugging & Common Issues
url: https://www.bookstackapp.com/docs/admin/debugging/
about: Find details on how to debug issues and view common issues with thier resolutions.

View File

@@ -49,6 +49,12 @@ Name :: Languages
@jzoy :: Simplified Chinese
@ististudio :: Korean
@leomartinez :: Spanish Argentina
@geins :: German
@Ereza :: Catalan
@benediktvolke :: German
@Baptistou :: French
@arcoai :: Spanish
@Jokuna :: Korean
cipi1965 :: Italian
Mykola Ronik (Mantikor) :: Ukrainian
furkanoyk :: Turkish
@@ -123,3 +129,39 @@ 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
Irfan Hukama Arsyad (IrfanArsyad) :: Indonesian
Jeff Huang (s8321414) :: Chinese Traditional
Luís Tiago Favas (starkyller) :: Portuguese
semirte :: Bosnian
aarchijs :: Latvian
Martins Pilsetnieks (pilsetnieks) :: Latvian
Yonatan Magier (yonatanmgr) :: Hebrew
FastHogi :: German Informal; German
Ole Anders (Swoy) :: Norwegian Bokmal
Atlochowski (atlochowski) :: Polish
Simon (DefaultSimon) :: Slovenian
Reinis Mednis (Mednis) :: Latvian
toisho (toishoki) :: Turkish
nikservik :: Ukrainian; Russian; Polish
HenrijsS :: Latvian
Pascal R-B (pborgner) :: German
Boris (Ginfred) :: Russian
Jonas Anker Rasmussen (jonasanker) :: Danish
Gerwin de Keijzer (gdekeijzer) :: Dutch; German Informal; German
kometchtech :: Japanese
Auri (Atalonica) :: Catalan
Francesco Franchina (ffranchina) :: Italian
Aimrane Kds (aimrane.kds) :: Arabic
whenwesober :: Indonesian
Rem (remkovdhoef) :: Dutch

View File

@@ -5,6 +5,7 @@ on:
branches:
- master
- release
- gh_actions_update
pull_request:
branches:
- '*'
@@ -13,13 +14,19 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
php: [7.2, 7.4]
php: ['7.3', '7.4', '8.0']
steps:
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
with:
php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory
id: composer-cache
run: |
@@ -38,7 +45,7 @@ jobs:
- name: Setup Database
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 "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'

View File

@@ -5,6 +5,7 @@ on:
branches:
- master
- release
- gh_actions_update
pull_request:
branches:
- '*'
@@ -13,13 +14,19 @@ on:
jobs:
build:
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
strategy:
matrix:
php: [7.2, 7.4]
php: ['7.3', '7.4', '8.0']
steps:
- uses: actions/checkout@v1
- name: Setup PHP
uses: shivammathur/setup-php@b7d1d9c9a92d8d8463ce36d7f60da34d461724f8
with:
php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, mysql, ldap
- name: Get Composer Cache Directory
id: composer-cache
run: |
@@ -38,7 +45,7 @@ jobs:
- 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 "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED WITH mysql_native_password BY 'bookstack-test';"
mysql -uroot -proot -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
mysql -uroot -proot -e 'FLUSH PRIVILEGES;'

View File

@@ -3,18 +3,20 @@
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\Database\Eloquent\Relations\MorphTo;
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
{
@@ -22,7 +24,7 @@ class Activity extends Model
/**
* Get the entity for this activity.
*/
public function entity()
public function entity(): MorphTo
{
if ($this->entity_type === '') {
$this->entity_type = null;
@@ -32,20 +34,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 +63,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,57 +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,
]);
}
@@ -61,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;
}
/**
@@ -78,7 +78,7 @@ class ActivityService
public function latest(int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['user', 'entity'])
->skip($count * $page)
@@ -94,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');
}
if ($entity->isA('book') || $entity->isA('chapter')) {
$queryIds[(new Page)->getMorphClass()] = $entity->pages()->visible()->pluck('id');
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['entity', 'user.avatar'])
$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();
@@ -118,7 +131,7 @@ class ActivityService
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->skip($count * $page)
@@ -152,9 +165,9 @@ 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);

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,6 +1,8 @@
<?php namespace BookStack\Actions;
use BookStack\Ownable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property string text
@@ -8,25 +10,25 @@ use BookStack\Ownable;
* @property int|null parent_id
* @property int local_id
*/
class Comment extends Ownable
class Comment extends Model
{
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,7 +1,8 @@
<?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
@@ -44,6 +45,7 @@ class CommentRepo
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
ActivityService::addForEntity($entity, ActivityType::COMMENTED_ON);
return $comment;
}

View File

@@ -2,14 +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'];
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
/**
* Get the entity that this tag belongs to

View File

@@ -1,7 +1,7 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use DB;
use Illuminate\Support\Collection;
@@ -26,7 +26,9 @@ class TagRepo
*/
public function getNameSuggestions(?string $searchTerm): Collection
{
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
$query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('name');
if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@@ -45,7 +47,9 @@ class TagRepo
*/
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
{
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
$query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('value');
if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');

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)
@@ -65,7 +65,7 @@ class ViewService
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
@@ -74,34 +74,37 @@ 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()
->having('last_viewed_at', '>', 0)
->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
{
@@ -123,5 +142,4 @@ class ApiDocsGenerator
];
});
}
}
}

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

@@ -163,4 +163,4 @@ class ApiTokenGuard implements Guard
{
$this->user = null;
}
}
}

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
{
@@ -301,5 +299,4 @@ class ExternalBaseSessionGuard implements StatefulGuard
return $this;
}
}

View File

@@ -5,14 +5,12 @@ namespace BookStack\Auth\Access\Guards;
use BookStack\Auth\Access\LdapService;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\LdapException;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\UserRegistrationException;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Session\Session;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class LdapSessionGuard extends ExternalBaseSessionGuard
@@ -23,13 +21,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
/**
* LdapSessionGuard constructor.
*/
public function __construct($name,
public function __construct(
$name,
UserProvider $provider,
Session $session,
LdapService $ldapService,
RegistrationService $registrationService
)
{
) {
$this->ldapService = $ldapService;
parent::__construct($name, $provider, $session, $registrationService);
}
@@ -119,5 +117,4 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
return $this->registrationService->registerUser($details, null, false);
}
}

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
{
@@ -36,5 +34,4 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
{
return false;
}
}

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
{
@@ -32,6 +31,14 @@ class Ldap
return ldap_set_option($ldapConnection, $option, $value);
}
/**
* Start TLS on the given LDAP connection.
*/
public function startTls($ldapConnection): bool
{
return ldap_start_tls($ldapConnection);
}
/**
* Set the version number for the given ldap connection.
* @param $ldapConnection

View File

@@ -85,9 +85,9 @@ class LdapService extends ExternalAuthService
$userCn = $this->getUserResponseProperty($user, 'cn', null);
$formatted = [
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
];
@@ -187,8 +187,8 @@ class LdapService extends ExternalAuthService
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
// Disable certificate verification.
// This option works globally and must be set before a connection is created.
if ($this->config['tls_insecure']) {
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
@@ -205,6 +205,14 @@ class LdapService extends ExternalAuthService
$this->ldap->setVersion($ldapConnection, $this->config['version']);
}
// Start and verify TLS if it's enabled
if ($this->config['start_tls']) {
$started = $this->ldap->startTls($ldapConnection);
if (!$started) {
throw new LdapException('Could not start TLS connection');
}
}
$this->ldapConnection = $ldapConnection;
return $this->ldapConnection;
}

View File

@@ -1,9 +1,13 @@
<?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 BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Exception;
class RegistrationService
@@ -68,6 +72,9 @@ class RegistrationService
$newUser->socialAccounts()->save($socialAccount);
}
Activity::add(ActivityType::AUTH_REGISTER, $socialAccount ?? $newUser);
Theme::dispatch(ThemeEvents::AUTH_REGISTER, $socialAccount ? $socialAccount->driver : auth()->getDefaultDriver(), $newUser);
// Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
$newUser->save();
@@ -79,7 +86,6 @@ class RegistrationService
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
}
return $newUser;
@@ -105,5 +111,4 @@ class RegistrationService
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
}
}
}
}

View File

@@ -1,9 +1,13 @@
<?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 BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Exception;
use Illuminate\Support\Str;
use OneLogin\Saml2\Auth;
@@ -372,6 +376,8 @@ class Saml2Service extends ExternalAuthService
}
auth()->login($user);
Activity::add(ActivityType::AUTH_LOGIN, "saml2; {$user->logDescriptor()}");
Theme::dispatch(ThemeEvents::AUTH_LOGIN, 'saml2', $user);
return $user;
}
}

View File

@@ -1,36 +1,64 @@
<?php namespace BookStack\Auth\Access;
use BookStack\Actions\ActivityType;
use BookStack\Auth\SocialAccount;
use BookStack\Auth\UserRepo;
use BookStack\Auth\User;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\Factory as Socialite;
use Laravel\Socialite\Contracts\Provider;
use Laravel\Socialite\Contracts\User as SocialUser;
use SocialiteProviders\Manager\SocialiteWasCalled;
use Symfony\Component\HttpFoundation\RedirectResponse;
class SocialAuthService
{
protected $userRepo;
/**
* The core socialite library used.
* @var Socialite
*/
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* The default built-in social drivers we support.
* @var string[]
*/
protected $validSocialDrivers = [
'google',
'github',
'facebook',
'slack',
'twitter',
'azure',
'okta',
'gitlab',
'twitch',
'discord'
];
/**
* Callbacks to run when configuring a social driver
* for an initial redirect action.
* Array is keyed by social driver name.
* Callbacks are passed an instance of the driver.
* @var array<string, callable>
*/
protected $configureForRedirectCallbacks = [];
/**
* SocialAuthService constructor.
*/
public function __construct(UserRepo $userRepo, Socialite $socialite, SocialAccount $socialAccount)
public function __construct(Socialite $socialite)
{
$this->userRepo = $userRepo;
$this->socialite = $socialite;
$this->socialAccount = $socialAccount;
}
/**
* Start the social login path.
* @throws SocialDriverNotConfigured
@@ -38,7 +66,7 @@ class SocialAuthService
public function startLogIn(string $socialDriver): RedirectResponse
{
$driver = $this->validateDriver($socialDriver);
return $this->getSocialDriver($driver)->redirect();
return $this->getDriverForRedirect($driver)->redirect();
}
/**
@@ -48,7 +76,7 @@ class SocialAuthService
public function startRegister(string $socialDriver): RedirectResponse
{
$driver = $this->validateDriver($socialDriver);
return $this->getSocialDriver($driver)->redirect();
return $this->getDriverForRedirect($driver)->redirect();
}
/**
@@ -58,11 +86,11 @@ class SocialAuthService
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser): SocialUser
{
// Check social account has not already been used
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
if (SocialAccount::query()->where('driver_id', '=', $socialUser->getId())->exists()) {
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount' => $socialDriver]), '/login');
}
if ($this->userRepo->getByEmail($socialUser->getEmail())) {
if (User::query()->where('email', '=', $socialUser->getEmail())->exists()) {
$email = $socialUser->getEmail();
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $email]), '/login');
}
@@ -89,7 +117,7 @@ class SocialAuthService
$socialId = $socialUser->getId();
// Get any attached social accounts or users
$socialAccount = $this->socialAccount->where('driver_id', '=', $socialId)->first();
$socialAccount = SocialAccount::query()->where('driver_id', '=', $socialId)->first();
$isLoggedIn = auth()->check();
$currentUser = user();
$titleCaseDriver = Str::title($socialDriver);
@@ -98,14 +126,16 @@ class SocialAuthService
// Simply log the user into the application.
if (!$isLoggedIn && $socialAccount !== null) {
auth()->login($socialAccount->user);
Activity::add(ActivityType::AUTH_LOGIN, $socialAccount);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $socialAccount->user);
return redirect()->intended('/');
}
// When a user is logged in but the social account does not exist,
// Create the social account and attach it to the user & redirect to the profile page.
if ($isLoggedIn && $socialAccount === null) {
$this->fillSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($this->socialAccount);
$account = $this->newSocialAccount($socialDriver, $socialUser);
$currentUser->socialAccounts()->save($account);
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
return redirect($currentUser->getEditUrl());
}
@@ -127,7 +157,7 @@ class SocialAuthService
if (setting('registration-enabled') && config('auth.method') !== 'ldap' && config('auth.method') !== 'saml2') {
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => $titleCaseDriver]);
}
throw new SocialSignInAccountNotUsed($message, '/login');
}
@@ -204,21 +234,19 @@ class SocialAuthService
/**
* Fill and return a SocialAccount from the given driver name and SocialUser.
*/
public function fillSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
public function newSocialAccount(string $socialDriver, SocialUser $socialUser): SocialAccount
{
$this->socialAccount->fill([
'driver' => $socialDriver,
return new SocialAccount([
'driver' => $socialDriver,
'driver_id' => $socialUser->getId(),
'avatar' => $socialUser->getAvatar()
'avatar' => $socialUser->getAvatar()
]);
return $this->socialAccount;
}
/**
* Detach a social account from a user.
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function detachSocialAccount(string $socialDriver)
public function detachSocialAccount(string $socialDriver): void
{
user()->socialAccounts()->where('driver', '=', $socialDriver)->delete();
}
@@ -226,7 +254,7 @@ class SocialAuthService
/**
* Provide redirect options per service for the Laravel Socialite driver
*/
public function getSocialDriver(string $driverName): Provider
protected function getDriverForRedirect(string $driverName): Provider
{
$driver = $this->socialite->driver($driverName);
@@ -237,6 +265,33 @@ class SocialAuthService
$driver->with(['resource' => 'https://graph.windows.net']);
}
if (isset($this->configureForRedirectCallbacks[$driverName])) {
$this->configureForRedirectCallbacks[$driverName]($driver);
}
return $driver;
}
/**
* Add a custom socialite driver to be used.
* Driver name should be lower_snake_case.
* Config array should mirror the structure of a service
* within the `Config/services.php` file.
* Handler should be a Class@method handler to the SocialiteWasCalled event.
*/
public function addSocialDriver(
string $driverName,
array $config,
string $socialiteHandler,
callable $configureForRedirect = null
) {
$this->validSocialDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
if (!is_null($configureForRedirect)) {
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
}
}
}

View File

@@ -1,7 +1,7 @@
<?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;

View File

@@ -1,26 +1,33 @@
<?php namespace BookStack\Auth\Permissions;
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\EntityProvider;
use BookStack\Entities\Page;
use BookStack\Ownable;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Illuminate\Database\Connection;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
use Throwable;
class PermissionService
{
/**
* @var ?array
*/
protected $userRoles = null;
protected $currentAction;
protected $isAdminUser;
protected $userRoles = false;
protected $currentUserModel = false;
/**
* @var ?User
*/
protected $currentUserModel = null;
/**
* @var Connection
@@ -28,52 +35,20 @@ class PermissionService
protected $db;
/**
* @var JointPermission
* @var array
*/
protected $jointPermission;
/**
* @var Role
*/
protected $role;
/**
* @var EntityPermission
*/
protected $entityPermission;
/**
* @var EntityProvider
*/
protected $entityProvider;
protected $entityCache;
/**
* PermissionService constructor.
* @param JointPermission $jointPermission
* @param EntityPermission $entityPermission
* @param Role $role
* @param Connection $db
* @param EntityProvider $entityProvider
*/
public function __construct(
JointPermission $jointPermission,
Permissions\EntityPermission $entityPermission,
Role $role,
Connection $db,
EntityProvider $entityProvider
) {
public function __construct(Connection $db)
{
$this->db = $db;
$this->jointPermission = $jointPermission;
$this->entityPermission = $entityPermission;
$this->role = $role;
$this->entityProvider = $entityProvider;
}
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
@@ -82,81 +57,63 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param \BookStack\Entities\Entity[] $entities
* @param Entity[] $entities
*/
protected function readyEntityCache($entities = [])
protected function readyEntityCache(array $entities = [])
{
$this->entityCache = [];
foreach ($entities as $entity) {
$type = $entity->getType();
if (!isset($this->entityCache[$type])) {
$this->entityCache[$type] = collect();
$class = get_class($entity);
if (!isset($this->entityCache[$class])) {
$this->entityCache[$class] = collect();
}
$this->entityCache[$type]->put($entity->id, $entity);
$this->entityCache[$class]->put($entity->id, $entity);
}
}
/**
* Get a book via ID, Checks local cache
* @param $bookId
* @return Book
*/
protected function getBook($bookId)
protected function getBook(int $bookId): ?Book
{
if (isset($this->entityCache['book']) && $this->entityCache['book']->has($bookId)) {
return $this->entityCache['book']->get($bookId);
if (isset($this->entityCache[Book::class]) && $this->entityCache[Book::class]->has($bookId)) {
return $this->entityCache[Book::class]->get($bookId);
}
$book = $this->entityProvider->book->find($bookId);
if ($book === null) {
$book = false;
}
return $book;
return Book::query()->withTrashed()->find($bookId);
}
/**
* Get a chapter via ID, Checks local cache
* @param $chapterId
* @return \BookStack\Entities\Book
*/
protected function getChapter($chapterId)
protected function getChapter(int $chapterId): ?Chapter
{
if (isset($this->entityCache['chapter']) && $this->entityCache['chapter']->has($chapterId)) {
return $this->entityCache['chapter']->get($chapterId);
if (isset($this->entityCache[Chapter::class]) && $this->entityCache[Chapter::class]->has($chapterId)) {
return $this->entityCache[Chapter::class]->get($chapterId);
}
$chapter = $this->entityProvider->chapter->find($chapterId);
if ($chapter === null) {
$chapter = false;
}
return $chapter;
return Chapter::query()
->withTrashed()
->find($chapterId);
}
/**
* Get the roles for the current user;
* @return array|bool
* Get the roles for the current logged in user.
*/
protected function getRoles()
protected function getCurrentUserRoles(): array
{
if ($this->userRoles !== false) {
if (!is_null($this->userRoles)) {
return $this->userRoles;
}
$roles = [];
if (auth()->guest()) {
$roles[] = $this->role->getSystemRole('public')->id;
return $roles;
$this->userRoles = [Role::getSystemRole('public')->id];
} else {
$this->userRoles = $this->currentUser()->roles->pluck('id')->values()->all();
}
foreach ($this->currentUser()->roles as $role) {
$roles[] = $role->id;
}
return $roles;
return $this->userRoles;
}
/**
@@ -164,59 +121,57 @@ class PermissionService
*/
public function buildJointPermissions()
{
$this->jointPermission->truncate();
JointPermission::query()->truncate();
$this->readyEntityCache();
// Get all roles (Should be the most limited dimension)
$roles = $this->role->with('permissions')->get()->all();
$roles = Role::query()->with('permissions')->get()->all();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
->chunk(50, function ($shelves) use ($roles) {
Bookshelf::query()->withTrashed()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
}
/**
* Get a query for fetching a book with it's children.
* @return QueryBuilder
*/
protected function bookFetchQuery()
protected function bookFetchQuery(): Builder
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
return Book::query()->withTrashed()
->select(['id', 'restricted', 'owned_by'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'restricted', 'owned_by', 'book_id', 'chapter_id']);
}
]);
}
/**
* @param Collection $shelves
* @param array $roles
* @param bool $deleteOld
* @throws \Throwable
* Build joint permissions for the given shelf and role combinations.
* @throws Throwable
*/
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
protected function buildJointPermissionsForShelves(EloquentCollection $shelves, array $roles, bool $deleteOld = false)
{
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($shelves->all());
}
$this->createManyJointPermissions($shelves, $roles);
$this->createManyJointPermissions($shelves->all(), $roles);
}
/**
* Build joint permissions for an array of books
* @param Collection $books
* @param array $roles
* @param bool $deleteOld
* Build joint permissions for the given book and role combinations.
* @throws Throwable
*/
protected function buildJointPermissionsForBooks($books, $roles, $deleteOld = false)
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
{
$entities = clone $books;
@@ -233,55 +188,53 @@ class PermissionService
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities, $roles);
$this->createManyJointPermissions($entities->all(), $roles);
}
/**
* Rebuild the entity jointPermissions for a particular entity.
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
* @throws Throwable
*/
public function buildJointPermissionsForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity->isA('book')) {
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, $this->role->newQuery()->get(), true);
$this->buildJointPermissionsForBooks($books, Role::query()->get()->all(), true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity->isA('page') && $entity->chapter_id) {
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity->isA('chapter')) {
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildJointPermissionsForEntities(collect($entities));
$this->buildJointPermissionsForEntities($entities);
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
* @param Collection $entities
* @throws \Throwable
* @throws Throwable
*/
public function buildJointPermissionsForEntities(Collection $entities)
public function buildJointPermissionsForEntities(array $entities)
{
$roles = $this->role->newQuery()->get();
$this->deleteManyJointPermissionsForEntities($entities->all());
$roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Build the entity jointPermissions for a particular role.
* @param Role $role
*/
public function buildJointPermissionForRole(Role $role)
{
@@ -294,7 +247,7 @@ class PermissionService
});
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
@@ -302,7 +255,6 @@ class PermissionService
/**
* Delete the entity jointPermissions attached to a particular role.
* @param Role $role
*/
public function deleteJointPermissionsForRole(Role $role)
{
@@ -318,13 +270,13 @@ class PermissionService
$roleIds = array_map(function ($role) {
return $role->id;
}, $roles);
$this->jointPermission->newQuery()->whereIn('role_id', $roleIds)->delete();
JointPermission::query()->whereIn('role_id', $roleIds)->delete();
}
/**
* Delete the entity jointPermissions for a particular entity.
* @param Entity $entity
* @throws \Throwable
* @throws Throwable
*/
public function deleteJointPermissionsForEntity(Entity $entity)
{
@@ -333,10 +285,10 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param \BookStack\Entities\Entity[] $entities
* @throws \Throwable
* @param Entity[] $entities
* @throws Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
protected function deleteManyJointPermissionsForEntities(array $entities)
{
if (count($entities) === 0) {
return;
@@ -358,19 +310,19 @@ class PermissionService
}
/**
* Create & Save entity jointPermissions for many entities and jointPermissions.
* @param Collection $entities
* @param array $roles
* @throws \Throwable
* Create & Save entity jointPermissions for many entities and roles.
* @param Entity[] $entities
* @param Role[] $roles
* @throws Throwable
*/
protected function createManyJointPermissions($entities, $roles)
protected function createManyJointPermissions(array $entities, array $roles)
{
$this->readyEntityCache($entities);
$jointPermissions = [];
// Fetch Entity Permissions and create a mapping of entity restricted statuses
$entityRestrictedMap = [];
$permissionFetch = $this->entityPermission->newQuery();
$permissionFetch = EntityPermission::query();
foreach ($entities as $entity) {
$entityRestrictedMap[$entity->getMorphClass() . ':' . $entity->id] = boolval($entity->getRawAttribute('restricted'));
$permissionFetch->orWhere(function ($query) use ($entity) {
@@ -414,16 +366,14 @@ class PermissionService
/**
* Get the actions related to an entity.
* @param \BookStack\Entities\Entity $entity
* @return array
*/
protected function getActions(Entity $entity)
protected function getActions(Entity $entity): array
{
$baseActions = ['view', 'update', 'delete'];
if ($entity->isA('chapter') || $entity->isA('book')) {
if ($entity instanceof Chapter || $entity instanceof Book) {
$baseActions[] = 'page-create';
}
if ($entity->isA('book')) {
if ($entity instanceof Book) {
$baseActions[] = 'chapter-create';
}
return $baseActions;
@@ -432,14 +382,8 @@ class PermissionService
/**
* Create entity permission data for an entity and role
* for a particular action.
* @param Entity $entity
* @param Role $role
* @param string $action
* @param array $permissionMap
* @param array $rolePermissionMap
* @return array
*/
protected function createJointPermissionData(Entity $entity, Role $role, $action, $permissionMap, $rolePermissionMap)
protected function createJointPermissionData(Entity $entity, Role $role, string $action, array $permissionMap, array $rolePermissionMap): array
{
$permissionPrefix = (strpos($action, '-') === false ? ($entity->getType() . '-') : '') . $action;
$roleHasPermission = isset($rolePermissionMap[$role->getRawAttribute('id') . ':' . $permissionPrefix . '-all']);
@@ -456,7 +400,7 @@ class PermissionService
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
}
if ($entity->isA('book') || $entity->isA('bookshelf')) {
if ($entity instanceof Book || $entity instanceof Bookshelf) {
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
}
@@ -466,7 +410,7 @@ class PermissionService
$hasPermissiveAccessToParents = !$book->restricted;
// For pages with a chapter, Check if explicit permissions are set on the Chapter
if ($entity->isA('page') && $entity->chapter_id !== 0 && $entity->chapter_id !== '0') {
if ($entity instanceof Page && intval($entity->chapter_id) !== 0) {
$chapter = $this->getChapter($entity->chapter_id);
$hasPermissiveAccessToParents = $hasPermissiveAccessToParents && !$chapter->restricted;
if ($chapter->restricted) {
@@ -485,101 +429,81 @@ class PermissionService
/**
* Check for an active restriction in an entity map.
* @param $entityMap
* @param Entity $entity
* @param Role $role
* @param $action
* @return bool
*/
protected function mapHasActiveRestriction($entityMap, Entity $entity, Role $role, $action)
protected function mapHasActiveRestriction(array $entityMap, Entity $entity, Role $role, string $action): bool
{
$key = $entity->getMorphClass() . ':' . $entity->getRawAttribute('id') . ':' . $role->getRawAttribute('id') . ':' . $action;
return isset($entityMap[$key]) ? $entityMap[$key] : false;
return $entityMap[$key] ?? false;
}
/**
* 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 Role $role
* @param $action
* @param $permissionAll
* @param $permissionOwn
* @return array
*/
protected function createJointPermissionDataArray(Entity $entity, Role $role, $action, $permissionAll, $permissionOwn)
protected function createJointPermissionDataArray(Entity $entity, Role $role, string $action, bool $permissionAll, bool $permissionOwn): array
{
return [
'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entity->getMorphClass(),
'action' => $action,
'has_permission' => $permissionAll,
'role_id' => $role->getRawAttribute('id'),
'entity_id' => $entity->getRawAttribute('id'),
'entity_type' => $entity->getMorphClass(),
'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;
$user = $this->currentUser();
$nonJointPermissions = ['restrictions', 'image', 'attachment', 'comment'];
// Handle non entity specific jointPermissions
if (in_array($explodedPermission[0], $nonJointPermissions)) {
$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;
$allPermission = $user && $user->can($permission . '-all');
$ownPermission = $user && $user->can($permission . '-own');
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$isOwner = $user && $user->id === $ownable->$ownerField;
return ($allPermission || ($isOwner && $ownPermission));
}
// Handle abnormal create jointPermissions
if ($action === 'create') {
$this->currentAction = $permission;
$action = $permission;
}
$q = $this->entityRestrictionQuery($baseQuery)->count() > 0;
$hasAccess = $this->entityRestrictionQuery($baseQuery, $action)->count() > 0;
$this->clean();
return $q;
return $hasAccess;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
public function checkUserHasPermissionOnAnything(string $permission, ?string $entityClass = null): bool
{
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
$permissionQuery = $this->db->table('joint_permissions')
$permissionQuery = JointPermission::query()
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
->where(function ($query) use ($userId) {
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
});
->where(function (Builder $query) use ($userId) {
$this->addJointHasPermissionCheck($query, $userId);
});
if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass);
$entityInstance = app($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
@@ -588,46 +512,22 @@ class PermissionService
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param \BookStack\Entities\Entity $entity
* @param $action
* @return bool|mixed
*/
public function checkIfRestrictionsSet(Entity $entity, $action)
{
$this->currentAction = $action;
if ($entity->isA('page')) {
return $entity->restricted || ($entity->chapter && $entity->chapter->restricted) || $entity->book->restricted;
} elseif ($entity->isA('chapter')) {
return $entity->restricted || $entity->book->restricted;
} elseif ($entity->isA('book')) {
return $entity->restricted;
}
}
/**
* The general query filter to remove all entities
* that the current user does not have access to.
* @param $query
* @return mixed
*/
protected function entityRestrictionQuery($query)
protected function entityRestrictionQuery(Builder $query, string $action): Builder
{
$q = $query->where(function ($parentQuery) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) {
$permissionQuery->whereIn('role_id', $this->getRoles())
->where('action', '=', $this->currentAction)
->where(function ($query) {
$query->where('has_permission', '=', true)
->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
$q = $query->where(function ($parentQuery) use ($action) {
$parentQuery->whereHas('jointPermissions', function ($permissionQuery) use ($action) {
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $action)
->where(function (Builder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
$this->clean();
return $q;
}
@@ -641,14 +541,10 @@ class PermissionService
$this->clean();
return $query->where(function (Builder $parentQuery) use ($ability) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) use ($ability) {
$permissionQuery->whereIn('role_id', $this->getRoles())
$permissionQuery->whereIn('role_id', $this->getCurrentUserRoles())
->where('action', '=', $ability)
->where(function (Builder $query) {
$query->where('has_permission', '=', true)
->orWhere(function (Builder $query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
@@ -658,121 +554,101 @@ class PermissionService
* Extend the given page query to ensure draft items are not visible
* unless created by the given user.
*/
public function enforceDraftVisiblityOnQuery(Builder $query): Builder
public function enforceDraftVisibilityOnQuery(Builder $query): Builder
{
return $query->where(function (Builder $query) {
$query->where('draft', '=', false)
->orWhere(function (Builder $query) {
$query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id);
->where('owned_by', '=', $this->currentUser()->id);
});
});
}
/**
* Add restrictions for a generic entity
* @param string $entityType
* @param Builder|\BookStack\Entities\Entity $query
* @param string $action
* @return Builder
* Add restrictions for a generic entity.
*/
public function enforceEntityRestrictions($entityType, $query, $action = 'view')
public function enforceEntityRestrictions(Entity $entity, Builder $query, string $action = 'view'): Builder
{
if (strtolower($entityType) === 'page') {
if ($entity instanceof Page) {
// Prevent drafts being visible to others.
$query = $query->where(function ($query) {
$query->where('draft', '=', false)
->orWhere(function ($query) {
$query->where('draft', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
});
$this->enforceDraftVisibilityOnQuery($query);
}
$this->currentAction = $action;
return $this->entityRestrictionQuery($query);
return $this->entityRestrictionQuery($query, $action);
}
/**
* Filter items that have entities set as a polymorphic relation.
* @param $query
* @param string $tableName
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return QueryBuilder
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
{
$this->currentAction = $action;
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
$q = $query->where(function ($query) use ($tableDetails, $action) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
->where('action', '=', $action)
->whereIn('role_id', $this->getCurrentUserRoles())
->where(function (QueryBuilder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
$this->clean();
return $q;
}
/**
* Add conditions to a query to filter the selection to related entities
* where permissions are granted.
* @param $entityType
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
* where view permissions are granted.
*/
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
public function filterRelatedEntity(string $entityClass, Builder $query, string $tableName, string $entityIdColumn): Builder
{
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$morphClass = app($entityClass)->getMorphClass();
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
$q = $query->where(function ($query) use ($tableDetails, $morphClass) {
$query->where(function ($query) use (&$tableDetails, $morphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $morphClass) {
$permissionQuery->select('id')->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where('entity_type', '=', $pageMorphClass)
->where('action', '=', $this->currentAction)
->whereIn('role_id', $this->getRoles())
->where(function ($query) {
$query->where('has_permission', '=', true)->orWhere(function ($query) {
$query->where('has_permission_own', '=', true)
->where('created_by', '=', $this->currentUser()->id);
});
->where('entity_type', '=', $morphClass)
->where('action', '=', 'view')
->whereIn('role_id', $this->getCurrentUserRoles())
->where(function (QueryBuilder $query) {
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
return $q;
}
/**
* Get the current user
* @return \BookStack\Auth\User
* Add the query for checking the given user id has permission
* within the join_permissions table.
* @param QueryBuilder|Builder $query
*/
private function currentUser()
protected function addJointHasPermissionCheck($query, int $userIdToCheck)
{
if ($this->currentUserModel === false) {
$query->where('has_permission', '=', true)->orWhere(function ($query) use ($userIdToCheck) {
$query->where('has_permission_own', '=', true)
->where('owned_by', '=', $userIdToCheck);
});
}
/**
* Get the current user
*/
private function currentUser(): User
{
if (is_null($this->currentUserModel)) {
$this->currentUserModel = user();
}
@@ -782,10 +658,9 @@ class PermissionService
/**
* Clean the cached user elements.
*/
private function clean()
private function clean(): void
{
$this->currentUserModel = false;
$this->userRoles = false;
$this->isAdminUser = null;
$this->currentUserModel = null;
$this->userRoles = null;
}
}

View File

@@ -1,10 +1,11 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
class PermissionsRepo
{
@@ -60,6 +61,7 @@ class PermissionsRepo
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionService->buildJointPermissionForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
return $role;
}
@@ -88,12 +90,13 @@ 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.
*/
public function assignRolePermissions(Role $role, array $permissionNameArray = [])
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
@@ -137,6 +140,7 @@ class PermissionsRepo
}
$this->permissionService->deleteJointPermissionsForRole($role);
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}
}

View File

@@ -2,8 +2,10 @@
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;
/**
@@ -14,7 +16,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
* @property string $external_auth_id
* @property string $system_name
*/
class Role extends Model
class Role extends Model implements Loggable
{
protected $fillable = ['display_name', 'description', 'external_auth_id'];
@@ -22,7 +24,7 @@ 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');
}
@@ -38,7 +40,7 @@ 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');
}
@@ -104,4 +106,12 @@ class Role extends Model
{
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,23 +1,30 @@
<?php namespace BookStack\Auth;
use BookStack\Api\ApiToken;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable;
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 $slug
* @property string $email
* @property string $password
* @property Carbon $created_at
@@ -26,8 +33,9 @@ use Illuminate\Notifications\Notifiable;
* @property int $image_id
* @property string $external_auth_id
* @property string $system_name
* @property Collection $roles
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
{
use Authenticatable, CanResetPassword, Notifiable;
@@ -43,6 +51,8 @@ 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
@@ -54,7 +64,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* This holds the user's permissions when loaded.
* @var array
* @var ?Collection
*/
protected $permissions;
@@ -66,23 +76,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Returns the default public user.
* @return User
*/
public static function getDefault()
public static function getDefault(): User
{
if (!is_null(static::$defaultUser)) {
return static::$defaultUser;
}
static::$defaultUser = static::where('system_name', '=', 'public')->first();
static::$defaultUser = static::query()->where('system_name', '=', 'public')->first();
return static::$defaultUser;
}
/**
* Check if the user is the default public user.
* @return bool
*/
public function isDefault()
public function isDefault(): bool
{
return $this->system_name === 'public';
}
@@ -109,12 +117,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasSystemRole($role)
public function hasSystemRole(string $roleSystemName): bool
{
return $this->roles->pluck('system_name')->contains($role);
return $this->roles->pluck('system_name')->contains($roleSystemName);
}
/**
@@ -128,35 +134,44 @@ 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;
}
/**
@@ -169,9 +184,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the social account associated with this user.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function socialAccounts()
public function socialAccounts(): HasMany
{
return $this->hasMany(SocialAccount::class);
}
@@ -192,11 +206,9 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}
/**
* Returns the user's avatar,
* @param int $size
* @return string
* Returns a URL to the user's avatar
*/
public function getAvatar($size = 50)
public function getAvatar(int $size = 50): string
{
$default = url('/user_avatar.png');
$imageId = $this->image_id;
@@ -206,7 +218,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;
@@ -214,9 +226,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the avatar for the user.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function avatar()
public function avatar(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
@@ -229,6 +240,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.
*/
@@ -243,15 +267,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getProfileUrl(): string
{
return url('/user/' . $this->id);
return url('/user/' . $this->slug);
}
/**
* Get a shortened version of the user's name.
* @param int $chars
* @return string
*/
public function getShortName($chars = 8)
public function getShortName(int $chars = 8): string
{
if (mb_strlen($this->name) <= $chars) {
return $this->name;
@@ -274,4 +296,21 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
$this->notify(new ResetPassword($token));
}
/**
* @inheritdoc
*/
public function logDescriptor(): string
{
return "({$this->id}) {$this->name}";
}
/**
* @inheritDoc
*/
public function refreshSlug(): string
{
$this->slug = app(SlugGenerator::class)->generate($this);
return $this->slug;
}
}

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,44 @@ 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 a user by their slug.
*/
public function getBySlug(string $slug): User
{
return User::query()->where('slug', '=', $slug)->firstOrFail();
}
/**
* 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 +98,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 +111,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 +142,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 +160,65 @@ 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'] ?? '',
]);
];
$user = new User();
$user->forceFill($details);
$user->refreshSlug();
$user->save();
return $user;
}
/**
* 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 +259,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('display_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

@@ -19,18 +19,18 @@ return [
// private configuration variables so should remain disabled in public.
'debug' => env('APP_DEBUG', false),
// Set the default view type for various lists. Can be overridden by user preferences.
// These will be used for public viewers and users that have not set a preference.
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list'),
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
],
// The number of revisions to keep in the database.
// Once this limit is reached older revisions will be deleted.
// 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 +45,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 +56,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'bg', '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', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
// Application Fallback Locale
'fallback_locale' => 'en',
@@ -111,12 +115,14 @@ return [
BookStack\Providers\TranslationServiceProvider::class,
// BookStack custom service providers
BookStack\Providers\ThemeServiceProvider::class,
BookStack\Providers\AuthServiceProvider::class,
BookStack\Providers\AppServiceProvider::class,
BookStack\Providers\BroadcastServiceProvider::class,
BookStack\Providers\EventServiceProvider::class,
BookStack\Providers\RouteServiceProvider::class,
BookStack\Providers\CustomFacadeProvider::class,
BookStack\Providers\CustomValidationServiceProvider::class,
],
/*
@@ -178,10 +184,10 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Setting' => BookStack\Facades\Setting::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
'Permissions' => BookStack\Facades\Permissions::class,
'Theme' => BookStack\Facades\Theme::class,
],

View File

@@ -85,6 +85,7 @@ return [
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
'port' => $mysql_port,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',

View File

@@ -38,7 +38,7 @@ return [
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
"DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
@@ -219,7 +219,7 @@ return [
*
* @var bool
*/
"DOMPDF_ENABLE_JAVASCRIPT" => true,
"DOMPDF_ENABLE_JAVASCRIPT" => false,
/**
* Enable remote file access

View File

@@ -11,7 +11,7 @@
return [
// Mail driver to use.
// Options: smtp, mail, sendmail, log
// Options: smtp, sendmail, log, array
'driver' => env('MAIL_DRIVER', 'smtp'),
// SMTP host address

View File

@@ -1,5 +1,7 @@
<?php
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
return [
// Display name, shown to users, for SAML2 option
@@ -139,6 +141,14 @@ return [
// )
// ),
],
'security' => [
// SAML2 Authn context
// When set to false no AuthContext will be sent in the AuthNRequest,
// When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.
// Multiple forced values can be passed via a space separated array, For example:
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
],
],
];

View File

@@ -132,6 +132,7 @@ return [
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
'start_tls' => env('LDAP_START_TLS', false),
],
];

View File

@@ -1,5 +1,7 @@
<?php
use \Illuminate\Support\Str;
/**
* Session configuration options.
*
@@ -57,7 +59,7 @@ return [
// The session cookie path determines the path for which the cookie will
// be regarded as available. Typically, this will be the root path of
// your application but you are free to change this when necessary.
'path' => '/',
'path' => '/' . (explode('/', env('APP_URL', ''), 4)[3] ?? ''),
// Session Cookie Domain
// Here you may change the domain of the cookie used to identify a session
@@ -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

@@ -24,4 +24,12 @@ return [
'app-custom-head' => false,
'registration-enabled' => false,
// User-level default settings
'user' => [
'dark-mode-enabled' => env('APP_DEFAULT_DARK_MODE', false),
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
'bookshelf_view_type' =>env('APP_VIEWS_BOOKSHELF', 'grid'),
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
],
];

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

@@ -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

@@ -4,6 +4,7 @@ namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Connection;
use Illuminate\Support\Facades\DB;
class UpdateUrl extends Command
{
@@ -60,22 +61,50 @@ class UpdateUrl extends Command
"attachments" => ["path"],
"pages" => ["html", "text", "markdown"],
"images" => ["url"],
"settings" => ["value"],
"comments" => ["html", "text"],
];
foreach ($columnsToUpdateByTable as $table => $columns) {
foreach ($columns as $column) {
$changeCount = $this->db->table($table)->update([
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
]);
$changeCount = $this->replaceValueInTable($table, $column, $oldUrl, $newUrl);
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
}
}
$jsonColumnsToUpdateByTable = [
"settings" => ["value"],
];
foreach ($jsonColumnsToUpdateByTable as $table => $columns) {
foreach ($columns as $column) {
$oldJson = trim(json_encode($oldUrl), '"');
$newJson = trim(json_encode($newUrl), '"');
$changeCount = $this->replaceValueInTable($table, $column, $oldJson, $newJson);
$this->info("Updated {$changeCount} JSON encoded rows in {$table}->{$column}");
}
}
$this->info("URL update procedure complete.");
$this->info('============================================================================');
$this->info('Be sure to run "php artisan cache:clear" to clear any old URLs in the cache.');
$this->info('============================================================================');
return 0;
}
/**
* Perform a find+replace operations in the provided table and column.
* Returns the count of rows changed.
*/
protected function replaceValueInTable(string $table, string $column, string $oldUrl, string $newUrl): int
{
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
$newQuoted = $this->db->getPdo()->quote($newUrl);
return $this->db->table($table)->update([
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})")
]);
}
/**
* Warn the user of the dangers of this operation.
* Returns a boolean indicating if they've accepted the warnings.

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,64 +0,0 @@
<?php namespace BookStack\Entities;
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'];
/**
* 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;
}
/**
* 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', 'image_id'];
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,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -10,7 +10,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 +28,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,12 +44,9 @@ 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) {
foreach ($this->pages()->withTrashed()->get() as $page) {
$page->changeBook($newBookId);
}
}

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', 'image_id'];
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,18 @@ 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\Interfaces\Sluggable;
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 +37,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 implements Sluggable
{
use SoftDeletes;
use HasCreatorAndUpdater;
use HasOwner;
/**
* @var string - Name of property where the main text content is found
@@ -50,7 +57,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 +99,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 +127,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 +137,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 +161,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 +170,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,13 +186,20 @@ 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');
}
/**
* Get the related delete records for this entity.
*/
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'
@@ -210,28 +210,12 @@ class Entity extends Ownable
}
/**
* 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);
}
/**
@@ -247,35 +231,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;
}
/**
@@ -292,17 +286,15 @@ class Entity extends Ownable
*/
public function indexForSearch()
{
$searchService = app()->make(SearchService::class);
$searchService->indexEntity(clone $this);
app(SearchIndex::class)->indexEntity(clone $this);
}
/**
* Generate and set a new URL slug for this model.
* @inheritdoc
*/
public function refreshSlug(): string
{
$generator = new SlugGenerator($this);
$this->slug = $generator->generate();
$this->slug = app(SlugGenerator::class)->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;
@@ -27,14 +28,19 @@ class Page extends BookChild
public $textField = 'text';
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
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);
$query = Permissions::enforceDraftVisibilityOnQuery($query);
return parent::scopeVisible($query);
}
@@ -49,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
@@ -94,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));
}
/**
@@ -120,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)
{
@@ -29,7 +30,7 @@ class BookshelfRepo
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
{
return Bookshelf::visible()
->with('visibleBooks')
->with(['visibleBooks', 'cover'])
->orderBy($sort, $order)
->paginate($count);
}
@@ -87,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
{
@@ -101,6 +103,7 @@ class BookshelfRepo
$this->updateBooks($shelf, $bookIds);
}
Activity::addForEntity($shelf, ActivityType::BOOKSHELF_UPDATE);
return $shelf;
}
@@ -134,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.
*/
@@ -174,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,47 +177,52 @@ 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');
}
$pageContent = new PageContent($page);
$pageContent->setNewHTML($input['html']);
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
// Update with new details
$page->revision_count++;
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->save();
// Remove all update drafts for this user & page.
$this->getUserDraftQuery($page)->delete();
// Save a revision after updating
$summary = $input['summary'] ?? null;
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
$summary = trim($input['summary'] ?? "");
$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) {
$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->getAttributes());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -237,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;
}
@@ -259,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();
}
/**
@@ -273,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($revision->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;
}
@@ -294,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) {
@@ -309,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;
}
/**
@@ -320,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');
}
@@ -368,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.
*/
@@ -439,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;
use Illuminate\Support\Str;
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 = 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): 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)
{

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) {

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,9 +1,17 @@
<?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 BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Util\HtmlContentFilter;
use DOMDocument;
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
{
@@ -25,6 +33,32 @@ 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());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
return $converter->convertToHtml($markdown);
}
/**
@@ -136,7 +170,7 @@ class PageContent
$content = $this->page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
$content = HtmlContentFilter::removeScripts($content);
}
if ($blankIncludes) {
@@ -275,65 +309,4 @@ class PageContent
return $innerContent;
}
/**
* Escape script tags within HTML content.
*/
protected function escapeScripts(string $html) : string
{
if (empty($html)) {
return $html;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//script');
foreach ($scriptElems as $scriptElem) {
$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) {
$badIframe->parentNode->removeChild($badIframe);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
}
}

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

@@ -1,4 +1,4 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Tools;
use Illuminate\Http\Request;
@@ -137,5 +137,4 @@ class SearchOptions
return $string;
}
}
}

View File

@@ -1,6 +1,9 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Tools;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Auth\User;
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 +11,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,25 +36,14 @@ class SearchService
*/
protected $queryOperators = ['<=', '>=', '=', '<', '>', 'like', '!='];
/**
* SearchService constructor.
*/
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
*/
public function setConnection(Connection $connection)
{
$this->db = $connection;
}
/**
* Search all entities in the system.
* The provided count is for each entity to search,
@@ -115,11 +103,12 @@ class SearchService
$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
* Search a chapter for entities
*/
public function searchChapter(int $chapterId, string $searchString): Collection
{
@@ -134,7 +123,7 @@ class SearchService
* matching instead of the items themselves.
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $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($searchOpts, $entityType, $action);
if ($getCount) {
@@ -155,28 +144,25 @@ class SearchService
// Handle normal search terms
if (count($searchOpts->searches) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$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 ($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($searchOpts->exacts) > 0) {
$entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) {
foreach ($searchOpts->exacts 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 .'%');
});
}
@@ -193,7 +179,7 @@ class SearchService
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
return $this->permissionService->enforceEntityRestrictions($entity, $entitySelect, $action);
}
/**
@@ -239,102 +225,6 @@ class SearchService
return $query;
}
/**
* 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 \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
*/
@@ -381,24 +271,29 @@ class SearchService
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') {
return;
$userSlug = $input === 'me' ? user()->slug : trim($input);
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
if ($user) {
$query->where('created_by', '=', $user->id);
}
if ($input === 'me') {
$input = user()->id;
}
$query->where('created_by', '=', $input);
}
protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
{
if (!is_numeric($input) && $input !== 'me') {
return;
$userSlug = $input === 'me' ? user()->slug : trim($input);
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
if ($user) {
$query->where('updated_by', '=', $user->id);
}
if ($input === 'me') {
$input = user()->id;
}
protected function filterOwnedBy(EloquentBuilder $query, Entity $model, $input)
{
$userSlug = $input === 'me' ? user()->slug : trim($input);
$user = User::query()->where('slug', '=', $userSlug)->first(['id']);
if ($user) {
$query->where('owned_by', '=', $user->id);
}
$query->where('updated_by', '=', $input);
}
protected function filterInName(EloquentBuilder $query, Entity $model, $input)

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,53 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Interfaces\Sluggable;
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(Sluggable $model): string
{
$slug = $this->formatNameAsSlug($model->name);
while ($this->slugInUse($slug, $model)) {
$slug .= '-' . Str::random(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, Sluggable $model): bool
{
$query = $model->newQuery()->where('slug', '=', $slug);
if ($model instanceof BookChild) {
$query->where('book_id', '=', $model->book_id);
}
if ($model->id) {
$query->where('id', '!=', $model->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

@@ -2,6 +2,7 @@
namespace BookStack\Exceptions;
class ApiAuthException extends UnauthorizedException {
class ApiAuthException extends UnauthorizedException
{
}
}

View File

@@ -3,49 +3,52 @@
namespace BookStack\Exceptions;
use Exception;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that should not be reported.
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
AuthorizationException::class,
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
NotFoundException::class,
];
/**
* Report or log an exception.
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param Exception $exception
* @return void
*
* @param \Exception $e
* @return mixed
* @throws Exception
*/
public function report(Exception $e)
public function report(Exception $exception)
{
return parent::report($e);
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Exception $e
* @param Exception $e
* @return \Illuminate\Http\Response
*/
public function render($request, Exception $e)
@@ -54,29 +57,6 @@ class Handler extends ExceptionHandler
return $this->renderApiException($e);
}
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($this->isExceptionType($e, NotifyException::class)) {
$message = $this->getOriginalMessage($e);
if (!empty($message)) {
session()->flash('error', $message);
}
return redirect($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
$message = $this->getOriginalMessage($e);
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
}
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
return \Route::respondWithRoute('fallback');
}
return parent::render($request, $e);
}
@@ -115,35 +95,6 @@ class Handler extends ExceptionHandler
return new JsonResponse($responseData, $code, $headers);
}
/**
* Check the exception chain to compare against the original exception type.
* @param Exception $e
* @param $type
* @return bool
*/
protected function isExceptionType(Exception $e, $type)
{
do {
if (is_a($e, $type)) {
return true;
}
} while ($e = $e->getPrevious());
return false;
}
/**
* Get original exception message.
* @param Exception $e
* @return string
*/
protected function getOriginalMessage(Exception $e)
{
do {
$message = $e->getMessage();
} while ($e = $e->getPrevious());
return $message;
}
/**
* Convert an authentication exception into an unauthenticated response.
*

View File

@@ -22,4 +22,4 @@ class JsonDebugException extends Exception
{
return response()->json($this->data);
}
}
}

View File

@@ -5,7 +5,6 @@ class NotFoundException extends PrettyException
/**
* NotFoundException constructor.
* @param string $message
*/
public function __construct($message = 'Item not found')
{

View File

@@ -1,8 +1,10 @@
<?php namespace BookStack\Exceptions;
class NotifyException extends \Exception
{
use Exception;
use Illuminate\Contracts\Support\Responsable;
class NotifyException extends Exception implements Responsable
{
public $message;
public $redirectLocation;
@@ -15,4 +17,19 @@ class NotifyException extends \Exception
$this->redirectLocation = $redirectLocation;
parent::__construct();
}
/**
* Send the response for this type of exception.
* @inheritdoc
*/
public function toResponse($request)
{
$message = $this->getMessage();
if (!empty($message)) {
session()->flash('error', $message);
}
return redirect($this->redirectLocation);
}
}

View File

@@ -1,6 +1,43 @@
<?php namespace BookStack\Exceptions;
class PrettyException extends \Exception
{
use Exception;
use Illuminate\Contracts\Support\Responsable;
class PrettyException extends Exception implements Responsable
{
/**
* @var ?string
*/
protected $subtitle = null;
/**
* @var ?string
*/
protected $details = null;
/**
* Render a response for when this exception occurs.
* @inheritdoc
*/
public function toResponse($request)
{
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
return response()->view('errors.' . $code, [
'message' => $this->getMessage(),
'subtitle' => $this->subtitle,
'details' => $this->details,
], $code);
}
public function setSubtitle(string $subtitle): self
{
$this->subtitle = $subtitle;
return $this;
}
public function setDetails(string $details): self
{
$this->details = $details;
return $this;
}
}

View File

@@ -14,4 +14,4 @@ class UnauthorizedException extends Exception
{
parent::__construct($message, $code);
}
}
}

View File

@@ -1,8 +1,9 @@
<?php namespace BookStack\Facades;
use BookStack\Theming\ThemeService;
use Illuminate\Support\Facades\Facade;
class Setting extends Facade
class Theme extends Facade
{
/**
* Get the registered name of the component.
@@ -11,6 +12,6 @@ class Setting extends Facade
*/
protected static function getFacadeAccessor()
{
return 'setting';
return ThemeService::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 = [];
@@ -27,4 +27,4 @@ class ApiController extends Controller
{
return $this->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,9 @@ 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,9 +1,8 @@
<?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;
@@ -26,9 +25,6 @@ class BookApiController extends ApiController
],
];
/**
* BooksApiController constructor.
*/
public function __construct(BookRepo $bookRepo)
{
$this->bookRepo = $bookRepo;
@@ -41,7 +37,7 @@ class BookApiController 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',
]);
}
@@ -55,8 +51,6 @@ class BookApiController 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);
}
@@ -65,7 +59,7 @@ class BookApiController 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);
}
@@ -80,15 +74,14 @@ class BookApiController 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)
{
@@ -96,8 +89,6 @@ class BookApiController extends ApiController
$this->checkOwnablePermission('book-delete', $book);
$this->bookRepo->destroy($book);
Activity::addMessage('book_delete', $book->name);
return response('', 204);
}
}
}

View File

@@ -1,23 +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 BookExportApiController extends ApiController
{
protected $bookRepo;
protected $exportService;
protected $exportFormatter;
/**
* 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;
}
/**
@@ -27,7 +20,7 @@ class BookExportApiController 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');
}
@@ -38,7 +31,7 @@ class BookExportApiController 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');
}
@@ -48,7 +41,7 @@ class BookExportApiController 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

@@ -1,7 +1,8 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
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;
@@ -42,7 +43,7 @@ class ChapterApiController extends ApiController
$chapters = Chapter::visible();
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
'created_at', 'updated_at', 'created_by', 'updated_by',
'created_at', 'updated_at', 'created_by', 'updated_by', 'owned_by',
]);
}
@@ -58,8 +59,6 @@ class ChapterApiController extends ApiController
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
Activity::add($chapter, 'chapter_create', $book->id);
return response()->json($chapter->load(['tags']));
}
@@ -68,7 +67,7 @@ class ChapterApiController extends ApiController
*/
public function read(string $id)
{
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'ownedBy', 'pages' => function (HasMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}])->findOrFail($id);
return response()->json($chapter);
@@ -83,13 +82,12 @@ class ChapterApiController extends ApiController
$this->checkOwnablePermission('chapter-update', $chapter);
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return response()->json($updatedChapter->load(['tags']));
}
/**
* Delete a chapter from the system.
* Delete a chapter.
* This will typically send the chapter to the recycle bin.
*/
public function delete(string $id)
{
@@ -97,8 +95,6 @@ class ChapterApiController extends ApiController
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
return response('', 204);
}
}

View File

@@ -1,23 +1,20 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Chapter;
use BookStack\Entities\ExportService;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Tools\ExportFormatter;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
class ChapterExportApiController extends ApiController
{
protected $chapterRepo;
protected $exportService;
protected $exportFormatter;
/**
* ChapterExportController constructor.
*/
public function __construct(BookRepo $chapterRepo, ExportService $exportService)
public function __construct(ExportFormatter $exportFormatter)
{
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
parent::__construct();
$this->exportFormatter = $exportFormatter;
}
/**
@@ -27,7 +24,7 @@ class ChapterExportApiController extends ApiController
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$pdfContent = $this->exportService->chapterToPdf($chapter);
$pdfContent = $this->exportFormatter->chapterToPdf($chapter);
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
}
@@ -38,7 +35,7 @@ class ChapterExportApiController extends ApiController
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$htmlContent = $this->exportService->chapterToContainedHtml($chapter);
$htmlContent = $this->exportFormatter->chapterToContainedHtml($chapter);
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
}
@@ -48,7 +45,7 @@ class ChapterExportApiController extends ApiController
public function exportPlainText(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$textContent = $this->exportService->chapterToPlainText($chapter);
$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

@@ -25,7 +25,6 @@ class AttachmentController extends Controller
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo;
parent::__construct();
}

View File

@@ -20,14 +20,23 @@ class AuditLogController extends Controller
'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'user' => $request->get('user', ''),
];
$query = Activity::query()
->with(['entity', 'user'])
->with([
'entity' => function ($query) {
$query->withTrashed();
},
'user'
])
->orderBy($listDetails['sort'], $listDetails['order']);
if ($listDetails['event']) {
$query->where('key', '=', $listDetails['event']);
$query->where('type', '=', $listDetails['event']);
}
if ($listDetails['user']) {
$query->where('user_id', '=', $listDetails['user']);
}
if ($listDetails['date_from']) {
@@ -40,12 +49,12 @@ class AuditLogController extends Controller
$activities = $query->paginate(100);
$activities->appends($listDetails);
$keys = DB::table('activities')->select('key')->distinct()->pluck('key');
$types = DB::table('activities')->select('type')->distinct()->pluck('type');
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
'listDetails' => $listDetails,
'activityKeys' => $keys,
'activityTypes' => $types,
]);
}
}

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