Compare commits

..

516 Commits

Author SHA1 Message Date
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
fdabafffda Added thumbnail attribute to complete .env 2021-05-30 15:22:58 +01:00
Dan Brown
e2df15fe20 Updated translators before next release 2021-05-30 15:11:59 +01:00
Dan Brown
54bac17ef0 New Crowdin updates (#2764) 2021-05-30 15:10:11 +01:00
Dan Brown
7634ac4e12 Updated test to align with export date format change 2021-05-30 13:23:51 +01:00
Dan Brown
c4f5ab12cf Aligned export and revision shown date format
As raised in #2771
2021-05-30 00:02:32 +01:00
Dan Brown
57a063cdfb Updated nav tests to look for shortened item names 2021-05-29 23:46:33 +01:00
Dan Brown
1fa90e4f12 Converted another couple of tests from browserkit 2021-05-29 23:42:21 +01:00
Dan Brown
d62cdd58d3 Upgraded php and npm deps
- Sass upgrade had some breaking changes where division was used
hence updated for newer sass version support.
2021-05-29 13:08:28 +01:00
Dan Brown
ed6ec341df Added testing to cover next/previous navigation
For #2511
2021-05-29 12:49:10 +01:00
Dan Brown
0cfff6ab6f Reviewed and refactored next/previous navigation button implementation
- Updated styling to include item name.
- Extracted used text to translations.
- Updated the design to better suit the surrounding blocks.
- Removed newly added model/repo methods.
- Moved core logic out of controller and instead into a "NextPreviousContentLocator"
helper with re-uses the output from the book-tree generation.
- Also added the system to chapters.

For #2511
2021-05-29 12:39:41 +01:00
Dan Brown
7ca66c5d5e Merge branch 'prev-next-button' of https://github.com/shubhamosmosys/BookStack into shubhamosmosys-prev-next-button 2021-05-26 22:13:19 +01:00
Dan Brown
9cbea1eb08 Updated drawing upload error to shown/handle server limit errors
Closes #2740
2021-05-26 18:23:27 +01:00
Dan Brown
1a2d374f24 Revert "Added app logo to outgoing emails"
This reverts commit e32929029b.
2021-05-26 17:13:59 +01:00
Dan Brown
e32929029b Added app logo to outgoing emails
Required changing the header bar of the email to be solid color to match
the configuration of the main app header since otherwise colors may not
work together.

Closes #2577
2021-05-26 17:11:03 +01:00
Dan Brown
eb76e882c5 Added deletion of revisions on page delete
Added testing to cover.
Closes #2668
2021-05-26 16:40:56 +01:00
Dan Brown
d326417edc Added name input autofocus on shelves, books and chapters
Closes #1956
2021-05-26 15:25:23 +01:00
Dan Brown
a3a8fef6b2 Made users header interface more adaptable
Search input was stacking on create button on default desktop view
due when viewing in russian due to combined width exceeding container.
Made into normal flexbox instead.

Closes #2147
2021-05-26 15:20:35 +01:00
Dan Brown
0c16334426 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-05-25 00:06:13 +01:00
Dan Brown
600f8cd142 Added origin verification to postMessage usage.
Closes #2769
2021-05-25 00:05:20 +01:00
Dan Brown
5c8c85a0ff Merge pull request #2768 from CorruptComputer/RSPEC-5148-Fixes
[sec] Fixes a few minor vulnerabilies when using target="_blank" on links (RSPEC-5148)
2021-05-24 22:11:49 +01:00
Nickolas Gupton
7a6f21648a Fixes minor vulnerability when using target="_blank" on links (RSPEC-5148) 2021-05-24 16:17:08 -04:00
Dan Brown
df0e03cd07 Reviewed PR to add import user avatars va LDAP
- Reduced options to single new configuration paramter instead of two.
- Moved more logic into UserAvatars class.
- Updated LDAP avatar import to also run on login when no image is
  currently set.
- Added thumbnail fetching to search requests.
- Added testing to cover.

Related to PR #2320, and issue #1161
2021-05-24 18:54:08 +01:00
Dan Brown
85db812fea Merge branch 'master' of https://github.com/jasonhoule/BookStack into jasonhoule-master 2021-05-24 17:06:50 +01:00
Dan Brown
fb5b5e138d Updated existing tag tests away from browserkit testing 2021-05-24 16:16:58 +01:00
Dan Brown
3eaf03a7ac Reviewed tag in seach work
- Refactored some tag code bits while reviewing.
- Updated tag design in search listing to be more subtle.
- Moved tags out of entity-list-item-basic template and instead moved
  them into entity-list-item, below the existing content.
- Tweaked existing tag colors a little.
- Changed tag icon to be more tag-like.
- Added tag-on-search test case.

Review of #2487, Related to #2462
2021-05-24 16:12:09 +01:00
Dan Brown
5420f3451c Merge branch 'show-tags' of https://github.com/burnoutberni/BookStack into burnoutberni-show-tags 2021-05-24 15:12:45 +01:00
Dan Brown
7d94da10fb Merge branch 'v21.04.x' 2021-05-24 13:08:51 +01:00
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
dd6076049c Merge pull request #2748 from BookStackApp/favourite_system
Favourite System
2021-05-23 14:45:42 +01:00
Dan Brown
ba8ba5c634 Added testing to favourite system
- Also removed some old view service references.
- Updated TopFavourites query to be based on favourites table and join
  in the views instead of the other way around, so that favourites still
show even if they have no views.
2021-05-23 14:34:36 +01:00
Dan Brown
c2069f37cc Added deletion of favourites on entity/user delete 2021-05-23 13:41:56 +01:00
Dan Brown
1e0aa7ee2c Added favourites page with link from header and home 2021-05-23 13:34:08 +01:00
Dan Brown
27942f5ce8 Deleted redundant complex relationmultimodel query class 2021-05-22 14:07:57 +01:00
Dan Brown
d0ff79ea60 Revamped some complex queries, added favourites to home
- Removed old view system and started use of new query classes instead.
- Finished off RelationMultiModelQuery but found it was less efficient
than x-many queries due to the amount of tables being scanned.
Adding now for history but will delete as not used.
- Updated recently viewed to use same query system as popular items
  rather than running and joining x-entities queries.
- Added "Most Viewed Faviourites" listing to homepages.
2021-05-22 14:05:28 +01:00
Dan Brown
3de02566bf Started building system for cross-model queries 2021-05-19 23:37:23 +01:00
Dan Brown
93fd869ba3 Started refactoring of view service
Phasing out the view service from being a generic 'service' class,
moving the core create/delete methods into the model.
The idea is that the existing query work will need to interlink
with the favourite system so maybe we have a (or many composable)
query building classes rather than mixing query building and
create/delete work as per the old service.
2021-05-16 10:49:37 +01:00
Dan Brown
3ca149137e Added faviourtes to other entity types 2021-05-16 10:26:28 +01:00
Dan Brown
db9aa41096 Started writing testing for favourites 2021-05-16 01:07:20 +01:00
Dan Brown
bf8e7f3393 Started addition of favourite system 2021-05-16 00:29:56 +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
Shubham Tiwari
99c42033b1 Add prev and next button to navigate through different pages 2021-01-27 10:15:28 +05:30
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
Bernhard Hayden
aad2ee675c Show tags of all search results 2021-01-15 15:52:03 +01: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
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
2acef3c2ec Fixed issue where restricted page content in plaintext export
The content of pages made non-viewable to a user via permissions, within a visible parent, could be seen via the plaintext export option. Before v0.30.6 this would have applied only to scenarios where all pages within the chapter were made non-visible. In v0.30.6 this would make all pages within the chapter visible.

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

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

Added an extra test to cover.

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

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

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

* New translations settings.php (Russian)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Simplified)

* New translations entities.php (Czech)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations settings.php (Czech)

* New translations errors.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (Czech)

* New translations settings.php (German)

* New translations settings.php (German)

* New translations entities.php (German)

* New translations validation.php (Czech)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (German Informal)

* New translations settings.php (German Informal)

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

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

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

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

* New translations entities.php (Persian)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Thai)

* New translations errors.php (German Informal)

* New translations entities.php (Spanish)

* New translations entities.php (French)

* New translations entities.php (Arabic)

* New translations entities.php (Arabic)

* New translations components.php (Portuguese, Brazilian)

* New translations entities.php (Portuguese, Brazilian)

* New translations auth.php (Italian)

* New translations common.php (Italian)

* New translations components.php (Italian)

* New translations entities.php (Italian)

* New translations settings.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Spanish)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Ukrainian)

* New translations components.php (Portuguese)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations common.php (Slovenian)

* New translations components.php (Slovenian)

* New translations common.php (Swedish)

* New translations components.php (Swedish)

* New translations common.php (Turkish)

* New translations components.php (Turkish)

* New translations components.php (Ukrainian)

* New translations components.php (Polish)

* New translations common.php (Chinese Simplified)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations common.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Persian)

* New translations components.php (Persian)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations common.php (Thai)

* New translations components.php (Thai)

* New translations common.php (Portuguese)

* New translations common.php (Polish)

* New translations common.php (Italian)

* New translations common.php (Bulgarian)

* New translations components.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations common.php (Arabic)

* New translations components.php (Arabic)

* New translations components.php (Bulgarian)

* New translations common.php (Dutch)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations common.php (Danish)

* New translations components.php (Danish)

* New translations common.php (German)

* New translations common.php (Hebrew)

* New translations components.php (Hebrew)

* New translations common.php (Hungarian)

* New translations components.php (Hungarian)

* New translations common.php (Japanese)

* New translations common.php (Korean)

* New translations components.php (Korean)

* New translations common.php (German Informal)

* New translations components.php (German)

* New translations common.php (German)

* New translations entities.php (German)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations components.php (Chinese Simplified)

* New translations common.php (Chinese Simplified)

* New translations common.php (Polish)

* New translations components.php (Polish)

* New translations auth.php (Polish)

* New translations entities.php (Polish)

* New translations errors.php (Polish)

* New translations passwords.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations auth.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations passwords.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations entities.php (German)

* New translations components.php (German Informal)

* New translations common.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (Italian)

* New translations settings.php (Dutch)

* New translations settings.php (Thai)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Korean)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Japanese)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (French)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations settings.php (Spanish)

* New translations settings.php (French)

* New translations components.php (Turkish)

* New translations settings.php (Turkish)

* New translations entities.php (Turkish)

* New translations common.php (Turkish)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Chinese Simplified)

* New translations activities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations passwords.php (Chinese Traditional)

* New translations settings.php (German)

* New translations settings.php (German Informal)

* New translations activities.php (Slovak)

* New translations auth.php (Slovak)

* New translations auth.php (Slovak)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations components.php (Slovak)

* New translations entities.php (Slovak)

* New translations common.php (Slovak)

* New translations entities.php (Slovak)

* New translations passwords.php (Slovak)

* New translations settings.php (Dutch)

* New translations components.php (Dutch)

* New translations entities.php (Dutch)

* New translations passwords.php (Dutch)

* New translations activities.php (Arabic)

* New translations entities.php (French)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations activities.php (Japanese)

* New translations auth.php (Japanese)

* New translations entities.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations entities.php (Russian)

* New translations entities.php (Portuguese)

* New translations entities.php (Thai)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Persian)

* New translations entities.php (Portuguese, Brazilian)

* New translations entities.php (Vietnamese)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Simplified)

* New translations entities.php (Ukrainian)

* New translations entities.php (Turkish)

* New translations entities.php (Swedish)

* New translations entities.php (Slovenian)

* New translations entities.php (Slovak)

* New translations entities.php (Polish)

* New translations entities.php (French)

* New translations entities.php (Dutch)

* New translations entities.php (Korean)

* New translations entities.php (Japanese)

* New translations entities.php (Italian)

* New translations entities.php (Hungarian)

* New translations entities.php (Hebrew)

* New translations entities.php (German)

* New translations entities.php (Danish)

* New translations entities.php (Czech)

* New translations entities.php (Bulgarian)

* New translations entities.php (Arabic)

* New translations entities.php (Spanish)

* New translations entities.php (German Informal)

* New translations entities.php (French)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Thai)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Polish)

* New translations settings.php (French)

* New translations settings.php (Dutch)

* New translations settings.php (Korean)

* New translations settings.php (Japanese)

* New translations settings.php (Italian)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations entities.php (Spanish)

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

For #2111
2020-05-12 22:21:45 +01:00
benrubson
58df3ad956 Log failed accesses option 2020-05-03 16:20:02 +02:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
d3ec38bee3 Removed unused function in registration service 2020-05-02 01:07:30 +01:00
Dan Brown
413cac23ae Added command to regenerate comment content 2020-05-01 23:41:47 +01:00
Dan Brown
3c26e7b727 Updated comment md rendering to be server-side 2020-05-01 23:24:11 +01:00
Dan Brown
2a2d0aa15b Fixed incorrect color code causing yellow/orange code blocks 2020-04-29 18:28:26 +01:00
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
benrubson
12a9a45747 Log failed accesses 2020-02-09 10:01:33 +01:00
jakob
6acd958927 Add the "Create Shelf" resp. "Create Book" to the home view 2019-10-30 11:42:37 +01:00
819 changed files with 31529 additions and 17204 deletions

View File

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

View File

@@ -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,10 +195,12 @@ 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
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_THUMBNAIL_ATTRIBUTE=null
LDAP_FOLLOW_REFERRALS=true
LDAP_DUMP_USER_DETAILS=false
@@ -221,6 +223,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/
@@ -238,23 +241,36 @@ DISABLE_EXTERNAL_SERVICES=false
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable draw.io integration
# Enable diagrams.net integration
# Can simply be true/false to enable/disable the integration.
# Alternatively, It can be URL to the draw.io instance you want to use.
# Alternatively, It can be URL to the diagrams.net instance you want to use.
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
DRAWIO=true
# 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,9 +281,23 @@ ALLOW_CONTENT_SCRIPTS=false
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null
# A list of hosts that BookStack can be iframed within.
# Space separated if multiple. BookStack host domain is auto-inferred.
# For Example: ALLOWED_IFRAME_HOSTS="https://example.com https://a.example.com"
# Setting this option will also auto-adjust cookies to be SameSite=None.
ALLOWED_IFRAME_HOSTS=null
# The default and maximum item-counts for listing API requests.
API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
# The number of API requests that can be made per minute by a single user.
API_REQUESTS_PER_MIN=180
API_REQUESTS_PER_MIN=180
# Enable the logging of failed email+password logins with the given message.
# The default log channel below uses the php 'error_log' function which commonly
# results in messages being output to the webserver error logs.
# The message can contain a %u parameter which will be replaced with the login
# user identifier (Username or email).
LOG_FAILED_LOGIN_MESSAGE=false
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver

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

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

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

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

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

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

@@ -0,0 +1,65 @@
name: test-migrations
on:
push:
branches:
- master
- release
- gh_actions_update
pull_request:
branches:
- '*'
- '*/*'
- '!l10n_master'
jobs:
build:
runs-on: ubuntu-20.04
strategy:
matrix:
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: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- name: Cache composer packages
uses: actions/cache@v1
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}
- name: Start MySQL
run: |
sudo /etc/init.d/mysql start
- name: Create database & user
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'
mysql -uroot -proot -e "CREATE USER 'bookstack-test'@'localhost' IDENTIFIED 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;'
- name: Install composer dependencies
run: composer install --prefer-dist --no-interaction --ansi
- name: Start migration test
run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing
- name: Start migration:rollback test
run: |
php${{ matrix.php }} artisan migrate:rollback --force -n --database=mysql_testing
- name: Start migration rerun test
run: |
php${{ matrix.php }} artisan migrate --force -n --database=mysql_testing

View File

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

View File

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

View File

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

View File

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

17
app/Actions/Favourite.php Normal file
View File

@@ -0,0 +1,17 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
protected $fillable = ['user_id'];
/**
* Get the related model that can be favourited.
*/
public function favouritable(): MorphTo
{
return $this->morphTo();
}
}

View File

@@ -1,21 +1,34 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class Attribute
* @package BookStack
*/
class Tag extends Model
{
protected $fillable = ['name', 'value', 'order'];
protected $hidden = ['id', 'entity_id', 'entity_type', 'created_at', 'updated_at'];
/**
* Get the entity that this tag belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
public function entity(): MorphTo
{
return $this->morphTo('entity');
}
/**
* Get a full URL to start a tag name search for this tag name.
*/
public function nameUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
}
/**
* Get a full URL to start a tag name and value search for this tag's values.
*/
public function valueUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
}
}

View File

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

View File

@@ -1,7 +1,19 @@
<?php namespace BookStack\Actions;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class View
* Views are stored per-item per-person within the database.
* They can be used to find popular items or recently viewed items
* at a per-person level. They do not record every view instance as an
* activity. Only the latest and original view times could be recognised.
*
* @property int $views
* @property int $user_id
*/
class View extends Model
{
@@ -9,10 +21,37 @@ class View extends Model
/**
* Get all owning viewable models.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function viewable()
public function viewable(): MorphTo
{
return $this->morphTo();
}
/**
* Increment the current user's view count for the given viewable model.
*/
public static function incrementFor(Viewable $viewable): int
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return 0;
}
/** @var View $view */
$view = $viewable->views()->firstOrNew([
'user_id' => $user->id,
], ['views' => 0]);
$view->forceFill(['views' => $view->views + 1])->save();
return $view->views;
}
/**
* Clear all views from the system.
*/
public static function clearAll()
{
static::query()->truncate();
}
}

View File

@@ -1,114 +0,0 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Book;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
use DB;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param View $view
* @param PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
* Add a view to the given entity.
* @param \BookStack\Entities\Entity $entity
* @return int
*/
public function add(Entity $entity)
{
$user = user();
if ($user === null || $user->isDefault()) {
return 0;
}
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
$view->increment('views');
return $view->views;
}
// Otherwise create new view count
$entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return Collection
*/
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, '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');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
}
/**
* 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)
{
$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());
}
$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;
}
/**
* Reset all view counts by deleting all views.
*/
public function resetAll()
{
$this->view->truncate();
}
}

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

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

View File

@@ -15,8 +15,6 @@ use Illuminate\Contracts\Session\Session;
* guard with 'remember' functionality removed. Basic auth and event emission
* has also been removed to keep this simple. Designed to be extended by external
* Auth Guards.
*
* @package Illuminate\Auth
*/
class ExternalBaseSessionGuard implements StatefulGuard
{
@@ -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);
}
@@ -92,6 +90,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
$this->ldapService->syncGroups($user, $username);
}
// Attach avatar if non-existent
if (is_null($user->avatar)) {
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
}
$this->login($user, $remember);
return true;
}
@@ -117,7 +120,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
'password' => Str::random(32),
];
return $this->registrationService->registerUser($details, null, false);
$user = $this->registrationService->registerUser($details, null, false);
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
return $user;
}
}

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

@@ -3,7 +3,9 @@
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Uploads\UserAvatars;
use ErrorException;
use Illuminate\Support\Facades\Log;
/**
* Class LdapService
@@ -14,15 +16,17 @@ class LdapService extends ExternalAuthService
protected $ldap;
protected $ldapConnection;
protected $userAvatars;
protected $config;
protected $enabled;
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}
@@ -76,19 +80,23 @@ class LdapService extends ExternalAuthService
$idAttr = $this->config['id_attribute'];
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$thumbnailAttr = $this->config['thumbnail_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
$user = $this->getUserWithAttributes($userName, array_filter([
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
]));
if ($user === null) {
if (is_null($user)) {
return null;
}
$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),
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
];
if ($this->config['dump_user_details']) {
@@ -187,8 +195,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 +213,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;
}
@@ -342,4 +358,22 @@ class LdapService extends ExternalAuthService
$userLdapGroups = $this->getUserGroups($username);
$this->syncWithGroups($user, $userLdapGroups);
}
/**
* Save and attach an avatar image, if found in the ldap details, and attach
* to the given user model.
*/
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
{
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
return;
}
try {
$imageData = $ldapUserDetails['avatar'];
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
} catch (\Exception $exception) {
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
}
}
}

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
@@ -57,7 +61,7 @@ class RegistrationService
// Ensure user does not already exist
$alreadyUser = !is_null($this->userRepo->getByEmail($userEmail));
if ($alreadyUser) {
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]));
throw new UserRegistrationException(trans('errors.error_user_exists_different_creds', ['email' => $userEmail]), '/login');
}
// Create the user
@@ -68,18 +72,20 @@ 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();
$message = '';
try {
$this->emailConfirmationService->sendConfirmation($newUser);
session()->flash('sent-email-confirmation', true);
} catch (Exception $e) {
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
throw new UserRegistrationException($message, '/register/confirm');
}
return $newUser;
@@ -105,14 +111,4 @@ class RegistrationService
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
}
}
/**
* Alias to the UserRepo method of the same name.
* Attaches the default system role, if configured, to the given user.
*/
public function attachDefaultRole(User $user): void
{
$this->userRepo->attachDefaultRole($user);
}
}
}

View File

@@ -1,9 +1,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;
@@ -311,7 +315,6 @@ class Saml2Service extends ExternalAuthService
/**
* Get the user from the database for the specified details.
* @throws SamlException
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User
@@ -373,6 +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,27 +1,28 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Entity;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class JointPermission extends Model
{
protected $primaryKey = null;
public $timestamps = false;
/**
* Get the role that this points to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function role()
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
* @return \Illuminate\Database\Eloquent\Relations\MorphOne
*/
public function entity()
public function entity(): MorphOne
{
return $this->morphOne(Entity::class, 'entity');
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,23 +1,31 @@
<?php namespace BookStack\Auth;
use BookStack\Actions\Favourite;
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 +34,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,18 +52,20 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $fillable = ['name', 'email'];
protected $casts = ['last_activity_at' => 'datetime'];
/**
* The attributes excluded from the model's JSON form.
* @var array
*/
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at',
'created_at', 'updated_at', 'image_id',
];
/**
* This holds the user's permissions when loaded.
* @var array
* @var ?Collection
*/
protected $permissions;
@@ -66,23 +77,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';
}
@@ -101,22 +110,18 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasRole($role)
public function hasRole($roleId): bool
{
return $this->roles->pluck('name')->contains($role);
return $this->roles->pluck('id')->contains($roleId);
}
/**
* 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);
}
/**
@@ -130,40 +135,48 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}
}
/**
* Get all permissions belonging to a the current user.
* @param bool $cache
* @return \Illuminate\Database\Eloquent\Relations\HasManyThrough
*/
public function permissions($cache = true)
{
if (isset($this->permissions) && $cache) {
return $this->permissions;
}
$this->load('roles.permissions');
$permissions = $this->roles->map(function ($role) {
return $role->permissions;
})->flatten()->unique();
$this->permissions = $permissions;
return $permissions;
}
/**
* Check if the user has a particular permission.
* @param $permissionName
* @return bool
*/
public function can($permissionName)
public function can(string $permissionName): bool
{
if ($this->email === 'guest') {
return false;
}
return $this->permissions()->pluck('name')->contains($permissionName);
return $this->permissions()->contains($permissionName);
}
/**
* Get all permissions belonging to a the current user.
*/
protected function permissions(): Collection
{
if (isset($this->permissions)) {
return $this->permissions;
}
$this->permissions = $this->newQuery()->getConnection()->table('role_user', 'ru')
->select('role_permissions.name as name')->distinct()
->leftJoin('permission_role', 'ru.role_id', '=', 'permission_role.role_id')
->leftJoin('role_permissions', 'permission_role.permission_id', '=', 'role_permissions.id')
->where('ru.user_id', '=', $this->id)
->get()
->pluck('name');
return $this->permissions;
}
/**
* Clear any cached permissions on this instance.
*/
public function clearPermissionCache()
{
$this->permissions = null;
}
/**
* Attach a role to this user.
* @param Role $role
*/
public function attachRole(Role $role)
{
@@ -172,9 +185,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);
}
@@ -195,11 +207,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;
@@ -209,7 +219,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
try {
$avatar = $this->avatar ? url($this->avatar->getThumb($size, $size, false)) : $default;
} catch (\Exception $err) {
} catch (Exception $err) {
$avatar = $default;
}
return $avatar;
@@ -217,9 +227,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');
}
@@ -232,6 +241,27 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->hasMany(ApiToken::class);
}
/**
* Get the favourite instances for this user.
*/
public function favourites(): HasMany
{
return $this->hasMany(Favourite::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.
*/
@@ -246,15 +276,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;
@@ -277,4 +305,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,30 @@
<?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 Images;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
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 +32,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 +96,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 +109,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 +140,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 +158,60 @@ 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->favourites()->delete();
$user->delete();
// Delete user profile images
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
$this->userAvatar->destroyAllForUser($user);
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 +252,22 @@ class UserRepo
/**
* Get the roles in the system that are assignable to a user.
* @return mixed
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->newQuery()->orderBy('name', 'asc')->get();
return Role::query()->orderBy('display_name', 'asc')->get();
}
/**
* Get an avatar image for a user and set it as their avatar.
* Returns early if avatars disabled or not set in config.
* @param User $user
* @return bool
*/
public function downloadAndAssignUserAvatar(User $user)
public function downloadAndAssignUserAvatar(User $user): void
{
if (!Images::avatarFetchEnabled()) {
return false;
}
try {
$avatar = Images::saveUserAvatar($user);
$user->avatar()->associate($avatar);
$user->save();
return true;
$this->userAvatar->fetchAndAssignToUser($user);
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
return false;
}
}
}

View File

@@ -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', '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,11 +184,8 @@ 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,
],
// Proxy configuration

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

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

View File

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

View File

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

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,6 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Actions\View;
use Illuminate\Console\Command;
class ClearViews extends Command
@@ -36,7 +37,7 @@ class ClearViews extends Command
*/
public function handle()
{
\Views::resetAll();
View::clearAll();
$this->comment('Views cleared');
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,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'];
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,17 +1,26 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
use BookStack\Actions\Favourite;
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\Favouritable;
use BookStack\Interfaces\Sluggable;
use BookStack\Interfaces\Viewable;
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 +40,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, Favouritable, Viewable
{
use SoftDeletes;
use HasCreatorAndUpdater;
use HasOwner;
/**
* @var string - Name of property where the main text content is found
@@ -50,7 +60,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 +102,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 +130,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 +140,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 +164,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 +173,15 @@ class Entity extends Ownable
/**
* Get this entities restrictions.
*/
public function permissions()
public function permissions(): MorphMany
{
return $this->morphMany(EntityPermission::class, 'restrictable');
}
/**
* Check if this entity has a specific restriction set against it.
* @param $role_id
* @param $action
* @return bool
*/
public function hasRestriction($role_id, $action)
public function hasRestriction(int $role_id, string $action): bool
{
return $this->permissions()->where('role_id', '=', $role_id)
->where('action', '=', $action)->count() > 0;
@@ -193,55 +189,42 @@ class Entity extends Ownable
/**
* Get the entity jointPermissions this is connected to.
* @return MorphMany
*/
public function jointPermissions()
public function jointPermissions(): MorphMany
{
return $this->morphMany(JointPermission::class, 'entity');
}
/**
* Allows checking of the exact class, Used to check entity type.
* Cleaner method for is_a.
* @param $type
* @return bool
* Get the related delete records for this entity.
*/
public static function isA($type)
public function deletions(): MorphMany
{
return $this->morphMany(Deletion::class, 'deletable');
}
/**
* Check if this instance or class is a certain type of entity.
* Examples of $type are 'page', 'book', 'chapter'
*/
public static function isA(string $type): bool
{
return static::getType() === strtolower($type);
}
/**
* Get entity type.
* @return mixed
* Get the entity type as a simple lowercase word.
*/
public static function getType()
public static function getType(): string
{
return strtolower(static::getClassName());
}
/**
* Get an instance of an entity of the given type.
* @param $type
* @return Entity
*/
public static function getEntityInstance($type)
{
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
$className = str_replace([' ', '-', '_'], '', ucwords($type));
if (!in_array($className, $types)) {
return null;
}
return app('BookStack\\Entities\\' . $className);
$className = array_slice(explode('\\', static::class), -1, 1)[0];
return strtolower($className);
}
/**
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public function getShortName($length = 25)
public function getShortName(int $length = 25): string
{
if (mb_strlen($this->name) <= $length) {
return $this->name;
@@ -251,35 +234,45 @@ class Entity extends Ownable
/**
* Get the body text of this entity.
* @return mixed
*/
public function getText()
public function getText(): string
{
return $this->{$this->textField};
return $this->{$this->textField} ?? '';
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt(int $length = 100)
public function getExcerpt(int $length = 100): string
{
$text = $this->getText();
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
return trim($text);
}
/**
* Get the url of this entity
* @param $path
* @return string
*/
public function getUrl($path = '/')
abstract public function getUrl(string $path = '/'): string;
/**
* Get the parent entity if existing.
* This is the "static" parent and does not include dynamic
* relations such as shelves to books.
*/
public function getParent(): ?Entity
{
return $path;
if ($this->isA('page')) {
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
}
if ($this->isA('chapter')) {
return $this->book()->withTrashed()->first();
}
return null;
}
/**
@@ -288,7 +281,7 @@ class Entity extends Ownable
public function rebuildPermissions()
{
/** @noinspection PhpUnhandledExceptionInspection */
Permissions::buildJointPermissionsForEntity($this);
Permissions::buildJointPermissionsForEntity(clone $this);
}
/**
@@ -296,17 +289,33 @@ class Entity extends Ownable
*/
public function indexForSearch()
{
$searchService = app()->make(SearchService::class);
$searchService->indexEntity($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;
}
/**
* @inheritdoc
*/
public function favourites(): MorphMany
{
return $this->morphMany(Favourite::class, 'favouritable');
}
/**
* Check if the entity is a favourite of the current user.
*/
public function isFavourite(): bool
{
return $this->favourites()
->where('user_id', '=', user()->id)
->exists();
}
}

View File

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

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Entities;
<?php namespace BookStack\Entities\Models;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\Attachment;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
@@ -21,18 +22,25 @@ use Permissions;
*/
class Page extends BookChild
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $fillable = ['name', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
public $textField = 'text';
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot', 'deleted_at'];
protected $casts = [
'draft' => 'boolean',
'template' => 'boolean',
];
/**
* Get the entities that are visible to the current user.
*/
public function scopeVisible(Builder $query)
public function scopeVisible(Builder $query): Builder
{
$query = Permissions::enforceDraftVisiblityOnQuery($query);
$query = Permissions::enforceDraftVisibilityOnQuery($query);
return parent::scopeVisible($query);
}
@@ -47,14 +55,6 @@ class Page extends BookChild
return $array;
}
/**
* Get the parent item
*/
public function parent(): Entity
{
return $this->chapter_id ? $this->chapter : $this->book;
}
/**
* Get the chapter that this page is in, If applicable.
* @return BelongsTo
@@ -75,11 +75,23 @@ class Page extends BookChild
/**
* Get the associated page revisions, ordered by created date.
* @return mixed
* Only provides actual saved page revision instances, Not drafts.
*/
public function revisions()
public function revisions(): HasMany
{
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
return $this->allRevisions()
->where('type', '=', 'version')
->orderBy('created_at', 'desc')
->orderBy('id', 'desc');
}
/**
* Get all revision instances assigned to this page.
* Includes all types of revisions.
*/
public function allRevisions(): HasMany
{
return $this->hasMany(PageRevision::class);
}
/**
@@ -92,22 +104,19 @@ class Page extends BookChild
}
/**
* Get the url for this page.
* @param string|bool $path
* @return string
* Get the url of this page.
*/
public function getUrl($path = false)
public function getUrl($path = ''): string
{
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
$midText = $this->draft ? '/draft/' : '/page/';
$idComponent = $this->draft ? $this->id : urlencode($this->slug);
$parts = [
'books',
urlencode($this->getAttribute('bookSlug') ?? $this->book->slug),
$this->draft ? 'draft' : 'page',
$this->draft ? $this->id : urlencode($this->slug),
trim($path, '/'),
];
$url = '/books/' . urlencode($bookSlug) . $midText . $idComponent;
if ($path !== false) {
$url .= '/' . trim($path, '/');
}
return url($url);
return url('/' . implode('/', $parts));
}
/**
@@ -118,4 +127,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

@@ -0,0 +1,17 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\EntityProvider;
abstract class EntityQuery
{
protected function permissionService(): PermissionService
{
return app()->make(PermissionService::class);
}
protected function entityProvider(): EntityProvider
{
return app()->make(EntityProvider::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Facades\DB;
class Popular extends EntityQuery
{
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
{
$query = $this->permissionService()
->filterRestrictedEntityRelations(View::query(), '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');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
}
return $query->with('viewable')
->skip($count * ($page - 1))
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Collection;
class RecentlyViewed extends EntityQuery
{
public function run(int $count, int $page): Collection
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()->filterRestrictedEntityRelations(
View::query(),
'views',
'viewable_id',
'viewable_type',
'view'
)
->orderBy('views.updated_at', 'desc')
->where('user_id', '=', user()->id);
return $query->with('viewable')
->skip(($page - 1) * $count)
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\Favourite;
use Illuminate\Database\Query\JoinClause;
class TopFavourites extends EntityQuery
{
public function run(int $count, int $skip = 0)
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
->select('favourites.*')
->leftJoin('views', function (JoinClause $join) {
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
$join->where('views.user_id', '=', user()->id);
})
->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id);
return $query->with('favouritable')
->skip($skip)
->take($count)
->get()
->pluck('favouritable')
->filter();
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,69 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use Illuminate\Support\Collection;
/**
* Finds the next or previous content of a book element (page or chapter).
*/
class NextPreviousContentLocator
{
protected $relativeBookItem;
protected $flatTree;
protected $currentIndex = null;
/**
* NextPreviousContentLocator constructor.
*/
public function __construct(BookChild $relativeBookItem, Collection $bookTree)
{
$this->relativeBookItem = $relativeBookItem;
$this->flatTree = $this->treeToFlatOrderedCollection($bookTree);
$this->currentIndex = $this->getCurrentIndex();
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getNext(): ?Entity
{
return $this->flatTree->get($this->currentIndex + 1);
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getPrevious(): ?Entity
{
return $this->flatTree->get($this->currentIndex - 1);
}
/**
* Get the index of the current relative item.
*/
protected function getCurrentIndex(): ?int
{
$index = $this->flatTree->search(function (Entity $entity) {
return get_class($entity) === get_class($this->relativeBookItem)
&& $entity->id === $this->relativeBookItem->id;
});
return $index === false ? null : $index;
}
/**
* Convert a book tree collection to a flattened version
* where all items follow the expected order of user flow.
*/
protected function treeToFlatOrderedCollection(Collection $bookTree): Collection
{
$flatOrdered = collect();
/** @var Entity $item */
foreach ($bookTree->all() as $item) {
$flatOrdered->push($item);
$childPages = $item->visible_pages ?? [];
$flatOrdered = $flatOrdered->concat($childPages);
}
return $flatOrdered;
}
}

View File

@@ -1,10 +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 DOMElement;
use DOMNodeList;
use DOMXPath;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
class PageContent
{
@@ -26,6 +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);
}
/**
@@ -44,18 +77,24 @@ class PageContent
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes
$idMap = [];
foreach ($childNodes as $index => $childNode) {
$this->setUniqueId($childNode, $idMap);
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap);
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Generate inner html as a string
@@ -67,23 +106,34 @@ class PageContent
return $html;
}
/**
* Update the all links to the $old location to instead point to $new.
*/
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
}
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element
* @param array $idMap
* Returns a pair of strings in the format [old_id, new_id]
*/
protected function setUniqueId($element, array &$idMap)
protected function setUniqueId(\DOMNode $element, array &$idMap): array
{
if (get_class($element) !== 'DOMElement') {
return;
return ['', ''];
}
// Overwrite id if not a BookStack custom id
// Stop if there's an existing valid id that has not already been used.
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return;
return [$existingId, $existingId];
}
// Create an unique id for the element
@@ -100,6 +150,7 @@ class PageContent
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
return [$existingId, $newId];
}
/**
@@ -108,7 +159,7 @@ class PageContent
protected function toPlainText(): string
{
$html = $this->render(true);
return strip_tags($html);
return html_entity_decode(strip_tags($html));
}
/**
@@ -119,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) {
@@ -258,47 +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 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

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

View File

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

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