Compare commits

...

269 Commits

Author SHA1 Message Date
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
3d5899d28c Fixed issue with using old non-existing reference in controller
Also done a little code cleanup.
2021-06-13 14:16:09 +01:00
Dan Brown
917d7428d6 Updated composer.lock 2021-06-13 14:06:56 +01:00
Dan Brown
bcc01bd8ff New Crowdin updates (#2790)
* New translations common.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations auth.php (Chinese Simplified)

* New translations auth.php (Chinese Simplified)

* New translations errors.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations validation.php (Indonesian)

* New translations settings.php (Spanish, Argentina)
2021-06-13 14:04:23 +01:00
Dan Brown
2c34a99248 Merge pull request #2791 from BookStackApp/attachments_open_in_browser
Attachment serving without forced download
2021-06-13 14:03:08 +01:00
Dan Brown
789d17ab3f Updated platform deps and development version number 2021-06-13 13:57:29 +01:00
Dan Brown
58117bcf2d Extracted not found text into its own simple blade file
Related/intended for #2796
2021-06-13 13:53:59 +01:00
Dan Brown
b5caaa73b7 Fixed content parsing break with line html comment
Fixes issues thrown in custom HMTL head & page content filtering when
the content is comprised of only a single HTML comment.
Adds tests to cover.

For #2804
2021-06-13 12:53:04 +01:00
Dan Brown
7997300f96 Added front-end toggle and testing of inline attachments 2021-06-06 13:55:56 +01:00
Dan Brown
888f435651 Added back-end attachments-in-browser support
A query string will cause attachments to be provided inline
with an appropriate mime type.
Remaining actions:
- Tests
- Front-end functionality
- Config option?
2021-06-06 00:51:06 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
a8471b2c66 Updated translator attribution before release v21.05.1 2021-06-04 23:08:43 +01:00
Dan Brown
0627efe5e9 Updated base64 image extraction to use url instead of path
To ensure it works with all storage types and follows the format of
manually uploaded image content
2021-06-04 22:59:31 +01:00
Dan Brown
af7d62799c New Crowdin updates (#2787)
* New translations common.php (German)

* New translations common.php (Dutch)
2021-06-04 22:50:48 +01:00
Dan Brown
bb00c331e4 Ordered entity permission roles by display name
Closes #2782
2021-06-04 22:36:30 +01:00
Dan Brown
807f92b693 Updated homepage action button colors for consistency
Were previously inconsistent with other homepage buttons for non-default
homepage options.
2021-06-04 22:28:38 +01:00
Dan Brown
c5d31ea7b2 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-06-04 22:21:06 +01:00
Dan Brown
ef1bde8bb1 Fixed wrong styles for homepage favourites
When using a non-default homepage option.

Fixes #2783
2021-06-04 22:20:11 +01:00
Dan Brown
8897945609 Roll-out and re-fix of croation via crowdin (#2785)
* New translations auth.php (Croatian)

* New translations activities.php (Croatian)

* New translations activities.php (German Informal)

* New translations common.php (Croatian)

* New translations passwords.php (Croatian)

* New translations settings.php (Czech)

* New translations settings.php (Spanish)

* New translations settings.php (Catalan)

* New translations settings.php (Arabic)

* New translations settings.php (French)

* New translations pagination.php (Croatian)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Hebrew)

* New translations validation.php (Korean)

* New translations validation.php (Croatian)

* New translations settings.php (Hungarian)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Japanese)

* New translations settings.php (Korean)

* New translations settings.php (Dutch)

* New translations settings.php (Polish)

* New translations settings.php (Portuguese)

* New translations settings.php (Russian)

* New translations settings.php (Slovak)

* New translations settings.php (Slovenian)

* New translations settings.php (Turkish)

* New translations settings.php (Ukrainian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Indonesian)

* New translations settings.php (Persian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Croatian)

* New translations settings.php (Latvian)

* New translations settings.php (Bosnian)

* New translations settings.php (Norwegian Bokmal)

* New translations settings.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (Italian)

* New translations settings.php (Swedish)

* New translations settings.php (Bulgarian)

* New translations errors.php (German Informal)

* New translations errors.php (Croatian)

* New translations components.php (Croatian)

* New translations entities.php (Croatian)

* New translations pagination.php (Croatian)

* New translations entities.php (Croatian)

* New translations components.php (Croatian)

* New translations errors.php (Croatian)

* New translations settings.php (Croatian)

* New translations validation.php (Croatian)

* New translations passwords.php (Croatian)

* New translations auth.php (Croatian)

* New translations common.php (Croatian)

* New translations activities.php (Croatian)
2021-06-02 22:15:58 +01:00
Dan Brown
9382d647d7 Merge branch 'ffranchina-master' 2021-06-02 21:57:23 +01:00
Dan Brown
0d17d18d07 New Crowdin updates (#2777)
* New translations common.php (Latvian)

* New translations entities.php (Latvian)

* New translations activities.php (Italian)

* New translations common.php (Italian)

* New translations entities.php (Italian)

* New translations errors.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations activities.php (Spanish, Argentina)

* New translations common.php (Spanish, Argentina)

* New translations common.php (French)

* New translations common.php (Swedish)

* New translations activities.php (Swedish)

* New translations common.php (Swedish)

* New translations entities.php (Swedish)

* New translations errors.php (Swedish)

* New translations settings.php (Swedish)

* New translations validation.php (Bulgarian)

* New translations validation.php (Bulgarian)

* New translations common.php (Bulgarian)

* New translations validation.php (Bulgarian)

* New translations settings.php (Bulgarian)

* New translations activities.php (Indonesian)

* New translations settings.php (Bulgarian)

* New translations common.php (Bulgarian)

* New translations entities.php (Bulgarian)

* New translations activities.php (Turkish)

* New translations settings.php (Bulgarian)

* New translations components.php (Bulgarian)

* New translations activities.php (Russian)

* New translations common.php (Russian)

* New translations entities.php (Russian)

* New translations common.php (Russian)

* New translations entities.php (Russian)
2021-06-02 21:56:53 +01:00
Dan Brown
24eef03fb9 Added croatian to required arrays/lists 2021-06-02 21:55:30 +01:00
Dan Brown
e51352e1a4 Added back in commas, reset settings language array
Related to #2784
2021-06-02 21:50:38 +01:00
Dan Brown
2dfb1ae3ee Merge branch 'master' of https://github.com/ffranchina/BookStack into ffranchina-master 2021-06-02 21:44:39 +01:00
Dan Brown
39928e1c63 Reviewed base64 image upload support
- Added test cases to cover.
- Altered parsing logic to be a little less reliant on regex.
- Added new iamge repo method for creating from data.
- Added extension validation and additional type support.
- Done some cleanup of common operations within PageContent.
- Added message to API docs/method to mention image usage.

For #2700 and #2631.
2021-06-02 21:34:34 +01:00
Dan Brown
40ca50e44f Merge branch 'master' of https://github.com/awarre/BookStack into awarre-master 2021-06-02 20:25:20 +01:00
Francesco Franchina
fc7b8c49fb Adding Croatian translation files 2021-06-02 17:32:31 +02:00
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
awarre
f8b5a0fd50 Add base64 image support 2021-04-20 23:41:21 +00: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
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
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
Boddy4
20f9a50cee LDAP: Added TLS support 2020-11-18 01:05:29 +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
Jan Mareš
034478409e Add support Windows Authentication via SAML 2020-04-03 14:05:07 +02:00
James Geiger
fe438bdb45 Add footer element, styles, and associated settings 2020-03-18 22:28:06 -05:00
jakob
6acd958927 Add the "Create Shelf" resp. "Create Book" to the home view 2019-10-30 11:42:37 +01:00
544 changed files with 16146 additions and 5268 deletions

View File

@@ -51,7 +51,7 @@ DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
@@ -195,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/
@@ -245,10 +248,15 @@ AVATAR_URL=
DRAWIO=true
# Default item listing view
# Used for public visitors and user's without a preference
# Can be 'list' or 'grid'
# Used for public visitors and user's without a preference.
# Can be 'list' or 'grid'.
APP_VIEWS_BOOKS=list
APP_VIEWS_BOOKSHELVES=grid
APP_VIEWS_BOOKSHELF=grid
# Use dark mode by default
# Will be overriden by any user/session preference.
APP_DEFAULT_DARK_MODE=false
# Page revision limit
# Number of page revisions to keep in the system before deleting old revisions.

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
@@ -134,3 +140,30 @@ 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
syn7ax69 :: Bulgarian; Turkish
Blaade :: French

View File

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

View File

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

View File

@@ -6,6 +6,7 @@ use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str;
/**
@@ -23,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;

View File

@@ -78,7 +78,7 @@ class ActivityService
public function latest(int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->filterRestrictedEntityRelations($this->activity->newQuery(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with(['user', 'entity'])
->skip($count * $page)
@@ -131,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)

View File

@@ -48,4 +48,4 @@ class ActivityType
const AUTH_PASSWORD_RESET_UPDATE = 'auth_password_reset_update';
const AUTH_LOGIN = 'auth_login';
const AUTH_REGISTER = 'auth_register';
}
}

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,6 +1,7 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Tag extends Model
{
@@ -9,10 +10,25 @@ class Tag extends Model
/**
* 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

@@ -26,7 +26,9 @@ class TagRepo
*/
public function getNameSuggestions(?string $searchTerm): Collection
{
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
$query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('name');
if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@@ -45,7 +47,9 @@ class TagRepo
*/
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
{
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
$query = $this->tag->newQuery()
->select('*', DB::raw('count(*) as count'))
->groupBy('value');
if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');

View File

@@ -1,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,116 +0,0 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\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\Models\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')
->filter();
}
/**
* Get all recently viewed entities for the current user.
*/
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$all = collect();
/** @var Entity $instance */
foreach ($this->entityProvider->all() as $name => $instance) {
$items = $instance::visible()->withLastView()
->orderBy('last_viewed_at', 'desc')
->skip($count * ($page - 1))
->take($count)
->get();
$all = $all->concat($items);
}
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
}
/**
* Reset all view counts by deleting all views.
*/
public function resetAll()
{
$this->view->truncate();
}
}

View File

@@ -142,5 +142,4 @@ class ApiDocsGenerator
];
});
}
}
}

View File

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

View File

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

@@ -34,5 +34,4 @@ class Saml2SessionGuard extends ExternalBaseSessionGuard
{
return false;
}
}

View File

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

@@ -6,6 +6,8 @@ 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
@@ -71,6 +73,7 @@ class RegistrationService
}
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) {
@@ -83,7 +86,6 @@ class RegistrationService
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
}
return $newUser;
@@ -109,5 +111,4 @@ class RegistrationService
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), $redirect);
}
}
}
}

View File

@@ -6,6 +6,8 @@ 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;
@@ -375,6 +377,7 @@ 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

@@ -2,37 +2,63 @@
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
@@ -40,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();
}
/**
@@ -50,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();
}
/**
@@ -60,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');
}
@@ -91,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);
@@ -101,14 +127,15 @@ class SocialAuthService
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());
}
@@ -130,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');
}
@@ -207,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();
}
@@ -229,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);
@@ -240,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,25 +1,33 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
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\EntityProvider;
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
@@ -27,47 +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.
*/
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)
{
@@ -76,81 +57,63 @@ class PermissionService
/**
* Prepare the local entity cache and ensure it's empty
* @param \BookStack\Entities\Models\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\Models\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;
}
/**
@@ -158,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()->withTrashed()->select(['id', 'restricted', 'owned_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->withTrashed()->newQuery()
->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']);
}]);
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;
@@ -227,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\Models\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)
{
@@ -288,7 +247,7 @@ class PermissionService
});
// Chunk through all bookshelves
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'owned_by'])
Bookshelf::query()->select(['id', 'restricted', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->buildJointPermissionsForShelves($shelves, $roles);
});
@@ -296,7 +255,6 @@ class PermissionService
/**
* Delete the entity jointPermissions attached to a particular role.
* @param Role $role
*/
public function deleteJointPermissionsForRole(Role $role)
{
@@ -312,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)
{
@@ -327,10 +285,10 @@ class PermissionService
/**
* Delete all of the entity jointPermissions for a list of entities.
* @param \BookStack\Entities\Models\Entity[] $entities
* @throws \Throwable
* @param Entity[] $entities
* @throws Throwable
*/
protected function deleteManyJointPermissionsForEntities($entities)
protected function deleteManyJointPermissionsForEntities(array $entities)
{
if (count($entities) === 0) {
return;
@@ -352,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) {
@@ -408,16 +366,14 @@ class PermissionService
/**
* Get the actions related to an entity.
* @param \BookStack\Entities\Models\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;
@@ -426,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']);
@@ -450,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);
}
@@ -460,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) {
@@ -479,38 +429,27 @@ 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\Models\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,
'owned_by' => $entity->getRawAttribute('owned_by')
'owned_by' => $entity->getRawAttribute('owned_by'),
];
}
@@ -524,55 +463,47 @@ class PermissionService
$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';
$allPermission = $user && $user->can($permission . '-all');
$ownPermission = $user && $user->can($permission . '-own');
$ownerField = ($ownable instanceof Entity) ? 'owned_by' : 'created_by';
$isOwner = $this->currentUser() && $this->currentUser()->id === $ownable->$ownerField;
$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('owned_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());
}
@@ -581,46 +512,22 @@ class PermissionService
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
* @param \BookStack\Entities\Models\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('owned_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;
}
@@ -634,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('owned_by', '=', $this->currentUser()->id);
});
$this->addJointHasPermissionCheck($query, $this->currentUser()->id);
});
});
});
@@ -651,7 +554,7 @@ 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)
@@ -663,109 +566,90 @@ class PermissionService
}
/**
* Add restrictions for a generic entity
* @param string $entityType
* @param Builder|\BookStack\Entities\Models\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('owned_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('owned_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('owned_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();
}
@@ -775,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

@@ -92,7 +92,7 @@ class Role extends Model implements Loggable
}
/**
* Get all visible roles
* Get all visible roles.
*/
public static function visible(): Collection
{
@@ -104,7 +104,10 @@ class Role extends Model implements Loggable
*/
public static function restrictable(): Collection
{
return static::query()->where('system_name', '!=', 'admin')->get();
return static::query()
->where('system_name', '!=', 'admin')
->orderBy('display_name', 'asc')
->get();
}
/**

View File

@@ -1,7 +1,10 @@
<?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;
@@ -22,6 +25,7 @@ use Illuminate\Support\Collection;
* Class User
* @property string $id
* @property string $name
* @property string $slug
* @property string $email
* @property string $password
* @property Carbon $created_at
@@ -30,8 +34,9 @@ use Illuminate\Support\Collection;
* @property int $image_id
* @property string $external_auth_id
* @property string $system_name
* @property Collection $roles
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
{
use Authenticatable, CanResetPassword, Notifiable;
@@ -72,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';
}
@@ -115,12 +118,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasSystemRole($role)
public function hasSystemRole(string $roleSystemName): bool
{
return $this->roles->pluck('system_name')->contains($role);
return $this->roles->pluck('system_name')->contains($roleSystemName);
}
/**
@@ -184,9 +185,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the social account associated with this user.
* @return HasMany
*/
public function socialAccounts()
public function socialAccounts(): HasMany
{
return $this->hasMany(SocialAccount::class);
}
@@ -207,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;
@@ -229,9 +227,8 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Get the avatar for the user.
* @return BelongsTo
*/
public function avatar()
public function avatar(): BelongsTo
{
return $this->belongsTo(Image::class, 'image_id');
}
@@ -244,6 +241,14 @@ 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.
*/
@@ -271,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;
@@ -310,4 +313,13 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
{
return "({$this->id}) {$this->name}";
}
/**
* @inheritDoc
*/
public function refreshSlug(): string
{
$this->slug = app(SlugGenerator::class)->generate($this);
return $this->slug;
}
}

View File

@@ -8,13 +8,11 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
@@ -45,6 +43,14 @@ class UserRepo
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.
*/
@@ -159,7 +165,13 @@ class UserRepo
'email_confirmed' => $emailConfirmed,
'external_auth_id' => $data['external_auth_id'] ?? '',
];
return User::query()->forceCreate($details);
$user = new User();
$user->forceFill($details);
$user->refreshSlug();
$user->save();
return $user;
}
/**
@@ -170,16 +182,11 @@ class UserRepo
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->delete();
// Delete user profile images
$profileImages = Image::query()->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);

View File

@@ -19,13 +19,6 @@ 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.
@@ -63,7 +56,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'nb', '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', 'hr', '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',
@@ -122,6 +115,7 @@ 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,
@@ -190,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

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

View File

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

View File

@@ -132,6 +132,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

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

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

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

@@ -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,8 +1,5 @@
<?php namespace BookStack\Entities\Models;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Book;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -49,7 +46,7 @@ abstract class BookChild extends Entity
// 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

@@ -2,6 +2,7 @@
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;
@@ -9,6 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions;
use BookStack\Interfaces\Favouritable;
use BookStack\Interfaces\Sluggable;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
@@ -37,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
abstract class Entity extends Model
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
{
use SoftDeletes;
use HasCreatorAndUpdater;
@@ -289,11 +293,29 @@ abstract class Entity extends Model
}
/**
* Generate and set a new URL slug for this model.
* @inheritdoc
*/
public function refreshSlug(): string
{
$this->slug = (new SlugGenerator)->generate($this);
$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

@@ -40,7 +40,7 @@ class Page extends BookChild
*/
public function scopeVisible(Builder $query): Builder
{
$query = Permissions::enforceDraftVisiblityOnQuery($query);
$query = Permissions::enforceDraftVisibilityOnQuery($query);
return parent::scopeVisible($query);
}
@@ -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);
}
/**

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

@@ -177,25 +177,24 @@ class PageRepo
// Hold the old details to compare later
$oldHtml = $page->html;
$oldName = $page->name;
$oldMarkdown = $page->markdown;
$this->updateTemplateStatusAndContentFromInput($page, $input);
$this->baseRepo->update($page, $input);
// Update with new details
$page->revision_count++;
if (setting('app-editor') !== 'markdown') {
$page->markdown = '';
}
$page->save();
// Remove all update drafts for this user & page.
$this->getUserDraftQuery($page)->delete();
// Save a revision after updating
$summary = $input['summary'] ?? null;
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
$summary = trim($input['summary'] ?? "");
$htmlChanged = isset($input['html']) && $input['html'] !== $oldHtml;
$nameChanged = isset($input['name']) && $input['name'] !== $oldName;
$markdownChanged = isset($input['markdown']) && $input['markdown'] !== $oldMarkdown;
if ($htmlChanged || $nameChanged || $markdownChanged || $summary) {
$this->savePageRevision($page, $summary);
}
@@ -213,7 +212,7 @@ class PageRepo
if (!empty($input['markdown'] ?? '')) {
$pageContent->setNewMarkdown($input['markdown']);
} else {
$pageContent->setNewHTML($input['html']);
$pageContent->setNewHTML($input['html'] ?? '');
}
}
@@ -224,10 +223,6 @@ class PageRepo
{
$revision = new PageRevision($page->getAttributes());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
}
$revision->page_id = $page->id;
$revision->slug = $page->slug;
$revision->book_slug = $page->book->slug;
@@ -290,7 +285,13 @@ class PageRepo
$page->fill($revision->toArray());
$content = new PageContent($page);
$content->setNewHTML($revision->html);
if (!empty($revision->markdown)) {
$content->setNewMarkdown($revision->markdown);
} else {
$content->setNewHTML($revision->html);
}
$page->updated_by = user()->id;
$page->refreshSlug();
$page->save();

View File

@@ -13,4 +13,4 @@ class CustomStrikeThroughExtension implements ExtensionInterface
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
}
}
}

View File

@@ -21,4 +21,4 @@ class CustomStrikethroughRenderer implements InlineRendererInterface
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

@@ -2,9 +2,15 @@
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Util\HtmlContentFilter;
use BookStack\Uploads\ImageRepo;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
@@ -28,6 +34,7 @@ class PageContent
*/
public function setNewHTML(string $html)
{
$html = $this->extractBase64Images($this->page, $html);
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
$this->page->markdown = '';
@@ -53,23 +60,70 @@ class PageContent
$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);
}
/**
* Convert all base64 image data to saved images
*/
public function extractBase64Images(Page $page, string $htmlText): string
{
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
return $htmlText;
}
$doc = $this->loadDocumentFromHtml($htmlText);
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
$imageRepo = app()->make(ImageRepo::class);
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// Get all img elements with image data blobs
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
foreach ($imageNodes as $imageNode) {
$imageSrc = $imageNode->getAttribute('src');
[$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
// Validate extension
if (!in_array($extension, $allowedExtensions)) {
$imageNode->setAttribute('src', '');
continue;
}
// Save image from data with a random name
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
try {
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id);
$imageNode->setAttribute('src', $image->url);
} catch (ImageUploadException $exception) {
$imageNode->setAttribute('src', '');
}
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Formats a page's html to be tagged correctly within the system.
*/
protected function formatHtml(string $htmlText): string
{
if ($htmlText == '') {
if (empty($htmlText)) {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($htmlText);
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
@@ -108,7 +162,7 @@ class PageContent
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
}
@@ -161,12 +215,12 @@ class PageContent
/**
* Render the page for viewing
*/
public function render(bool $blankIncludes = false) : string
public function render(bool $blankIncludes = false): string
{
$content = $this->page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
$content = HtmlContentFilter::removeScripts($content);
}
if ($blankIncludes) {
@@ -187,9 +241,7 @@ class PageContent
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($htmlContent);
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
@@ -229,7 +281,7 @@ class PageContent
/**
* Remove any page include tags within the given HTML.
*/
protected function blankPageIncludes(string $html) : string
protected function blankPageIncludes(string $html): string
{
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
}
@@ -237,7 +289,7 @@ class PageContent
/**
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
*/
protected function parsePageIncludes(string $html) : string
protected function parsePageIncludes(string $html): string
{
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
@@ -280,9 +332,7 @@ class PageContent
protected function fetchSectionOfPage(Page $page, string $sectionId): string
{
$topLevelTags = ['table', 'ul', 'ol'];
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($page->html);
// Search included content for the id given and blank out if not exists.
$matchingElem = $doc->getElementById($sectionId);
@@ -307,63 +357,14 @@ class PageContent
}
/**
* Escape script tags within HTML content.
* Create and load a DOMDocument from the given html content.
*/
protected function escapeScripts(string $html) : string
protected function loadDocumentFromHtml(string $html): DOMDocument
{
if (empty($html)) {
return $html;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$html = '<body>' . $html . '</body>';
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove clickable links to JavaScript URI
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
foreach ($badLinks as $badLink) {
$badLink->parentNode->removeChild($badLink);
}
// Remove forms with calls to JavaScript URI
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
foreach ($badForms as $badForm) {
$badForm->parentNode->removeChild($badForm);
}
// Remove meta tag to prevent external redirects
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
foreach ($metaTags as $metaTag) {
$metaTag->parentNode->removeChild($metaTag);
}
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
foreach ($badIframes as $badIframe) {
$badIframe->parentNode->removeChild($badIframe);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
return $doc;
}
}

View File

@@ -137,5 +137,4 @@ class SearchOptions
return $string;
}
}
}

View File

@@ -1,6 +1,7 @@
<?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;
@@ -178,7 +179,7 @@ class SearchRunner
}
}
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
return $this->permissionService->enforceEntityRestrictions($entity, $entitySelect, $action);
}
/**
@@ -270,24 +271,29 @@ class SearchRunner
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,6 +1,7 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\BookChild;
use BookStack\Interfaces\Sluggable;
use Illuminate\Support\Str;
class SlugGenerator
@@ -10,11 +11,11 @@ class SlugGenerator
* Generate a fresh slug for the given entity.
* The slug will generated so it does not conflict within the same parent item.
*/
public function generate(Entity $entity): string
public function generate(Sluggable $model): string
{
$slug = $this->formatNameAsSlug($entity->name);
while ($this->slugInUse($slug, $entity)) {
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
$slug = $this->formatNameAsSlug($model->name);
while ($this->slugInUse($slug, $model)) {
$slug .= '-' . Str::random(3);
}
return $slug;
}
@@ -35,16 +36,16 @@ class SlugGenerator
* Check if a slug is already in-use for this
* type of model within the same parent.
*/
protected function slugInUse(string $slug, Entity $entity): bool
protected function slugInUse(string $slug, Sluggable $model): bool
{
$query = $entity->newQuery()->where('slug', '=', $slug);
$query = $model->newQuery()->where('slug', '=', $slug);
if ($entity instanceof BookChild) {
$query->where('book_id', '=', $entity->book_id);
if ($model instanceof BookChild) {
$query->where('book_id', '=', $model->book_id);
}
if ($entity->id) {
$query->where('id', '!=', $entity->id);
if ($model->id) {
$query->where('id', '!=', $model->id);
}
return $query->count() > 0;

View File

@@ -151,6 +151,7 @@ class TrashCan
protected function destroyPage(Page $page): int
{
$this->destroyCommonRelations($page);
$page->allRevisions()->delete();
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
@@ -273,11 +274,11 @@ class TrashCan
$count++;
};
if ($entity->isA('chapter') || $entity->isA('book')) {
if ($entity instanceof Chapter || $entity instanceof Book) {
$entity->pages()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
if ($entity->isA('book')) {
if ($entity instanceof Book) {
$entity->chapters()->withTrashed()->withCount('deletions')->get()->each($restoreAction);
}
@@ -286,19 +287,20 @@ class TrashCan
/**
* Destroy the given entity.
* @throws Exception
*/
protected function destroyEntity(Entity $entity): int
{
if ($entity->isA('page')) {
if ($entity instanceof Page) {
return $this->destroyPage($entity);
}
if ($entity->isA('chapter')) {
if ($entity instanceof Chapter) {
return $this->destroyChapter($entity);
}
if ($entity->isA('book')) {
if ($entity instanceof Book) {
return $this->destroyBook($entity);
}
if ($entity->isA('shelf')) {
if ($entity instanceof Bookshelf) {
return $this->destroyShelf($entity);
}
}
@@ -316,6 +318,7 @@ class TrashCan
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
$entity->deletions()->delete();
$entity->favourites()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Setting extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'setting';
}
}

View File

@@ -1,8 +1,9 @@
<?php namespace BookStack\Facades;
use BookStack\Theming\ThemeService;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
class Theme extends Facade
{
/**
* Get the registered name of the component.
@@ -11,6 +12,6 @@ class Images extends Facade
*/
protected static function getFacadeAccessor()
{
return 'images';
return ThemeService::class;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Views extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'views';
}
}

View File

@@ -27,4 +27,4 @@ abstract class ApiController extends Controller
{
return $this->rules;
}
}
}

View File

@@ -25,5 +25,4 @@ class ApiDocsController extends ApiController
$docs = ApiDocsGenerator::generateConsideringCache();
return response()->json($docs);
}
}

View File

@@ -91,4 +91,4 @@ class BookApiController extends ApiController
$this->bookRepo->destroy($book);
return response('', 204);
}
}
}

View File

@@ -112,4 +112,4 @@ class BookshelfApiController extends ApiController
$this->bookshelfRepo->destroy($shelf);
return response('', 204);
}
}
}

View File

@@ -60,6 +60,8 @@ class PageApiController extends ApiController
*
* Any HTML content provided should be kept to a single-block depth of plain HTML
* elements to remain compatible with the BookStack front-end and editors.
* Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload.
*/
public function create(Request $request)
{

View File

@@ -14,16 +14,14 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $attachment;
protected $pageRepo;
/**
* AttachmentController constructor.
*/
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
{
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo;
}
@@ -67,7 +65,7 @@ class AttachmentController extends Controller
'file' => 'required|file'
]);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
@@ -89,7 +87,7 @@ class AttachmentController extends Controller
*/
public function getUpdateForm(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
@@ -104,8 +102,8 @@ class AttachmentController extends Controller
*/
public function update(Request $request, string $attachmentId)
{
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
@@ -160,7 +158,7 @@ class AttachmentController extends Controller
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
return view('attachments.manager-link-form', [
'pageId' => $pageId,
@@ -202,9 +200,10 @@ class AttachmentController extends Controller
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function get(string $attachmentId)
public function get(Request $request, string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
try {
$page = $this->pageRepo->getById($attachment->uploaded_to);
} catch (NotFoundException $exception) {
@@ -217,8 +216,13 @@ class AttachmentController extends Controller
return redirect($attachment->path);
}
$fileName = $attachment->getFileName();
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
}
return $this->downloadResponse($attachmentContents, $fileName);
}
/**
@@ -227,7 +231,8 @@ class AttachmentController extends Controller
*/
public function delete(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
return response()->json(['message' => trans('entities.attachments_deleted')]);

View File

@@ -20,6 +20,7 @@ class AuditLogController extends Controller
'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
'user' => $request->get('user', ''),
];
$query = Activity::query()
@@ -34,6 +35,9 @@ class AuditLogController extends Controller
if ($listDetails['event']) {
$query->where('type', '=', $listDetails['event']);
}
if ($listDetails['user']) {
$query->where('user_id', '=', $listDetails['user']);
}
if ($listDetails['date_from']) {
$query->where('created_at', '>=', $listDetails['date_from']);

View File

@@ -2,12 +2,15 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\EmailConfirmationService;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\ConfirmationEmailException;
use BookStack\Exceptions\UserTokenExpiredException;
use BookStack\Exceptions\UserTokenNotFoundException;
use BookStack\Facades\Theme;
use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -80,6 +83,8 @@ class ConfirmEmailController extends Controller
$user->save();
auth()->login($user);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
$this->showSuccessNotification(trans('auth.email_confirm_success'));
$this->emailConfirmationService->deleteByUser($user);

View File

@@ -7,9 +7,12 @@ use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Facades\Theme;
use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
@@ -150,6 +153,7 @@ class LoginController extends Controller
}
}
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
return redirect()->intended($this->redirectPath());
}
@@ -196,4 +200,18 @@ class LoginController extends Controller
return redirect('/login');
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
])->redirectTo('/login');
}
}

View File

@@ -2,11 +2,14 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -93,6 +96,8 @@ class RegisterController extends Controller
try {
$user = $this->registrationService->registerUser($userData);
auth()->login($user);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
} catch (UserRegistrationException $exception) {
if ($exception->getMessage()) {
$this->showErrorNotification($exception->getMessage());
@@ -117,5 +122,4 @@ class RegisterController extends Controller
'password' => Hash::make($data['password']),
]);
}
}

View File

@@ -82,5 +82,4 @@ class Saml2Controller extends Controller
return redirect()->intended();
}
}

View File

@@ -2,16 +2,17 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\RegistrationService;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\SocialDriverNotConfigured;
use BookStack\Exceptions\SocialSignInAccountNotUsed;
use BookStack\Exceptions\SocialSignInException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Http\Controllers\Controller;
use Illuminate\Http\RedirectResponse;
use BookStack\Theming\ThemeEvents;
use Illuminate\Http\Request;
use Illuminate\Routing\Redirector;
use Illuminate\Support\Str;
use Laravel\Socialite\Contracts\User as SocialUser;
@@ -31,12 +32,11 @@ class SocialController extends Controller
$this->registrationService = $registrationService;
}
/**
* Redirect to the relevant social site.
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
* @throws SocialDriverNotConfigured
*/
public function getSocialLogin(string $socialDriver)
public function login(string $socialDriver)
{
session()->put('social-callback', 'login');
return $this->socialAuthService->startLogIn($socialDriver);
@@ -47,7 +47,7 @@ class SocialController extends Controller
* @throws SocialDriverNotConfigured
* @throws UserRegistrationException
*/
public function socialRegister(string $socialDriver)
public function register(string $socialDriver)
{
$this->registrationService->ensureRegistrationAllowed();
session()->put('social-callback', 'register');
@@ -60,7 +60,7 @@ class SocialController extends Controller
* @throws SocialDriverNotConfigured
* @throws UserRegistrationException
*/
public function socialCallback(Request $request, string $socialDriver)
public function callback(Request $request, string $socialDriver)
{
if (!session()->has('social-callback')) {
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
@@ -99,7 +99,7 @@ class SocialController extends Controller
/**
* Detach a social account from a user.
*/
public function detachSocialAccount(string $socialDriver)
public function detach(string $socialDriver)
{
$this->socialAuthService->detachSocialAccount($socialDriver);
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
@@ -113,7 +113,7 @@ class SocialController extends Controller
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
{
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
// Create an array of the user data to create a new user instance
@@ -130,6 +130,8 @@ class SocialController extends Controller
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
auth()->login($user);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $socialDriver, $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
$this->showSuccessNotification(trans('auth.register_success'));
return redirect('/');

View File

@@ -2,11 +2,14 @@
namespace BookStack\Http\Controllers\Auth;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserTokenExpiredException;
use BookStack\Exceptions\UserTokenNotFoundException;
use BookStack\Facades\Theme;
use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Exception;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
@@ -68,6 +71,8 @@ class UserInviteController extends Controller
$user->save();
auth()->login($user);
Theme::dispatch(ThemeEvents::AUTH_LOGIN, auth()->getDefaultDriver(), $user);
$this->logActivity(ActivityType::AUTH_LOGIN, $user);
$this->showSuccessNotification(trans('auth.user_invite_success', ['appName' => setting('app-name')]));
$this->inviteService->deleteByUser($user);

View File

@@ -2,6 +2,7 @@
use Activity;
use BookStack\Actions\ActivityType;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\PermissionsUpdater;
@@ -11,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class BookController extends Controller
{
@@ -30,7 +30,7 @@ class BookController extends Controller
*/
public function index()
{
$view = setting()->getForCurrentUser('books_view_type', config('app.views.books'));
$view = setting()->getForCurrentUser('books_view_type');
$sort = setting()->getForCurrentUser('books_sort', 'name');
$order = setting()->getForCurrentUser('books_sort_order', 'asc');
@@ -112,7 +112,7 @@ class BookController extends Controller
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->visible()->get();
Views::add($book);
View::incrementFor($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
@@ -32,7 +33,7 @@ class BookshelfController extends Controller
*/
public function index()
{
$view = setting()->getForCurrentUser('bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$view = setting()->getForCurrentUser('bookshelves_view_type');
$sort = setting()->getForCurrentUser('bookshelves_sort', 'name');
$order = setting()->getForCurrentUser('bookshelves_sort_order', 'asc');
$sortOptions = [
@@ -101,15 +102,26 @@ class BookshelfController extends Controller
$shelf = $this->bookshelfRepo->getBySlug($slug);
$this->checkOwnablePermission('book-view', $shelf);
Views::add($shelf);
$sort = setting()->getForCurrentUser('shelf_books_sort', 'default');
$order = setting()->getForCurrentUser('shelf_books_sort_order', 'asc');
$sortedVisibleShelfBooks = $shelf->visibleBooks()->get()
->sortBy($sort === 'default' ? 'pivot.order' : $sort, SORT_REGULAR, $order === 'desc')
->values()
->all();
View::incrementFor($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$view = setting()->getForCurrentUser('bookshelf_view_type', config('app.views.books'));
$view = setting()->getForCurrentUser('bookshelf_view_type');
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
'sortedVisibleShelfBooks' => $sortedVisibleShelfBooks,
'view' => $view,
'activity' => Activity::entityActivity($shelf, 20, 1)
'activity' => Activity::entityActivity($shelf, 20, 1),
'order' => $order,
'sort' => $sort
]);
}

View File

@@ -1,15 +1,16 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class ChapterController extends Controller
{
@@ -64,7 +65,8 @@ class ChapterController extends Controller
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $chapter->getVisiblePages();
Views::add($chapter);
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters.show', [
@@ -72,7 +74,9 @@ class ChapterController extends Controller
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}

View File

@@ -6,6 +6,7 @@ use BookStack\Facades\Activity;
use BookStack\Interfaces\Loggable;
use BookStack\HasCreatorAndUpdater;
use BookStack\Model;
use finfo;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException;
@@ -121,6 +122,20 @@ abstract class Controller extends BaseController
]);
}
/**
* Create a file download response that provides the file with a content-type
* correct for the file, in a way so the browser can show the content in browser.
*/
protected function inlineDownloadResponse(string $content, string $fileName): Response
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
return response()->make($content, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
]);
}
/**
* Show a positive, successful notification to the user on next view load.
*/
@@ -159,6 +174,6 @@ abstract class Controller extends BaseController
*/
protected function getImageValidationRules(): string
{
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,webp';
return 'image_extension|mimes:jpeg,png,gif,webp';
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Interfaces\Favouritable;
use BookStack\Model;
use Illuminate\Http\Request;
class FavouriteController extends Controller
{
/**
* Show a listing of all favourite items for the current user.
*/
public function index(Request $request)
{
$viewCount = 20;
$page = intval($request->get('page', 1));
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
return view('common.detailed-listing-with-more', [
'title' => trans('entities.my_favourites'),
'entities' => $favourites->slice(0, $viewCount),
'hasMoreLink' => $hasMoreLink,
]);
}
/**
* Add a new item as a favourite.
*/
public function add(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->firstOrCreate([
'user_id' => user()->id,
]);
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* Remove an item as a favourite.
*/
public function remove(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->where([
'user_id' => user()->id,
])->delete();
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* @throws \Illuminate\Validation\ValidationException
* @throws \Exception
*/
protected function getValidatedModelFromRequest(Request $request): Favouritable
{
$modelInfo = $this->validate($request, [
'type' => 'required|string',
'id' => 'required|integer',
]);
if (!class_exists($modelInfo['type'])) {
throw new \Exception('Model not found');
}
/** @var Model $model */
$model = new $modelInfo['type'];
if (! $model instanceof Favouritable) {
throw new \Exception('Model not favouritable');
}
$modelInstance = $model->newQuery()
->where('id', '=', $modelInfo['id'])
->first(['id', 'name']);
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
if (is_null($modelInstance) || $inaccessibleEntity) {
throw new \Exception('Model instance not found');
}
return $modelInstance;
}
}

View File

@@ -2,11 +2,12 @@
use Activity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Queries\RecentlyViewed;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
@@ -32,12 +33,13 @@ class HomeController extends Controller
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
Views::getUserRecentlyViewed(12*$recentFactor, 1)
(new RecentlyViewed)->run(12*$recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
$favourites = (new TopFavourites)->run(6);
$recentlyUpdatedPages = Page::visible()->with('book')
->where('draft', false)
->orderBy('updated_at', 'desc')
->take(12)
->take($favourites->count() > 0 ? 6 : 12)
->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@@ -51,12 +53,13 @@ class HomeController extends Controller
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'favourites' => $favourites,
];
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getForCurrentUser($key . '_view_type', config('app.views.' . $key));
$view = setting()->getForCurrentUser($key . '_view_type');
$sort = setting()->getForCurrentUser($key . '_sort', 'name');
$order = setting()->getForCurrentUser($key . '_sort_order', 'asc');
@@ -105,20 +108,21 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials.custom-head-content');
return view('partials.custom-head');
}
/**
* Show the view for /robots.txt
* @return $this
*/
public function getRobots()
{
$sitePublic = setting('app-public', false);
$allowRobots = config('app.allow_robots');
if ($allowRobots === null) {
$allowRobots = $sitePublic;
}
return response()
->view('common.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controllers\Controller;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
@@ -27,12 +28,15 @@ class ImageController extends Controller
/**
* Provide an image file from storage.
* @throws NotFoundException
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
throw (new NotFoundException(trans('errors.image_not_found')))
->setSubtitle(trans('errors.image_not_found_subtitle'))
->setDetails(trans('errors.image_not_found_details'));
}
return response()->file($path);

View File

@@ -1,19 +1,19 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class PageController extends Controller
{
@@ -142,7 +142,9 @@ class PageController extends Controller
$page->load(['comments.createdBy']);
}
Views::add($page);
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
View::incrementFor($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
'page' => $page,
@@ -150,7 +152,9 @@ class PageController extends Controller
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
'pageNav' => $pageNav,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}
@@ -243,8 +247,8 @@ class PageController extends Controller
$updateTime = $draft->updated_at->timestamp;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $updateTime
]);
}
@@ -267,7 +271,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@@ -283,7 +287,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@@ -295,7 +299,6 @@ class PageController extends Controller
* Remove the specified page from storage.
* @throws NotFoundException
* @throws Throwable
* @throws NotifyException
*/
public function destroy(string $bookSlug, string $pageSlug)
{
@@ -311,7 +314,6 @@ class PageController extends Controller
/**
* Remove the specified draft page from storage.
* @throws NotFoundException
* @throws NotifyException
* @throws Throwable
*/
public function destroyDraft(string $bookSlug, int $pageId)
@@ -340,9 +342,9 @@ class PageController extends Controller
->paginate(20)
->setPath(url('/pages/recently-updated'));
return view('pages.detailed-listing', [
return view('common.detailed-listing-paginated', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
'entities' => $pages
]);
}
@@ -380,7 +382,7 @@ class PageController extends Controller
try {
$parent = $this->pageRepo->move($page, $entitySelection);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -424,7 +426,7 @@ class PageController extends Controller
try {
$pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -445,7 +447,7 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
return view('pages.permissions', [
'page' => $page,
'page' => $page,
]);
}

View File

@@ -1,9 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\Popular;
use BookStack\Entities\Tools\SearchRunner;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Tools\SearchOptions;
@@ -12,16 +9,13 @@ use Illuminate\Http\Request;
class SearchController extends Controller
{
protected $viewService;
protected $searchRunner;
protected $entityContextManager;
public function __construct(
ViewService $viewService,
SearchRunner $searchRunner,
ShelfContext $entityContextManager
) {
$this->viewService = $viewService;
$this->searchRunner = $searchRunner;
$this->entityContextManager = $entityContextManager;
}
@@ -85,7 +79,7 @@ class SearchController extends Controller
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
}
return view('search.entity-ajax-list', ['entities' => $entities]);

View File

@@ -0,0 +1,47 @@
<?php namespace BookStack\Http\Controllers;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
class StatusController extends Controller
{
/**
* Show the system status as a simple json page.
*/
public function show()
{
$statuses = [
'database' => $this->trueWithoutError(function () {
return DB::table('migrations')->count() > 0;
}),
'cache' => $this->trueWithoutError(function () {
$rand = Str::random();
Cache::set('status_test', $rand);
return Cache::get('status_test') === $rand;
}),
'session' => $this->trueWithoutError(function () {
$rand = Str::random();
Session::put('status_test', $rand);
return Session::get('status_test') === $rand;
}),
];
$hasError = in_array(false, $statuses);
return response()->json($statuses, $hasError ? 500 : 200);
}
/**
* Check the callable passed returns true and does not throw an exception.
*/
protected function trueWithoutError(callable $test): bool
{
try {
return $test() === true;
} catch (\Exception $e) {
return false;
}
}
}

View File

@@ -140,5 +140,4 @@ class UserApiTokenController extends Controller
$token = ApiToken::query()->where('user_id', '=', $user->id)->where('id', '=', $tokenId)->firstOrFail();
return [$user, $token];
}
}

View File

@@ -5,10 +5,13 @@ use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\Access\UserInviteService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
class UserController extends Controller
{
@@ -61,7 +64,7 @@ class UserController extends Controller
/**
* Store a newly created user in storage.
* @throws UserUpdateException
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function store(Request $request)
{
@@ -90,6 +93,7 @@ class UserController extends Controller
$user->external_auth_id = $request->get('external_auth_id');
}
$user->refreshSlug();
$user->save();
if ($sendInvite) {
@@ -132,8 +136,8 @@ class UserController extends Controller
/**
* Update the specified user in storage.
* @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Illuminate\Validation\ValidationException
* @throws ImageUploadException
* @throws ValidationException
*/
public function update(Request $request, int $id)
{
@@ -157,6 +161,11 @@ class UserController extends Controller
$user->email = $request->get('email');
}
// Refresh the slug if the user's name has changed
if ($user->isDirty('name')) {
$user->refreshSlug();
}
// Role updates
if (userCan('users-manage') && $request->filled('roles')) {
$roles = $request->get('roles');
@@ -216,7 +225,7 @@ class UserController extends Controller
/**
* Remove the specified user from storage.
* @throws \Exception
* @throws Exception
*/
public function destroy(Request $request, int $id)
{
@@ -243,25 +252,6 @@ class UserController extends Controller
return redirect('/settings/users');
}
/**
* Show the user profile page
*/
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users.profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
'assetCounts' => $assetCounts
]);
}
/**
* Update the user's preferred book-list display setting.
*/
@@ -310,7 +300,7 @@ class UserController extends Controller
*/
public function changeSort(Request $request, string $id, string $type)
{
$validSortTypes = ['books', 'bookshelves'];
$validSortTypes = ['books', 'bookshelves', 'shelf_books'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
@@ -353,7 +343,7 @@ class UserController extends Controller
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$sort = $request->get('sort');
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
if (!in_array($sort, ['name', 'created_at', 'updated_at', 'default'])) {
$sort = 'name';
}

View File

@@ -0,0 +1,25 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\UserRepo;
class UserProfileController extends Controller
{
/**
* Show the user profile page
*/
public function show(UserRepo $repo, string $slug)
{
$user = $repo->getBySlug($slug);
$userActivity = $repo->getActivity($user);
$recentlyCreated = $repo->getRecentlyCreated($user, 5);
$assetCounts = $repo->getAssetCounts($user);
return view('users.profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
'assetCounts' => $assetCounts
]);
}
}

View File

@@ -28,8 +28,8 @@ class Kernel extends HttpKernel
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\BookStack\Http\Middleware\RunThemeActions::class,
\BookStack\Http\Middleware\Localization::class,
\BookStack\Http\Middleware\GlobalViewData::class,
],
'api' => [
\BookStack\Http\Middleware\ThrottleApiRequests::class,

View File

@@ -33,4 +33,4 @@ trait ChecksForEmailConfirmation
return false;
}
}
}

View File

@@ -1,27 +0,0 @@
<?php namespace BookStack\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
/**
* Class GlobalViewData
* Sets up data that is accessible to any view rendered by the web routes.
*/
class GlobalViewData
{
/**
* Handle an incoming request.
*
* @param Request $request
* @param Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
view()->share('signedIn', auth()->check());
view()->share('currentUser', user());
return $next($request);
}
}

View File

@@ -18,6 +18,8 @@ class Localization
protected $localeMap = [
'ar' => 'ar',
'bg' => 'bg_BG',
'bs' => 'bs_BA',
'ca' => 'ca',
'da' => 'da_DK',
'de' => 'de_DE',
'de_informal' => 'de_DE',
@@ -26,13 +28,16 @@ class Localization
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'he' => 'he_IL',
'hr' => 'hr_HR',
'id' => 'id_ID',
'it' => 'it_IT',
'ja' => 'ja',
'ko' => 'ko_KR',
'lv' => 'lv_LV',
'nl' => 'nl_NL',
'nb' => 'nb_NO',
'pl' => 'pl_PL',
'pt' => 'pl_PT',
'pt' => 'pt_PT',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
@@ -57,12 +62,7 @@ class Localization
$defaultLang = config('app.locale');
config()->set('app.default_locale', $defaultLang);
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
} else {
$locale = setting()->getUser(user(), 'language', $defaultLang);
}
$locale = $this->getUserLocale($request, $defaultLang);
config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
// Set text direction
@@ -76,14 +76,29 @@ class Localization
return $next($request);
}
/**
* Get the locale specifically for the currently logged in user if available.
*/
protected function getUserLocale(Request $request, string $default): string
{
try {
$user = user();
} catch (\Exception $exception) {
return $default;
}
if ($user->isDefault() && config('app.auto_detect_locale')) {
return $this->autoDetectLocale($request, $default);
}
return setting()->getUser($user, 'language', $default);
}
/**
* Autodetect the visitors locale by matching locales in their headers
* against the locales supported by BookStack.
* @param Request $request
* @param string $default
* @return string
*/
protected function autoDetectLocale(Request $request, string $default)
protected function autoDetectLocale(Request $request, string $default): string
{
$availableLocales = config('app.locales');
foreach ($request->getLanguages() as $lang) {
@@ -96,10 +111,8 @@ class Localization
/**
* Get the ISO version of a BookStack language name
* @param string $locale
* @return string
*/
public function getLocaleIso(string $locale)
public function getLocaleIso(string $locale): string
{
return $this->localeMap[$locale] ?? $locale;
}
@@ -107,7 +120,6 @@ class Localization
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
* @param string $locale
*/
protected function setSystemDateLocale(string $locale)
{

View File

@@ -0,0 +1,29 @@
<?php
namespace BookStack\Http\Middleware;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Closure;
class RunThemeActions
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle($request, Closure $next)
{
$earlyResponse = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_BEFORE, $request);
if (!is_null($earlyResponse)) {
return $earlyResponse;
}
$response = $next($request);
$response = Theme::dispatch(ThemeEvents::WEB_MIDDLEWARE_AFTER, $request, $response) ?? $response;
return $response;
}
}

View File

@@ -14,5 +14,4 @@ class ThrottleApiRequests extends Middleware
{
return (int) config('api.requests_per_minute');
}
}
}

View File

@@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Favouritable
{
/**
* Get the related favourite instances.
*/
public function favourites(): MorphMany;
}

View File

@@ -8,4 +8,4 @@ interface Loggable
* Get the string descriptor for this item.
*/
public function logDescriptor(): string;
}
}

View File

@@ -0,0 +1,23 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Builder;
/**
* Interface Sluggable
*
* Assigned to models that can have slugs.
* Must have the below properties.
*
* @property int $id
* @property string $name
* @method Builder newQuery
*/
interface Sluggable
{
/**
* Regenerate the slug for this model.
*/
public function refreshSlug(): string;
}

View File

@@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Viewable
{
/**
* Get all view instances for this viewable model.
*/
public function views(): MorphMany;
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Providers;
use Blade;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
@@ -8,9 +9,11 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Laravel\Socialite\Contracts\Factory as SocialiteFactory;
use Schema;
use URL;
@@ -59,7 +62,11 @@ class AppServiceProvider extends ServiceProvider
public function register()
{
$this->app->singleton(SettingService::class, function ($app) {
return new SettingService($app->make(Setting::class), $app->make('Illuminate\Contracts\Cache\Repository'));
return new SettingService($app->make(Setting::class), $app->make(Repository::class));
});
$this->app->singleton(SocialAuthService::class, function($app) {
return new SocialAuthService($app->make(SocialiteFactory::class));
});
}
}

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