Compare commits

...

543 Commits

Author SHA1 Message Date
Dan Brown
f9b9040a06 LLM: Played with a reduced-scope non-vector LLM query system 2026-01-11 18:00:07 +00:00
Dan Brown
8e0edb63c7 Merge branch 'development' into vectors 2026-01-07 11:14:53 +00:00
Dan Brown
20db372596 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2026-01-07 11:10:55 +00:00
Dan Brown
43eed1660c Meta: Updated dev version, license year, crowdin config
Added Id to crowdin config for compatibility with upcoming change to
crowdin CLI process after switch to codeberg
2026-01-07 11:09:39 +00:00
Dan Brown
e6b754fad0 Merge pull request #5969 from shaoliang123456/fix/git-safe-directory-in-docker
Git 2.35+ may refuse to operate on bind-mounted repos with differing ownership ("dubious ownership"), Mark /app as safe within the container.
2026-01-03 17:56:52 +00:00
leon
018de5def3 fix: Configure safe directory for git in dockerfile 2025-12-31 16:20:52 +08:00
leon
5c4fc3dc2c fix: Docker: Add
git safe.directory config for bind-mounted repos.Mark
 /app as safe directory to handle Git 2.35+ ownership
 checks in Docker containers.
2025-12-31 11:53:22 +08:00
Dan Brown
07ec880e33 Testing: Updated search tests to consider new limits 2025-12-30 17:09:26 +00:00
Dan Brown
ab436ed5c3 Updated translations with latest Crowdin changes (#5962) 2025-12-30 16:32:21 +00:00
Dan Brown
082befb2fc Updated PHP packages and translators pre v25.12.1 2025-12-30 16:16:39 +00:00
Dan Brown
b0a8cb0c5d Merge pull request #5968 from BookStackApp/limits
Add some additional resource-based limits
2025-12-30 16:14:04 +00:00
Dan Brown
b08d1b36de Search: Set limits on the amount of search terms
Sets some reasonable limits, which are higher when logged in since that
infers a little extra trust.
Helps prevent against large resource consuption attacks via super heavy
search queries.

Thanks to Gabriel Rodrigues AKA TEXUGO for reporting.
2025-12-30 13:32:14 +00:00
Dan Brown
88d86df66f ZIP Exports: Added limit to ZIP file size before extraction
Checks files within the ZIP again the app upload file limit
before using/streaming/extracting, to help ensure that they do no exceed
what might be expected on that instance, and to prevent disk exhaustion
via things like super high compression ratio files.

Thanks to Jeong Woo Lee (eclipse07077-ljw) for reporting.
2025-12-29 23:08:18 +00:00
Dan Brown
38d3697246 Updated translations with latest Crowdin changes (#5933) 2025-12-24 11:52:56 +00:00
Dan Brown
d93354ff0e Updated licenses and translation attribution pre v25.12 2025-12-24 11:51:37 +00:00
Dan Brown
3336e0c6ae Deps: Updated PHP packages via composer 2025-12-24 11:48:42 +00:00
Dan Brown
8fc9a2af4e Lexical API: Updated docs to reflect public event usage 2025-12-23 18:33:54 +00:00
Dan Brown
8aec571123 Mentions: Fixed some users not showing in mention selector 2025-12-21 18:33:50 +00:00
Dan Brown
382f4db276 Merge pull request #5956 from BookStackApp/system_cli_update_dec25
System CLI: Update to v0.4
2025-12-20 15:48:59 +00:00
Dan Brown
d504b19143 System CLI: Update to v0.4
- The init & update commands will now use download-vendor logic instead
  of using composer to install required PHP packages.
- The init command will now use our source.bookstackapp.com git mirror
  instead of GitHub.
- Updated depenancy PHP package versions.
2025-12-20 15:46:15 +00:00
Dan Brown
d87e8d05c7 Merge pull request #5939 from BookStackApp/lexical_fixes_2512
Lexical fixes for v25.12
2025-12-20 14:05:30 +00:00
Dan Brown
0b48361780 Search: Fixed pagination not considering sub-paths
For #5951
Added test to cover.
2025-12-19 15:15:23 +00:00
Dan Brown
2de3247ae4 Deps: Updated PHP package versions
Includes major version change of antonioribeiro/google2fa which changes
secret length. From manual testing of old MFA secrets and new, this
should not be breaking at all.
2025-12-19 14:22:27 +00:00
Dan Brown
48df2be0d8 DB: Added views->viewable_type index
For #5948
2025-12-19 10:54:09 +00:00
Dan Brown
a4c0556551 Merge pull request #5952 from gerundt/iis-module-link
Update "Microsoft URL Rewrite Module for IIS" download link
2025-12-19 10:24:22 +00:00
Dan Brown
a941d1b403 Comment mentions: Fixed CI and test scenarios 2025-12-18 17:40:05 +00:00
Dan Brown
51f9b63db0 Comment Mentions: Fixed and tweaks during review of changes
- Added advisory on role permission form to advise which allow listing
  of users/roles.
- Updated database config to avoid PHP8.5 deprecation.
- Tweaked migration to remove unused index.
- Fixed test namespace.
2025-12-18 17:15:29 +00:00
Dan Brown
90fc02c57f Esbuild & Mentions: Updated interaction stability and build system
- Updated esbuild system to be module, and fixed build command.
- Reverted module use in package.json by default as this impacted test
  runs/files.
- Updated mention user select:
  - To look better in dark mode.
  - To not remove text after on select.
  - To properly revert/restore focus on enter or cancel.
2025-12-17 21:11:01 +00:00
Tim Gerundt
4aeb571126 Update "Microsoft URL Rewrite Module for IIS" download link 2025-12-17 15:11:29 +01:00
Dan Brown
3d9aba7b1f Mentions: Added coverage for mentions search endpoint 2025-12-17 13:48:20 +00:00
Dan Brown
48cdaab690 Comment Mentions: Added tests to cover back-end functionality 2025-12-17 10:49:12 +00:00
Dan Brown
4f760479c3 Merge branch 'development' into comment_mentions 2025-12-17 09:58:27 +00:00
Dan Brown
9211062e8e Merge pull request #5919 from BookStackApp/v25-11
Merge v25-11 changes into dev
2025-12-17 09:57:49 +00:00
Dan Brown
221c6c7e9f Comment Mentions: Added core back-end logic
- Added new user notification preference, opt-in by default
- Added parser to extract mentions from comment HTML, with tests to
  cover.
- Added notification and notification handling

Not yet tested, needs testing coverage.
2025-12-17 09:57:14 +00:00
Dan Brown
e2f91c2bbb Comment Mentions: Added keyboard nav, worked on design 2025-12-14 17:19:08 +00:00
Dan Brown
147ff00c7a JS: Swapped livereload lib for esbuild livereload setup 2025-12-14 16:25:19 +00:00
Dan Brown
1e768ce33f Lexical: Changed mention to be a decorator node
Allows better selection.
Also updated existing decorator file names to align with classes so
they're easier to find.
Also aligned/fixed decorator constuctor/setup methods.
2025-12-13 17:03:48 +00:00
Dan Brown
313326b32a Updated translator & dependency attribution before release v25.11.6 2025-12-09 20:59:57 +00:00
Dan Brown
1d87b513be Deps: Updated PHP package versions 2025-12-09 20:40:48 +00:00
Dan Brown
9bf9ae9c37 Mentions: Added new endpoint, Built editor list display 2025-12-09 16:56:34 +00:00
Dan Brown
50540e23a1 Lexical: Created mention node, started mention service, split comment editor out 2025-12-08 15:52:21 +00:00
Dan Brown
3e1b0587ec Lexical: Fixed undefined entity selector value
Also added pre-fill of selector search based on selected text range.
2025-12-07 15:07:08 +00:00
Dan Brown
6661ae8178 Lexical: Improved focus control for popup modal forms
Now moves focus to first field on open, and restores focus back to
editor on submit/close.
2025-12-07 15:02:06 +00:00
Dan Brown
1ee5711435 Pagination: Fixed lack of responsiveness causing overflow
For #5920
2025-12-07 13:09:50 +00:00
Dan Brown
08e7ba7064 Images: Made resize errors log with error detail
Closes #5869
2025-12-07 12:54:57 +00:00
Dan Brown
34e747162f Testing: Fixed some incorrect test case names 2025-12-06 23:24:33 +00:00
Dan Brown
10f5ceee35 List page settings: Review of #5606
Updated setting display to show mulitple number inputs under one heading
group.
Updated settings to use general number field form view template.
Updated translations to match display changes, and to advise on counts.
Added page count control for search results.
Added setting service method, to get settings as integers, with
min/max/default control.
Updating sorting group to be names "Lists & Sorting".
Added tests to cover.
2025-12-06 23:10:54 +00:00
Dan Brown
9886bbd3a0 Merge branch 'feature/pagination_settings' of github.com:Xenoamor/BookStack into Xenoamor-feature/pagination_settings 2025-12-06 21:47:13 +00:00
Dan Brown
92a3c22b4c Merge pull request #5928 from BookStackApp/lexical_dev_api
Initial developer API for WYSIWYG editor
2025-12-06 15:32:47 +00:00
Dan Brown
b5246a28f0 Lexical API: Updated docs to align method format 2025-12-06 15:18:28 +00:00
Dan Brown
ab4b1c8efa Lexical API: Reviewed docs, Made toolbar its own UI class 2025-12-05 14:44:19 +00:00
Dan Brown
8890746278 Lexical API: Added public event to access editor API
Updated documentation to match.
Ran manual testing of examples.
2025-12-05 14:07:24 +00:00
Dan Brown
dfdcfcfdb8 Lexical API: Added content module, testing and documented 2025-12-05 12:15:18 +00:00
Dan Brown
ebceba0afe Lexical API: Started working on docs format and jest testing 2025-12-04 21:13:17 +00:00
Dan Brown
65f7b61c1f Sessions: Ignored extra meta/dist content in history tracking
For #5925
Added tests to cover.
Extracted existing test to place with similiar sessions tests
2025-12-03 14:10:09 +00:00
Dan Brown
2fde803c76 Deps: Updated PHP package versions
Needed to update some tests due to charset casing change in Symfony 7.4
2025-12-03 13:55:00 +00:00
Dan Brown
adfac3e30e OIDC: Updated state handling to prevent loss from other requests
Which was occuring in chrome, where background requests to the PWA
manifest, or opensearch, endpoint caused OIDC to fail due to lost state
since it was only flashed to the session.
This persists it with a manual TTL.

Added tests to cover.
Manually tested against Azure.
For #5929
2025-12-03 13:34:00 +00:00
Dan Brown
21730aeb39 Sponsors: Tweaked logo sizing 2025-12-02 18:48:21 +00:00
Dan Brown
75231d2d4a Sponsors: Added Admin Intelligence 2025-12-02 18:46:36 +00:00
Dan Brown
9d732d8dd8 Lexical: Started dev API outline 2025-11-30 17:02:17 +00:00
Dan Brown
9e8088f186 JS: Bumped up JS package versions, and node version 2025-11-30 15:07:23 +00:00
Dan Brown
cf847974d2 Merge pull request #5917 from BookStackApp/copy_references
Internal reference handling on content copying
2025-11-29 20:50:05 +00:00
Dan Brown
3cd3e73f60 Copying: Fixed issue with non-page links to page permalinks
Found during manual testing.
Added test case to cover.
2025-11-29 20:35:16 +00:00
Dan Brown
bb350639c6 Styles: Made non-active dark/light css variables exist by default
This means that it would be possible to jump between light/dark mode
with just the class, and no reload needed.
Not something we'll directly use right now, but may be useful in
customizations.
2025-11-27 21:56:45 +00:00
Dan Brown
9de294343d Notifications: Fixed error on comment notification
Fixes an error where a used relation (entity) on the comment was
resulting in null due to eager loading the notification when
deserializing from the queue, where Laravel was then mis-matching the
names when performing the eager loading.

For #5918
2025-11-25 21:08:45 +00:00
Dan Brown
98a09bcc37 Deps: Updated PHP packages 2025-11-25 19:55:22 +00:00
Dan Brown
959981a676 Copying: Added logic to find & update references 2025-11-25 17:52:26 +00:00
Dan Brown
674bb84fac Copying: Added reference change context tracking
Added core wiring in the cloning logic, just need to implement core
logic in the updater now.
2025-11-25 14:46:36 +00:00
Dan Brown
ba675b6349 Copying: Added tests to cover copy self-references
Logic to make tests pass to follow
2025-11-25 13:52:36 +00:00
Dan Brown
f073994bc3 Testing: Extracted copy tests to their own class 2025-11-25 12:36:33 +00:00
Dan Brown
0f40aeb0d3 Merge pull request #5913 from BookStackApp/slug_history
Slug History Tracking & Usage
2025-11-24 20:29:44 +00:00
Dan Brown
cdd164e3e3 Slugs: Added test to cover history lookup permission usage 2025-11-24 20:04:55 +00:00
Dan Brown
c90816987c Slugs: Rolled out history lookup to other types
Added testing to cover.
Also added batch recording of child slug pairs on book slug changes.
2025-11-24 19:49:34 +00:00
Dan Brown
dd393691b1 Slugs: Added lookup system using history
Switched page lookup to use this.
2025-11-24 13:55:11 +00:00
Dan Brown
dd5375f480 Slugs: Fixed storage bugs, added testing coverage 2025-11-24 10:46:24 +00:00
Dan Brown
291a807d98 Slugs: Added slug recording at points of generation
Also moved some model-level helpers, which used app container
resolution, to be injected services instead.
2025-11-23 23:29:30 +00:00
Dan Brown
e64fc60bdf Slugs: Created history table to track changes 2025-11-23 16:50:37 +00:00
Dan Brown
ad582ab9f8 DB testing: Prevented caching during build
To prevent re-using code when re-run for same branch.
2025-11-22 12:40:30 +00:00
Dan Brown
870f3c58c0 DB Testing: Updated supported DB list, added readme
Also fixed potentally flaky GIf test to be more accurate with single
frame GIF images.
2025-11-22 12:24:36 +00:00
Dan Brown
22a7772c3d Env: Added storage type to default example env
Provides greater consideration to the storage type used and the fact
that it'll place images in public space by default.
2025-11-21 13:57:38 +00:00
Dan Brown
9934f85ba9 Deps: Updated PHP packages via composer 2025-11-21 13:42:50 +00:00
Dan Brown
73c6bf4f8d Images: Updated access to consider public secure_restricted
Had prevented public access for images when secure_restricted images was
enabled (and for just secure images) when app settings allowed public
access.

This considers the app public setting, and adds tests to cover extra
scenarios to prevent regression.
2025-11-21 12:09:25 +00:00
Dan Brown
47f12cc8f6 Maintenance: Fixed type issue, updated translator list 2025-11-19 14:38:35 +00:00
Dan Brown
b2f81f5c62 New translations common.php (Albanian) (#5887) 2025-11-19 14:37:04 +00:00
Dan Brown
1be2969055 Dev: Set timezone for test DB creation, added PHP 8.5 to tests
Also fixed some test namespaces
Related to #5881
2025-11-18 19:50:09 +00:00
Dan Brown
99a1d82f0a Deps: Updated PHP package versions
Also updated dev version
2025-11-18 18:36:11 +00:00
Dan Brown
f06a6de2e7 Merge pull request #5899 from BookStackApp/zip_image_handling
Exports: Updated perm checking for images in ZIP exports
2025-11-18 18:27:38 +00:00
Dan Brown
aaa28186bc Exports: Updated perm checking for images in ZIP exports
For #5885
Adds to, uses and cleans-up central permission checking in ImageService
to mirror that which would be experienced by users in the UI to result
in the same image access conditions.

Adds testing to cover.
2025-11-18 14:19:46 +00:00
Dan Brown
8ab9252f9b DB: Added extra query tests, updated db-testing scripts
Also added skipping to avif tests for environments where GD does not
have built-in AVIF support
2025-11-11 11:23:16 +00:00
Dan Brown
befc645705 DB: Added initial DB testing docker-based script 2025-11-11 10:24:56 +00:00
Dan Brown
4eb4407ef7 DB: Updated entity scope to use models dynamic table
This was hardcoded since the table was always the same, but in some
cases Laravel will auto-alias the table name (for example, when in
sub-queries) which will break MySQL 5.7 when the scope attempts to use
the table name instead of the alias.

Needs testing coverage.
For #5877
2025-11-10 19:57:18 +00:00
Dan Brown
5bf2d801cf Notifications: Fixed attempted null usage issue where int expected 2025-11-09 11:39:38 +00:00
Dan Brown
1421ba871d Updated translator & dependency attribution before release v25.11 2025-11-09 10:52:09 +00:00
Dan Brown
563828ba52 Updated translations with latest Crowdin changes (#5843) 2025-11-02 14:41:16 +00:00
Dan Brown
d40a68b411 API: Re-ordered routes, Improved navigation
Updated route order to follow some kind of logic.
Updated scrolling sidebar to not be so cut-off in various scenarios.
Added new nav helper to quick jump to specific API models.

Closes #5865
2025-11-02 14:31:08 +00:00
Dan Brown
4a57933cd1 Deps: Updated PHP composer packages 2025-11-02 13:11:38 +00:00
Dan Brown
1df850ea3e Search: Fixed formatting timeout with many term occurrences
For #5863
2025-10-31 15:55:45 +00:00
Dan Brown
7881bddce0 Merge pull request #5860 from BookStackApp/api_image_data_endpoint
API: Added endpoints for reading image data
2025-10-31 13:48:54 +00:00
Dan Brown
02d024aa32 API: Added endpoints for reading image data 2025-10-29 18:17:51 +00:00
Dan Brown
652124abaf Merge pull request #5854 from BookStackApp/efficient_search
Pagable and efficient search
2025-10-29 13:12:52 +00:00
Dan Brown
751934c84a Search: Tested changes to single-table search
Updated filters to use single table where needed.
2025-10-29 12:59:34 +00:00
Dan Brown
3fd25bd03e Search: Added pagination, updated other search uses
Also updated hydrator to be created via injection.
2025-10-28 20:37:41 +00:00
Dan Brown
f0303de2e5 Search: Improved result hydration performance 2025-10-27 18:02:54 +00:00
Dan Brown
0b26573314 Search: Started work to make search result size consistent 2025-10-27 17:23:15 +00:00
Dan Brown
c21c36e2a6 Merge pull request #5850 from BookStackApp/comments_api
API: Started building comments API endpoints
2025-10-24 15:26:55 +01:00
Dan Brown
a949900570 API: Added examples for comments
Tweaked comment repo to avoid returning a lot of extra data on API
update responses.
2025-10-24 15:14:25 +01:00
Dan Brown
9c4a9225af Comments API: Addressed failing tests and static testing 2025-10-24 14:22:53 +01:00
Dan Brown
4627dfd4f7 API: Added comment tree to pages-read endpoint
Includes tests to cover
2025-10-24 10:18:52 +01:00
Dan Brown
fcacf7cacb API: Built out tests for comment API endpoints 2025-10-23 16:52:29 +01:00
Dan Brown
cbf27d70c8 API: Added comment CUD endpoints, drafted tests
Move some checks and made some tweaks to the repo to support consistency
between API and UI.
2025-10-23 10:21:33 +01:00
Dan Brown
3ad1e31fcc API: Added comment-read endpoint, added api docs section descriptions 2025-10-22 18:44:49 +01:00
Dan Brown
082dbc9944 API: Started building comments API endpoints 2025-10-22 14:58:29 +01:00
Dan Brown
abe9c1e5a3 API Docs: Updated link to archived GitHub repo
Closes #5813
2025-10-22 14:29:02 +01:00
Dan Brown
ebf82617b8 Code: Added groovy syntax highlighting
For #5822
2025-10-21 18:34:21 +01:00
Dan Brown
2c81447c9e Deps: Updated PHP deps via composer 2025-10-21 14:47:24 +01:00
Dan Brown
8898647f78 Merge pull request #5846 from BookStackApp/page_image_nullification
Images: Added nulling of image page relation on page delete
2025-10-21 14:46:49 +01:00
Dan Brown
ea6344898f Images: Added nulling of image page relation on page delete 2025-10-21 14:12:55 +01:00
Dan Brown
0bfd79925e Merge pull request #5844 from BookStackApp/user_ids
Updated handling of deleted user ID handling in DB
2025-10-21 13:45:00 +01:00
Dan Brown
efff8700d4 DB: Addressed test issues for user ID changes
Reverted change for activities table so that a record is retained of
past activity, and added a check where the ID may be displayed to ensure
it does not mislead and accidentially reference other, newer users.
2025-10-19 19:52:15 +01:00
Dan Brown
5754acf2fb DB: Updated handling of deleted user ID handling in DB
Updated uses of user ID to nullify on delete.
Added testing to cover deletion of user relations.
Added model factories to support changes and potential other tests.
Cleans existing ID references in the DB via migration.
2025-10-19 19:10:15 +01:00
Dan Brown
4c7d6420ee DB: Aligned entity structure to a common table
As per PR #5800

* DB: Planned out new entity table format via migrations

* DB: Created entity migration logic

Made some other tweaks/fixes while testing.

* DB: Added change of entity relation columns to suit new entities table

* DB: Got most view queries working for new structure

* Entities: Started logic change to new structure

Updated base entity class, and worked through BaseRepo.
Need to go through other repos next.

Removed a couple of redundant interfaces as part of this since we can
move the logic onto the shared ContainerData model as needed.

* Entities: Been through repos to update for new format

* Entities: Updated repos to act on refreshed clones

Changes to core entity models are now done on clones to ensure clean
state before save, and those clones are returned back if changes are
needed after that action.

* Entities: Updated model classes & relations for changes

* Entities: Changed from *Data to a common "contents" system

Added smart loading from builder instances which should hydrate with
"contents()" loaded via join, while keeping the core model original.

* Entities: Moved entity description/covers to own non-model classes

Added back some interfaces.

* Entities: Removed use of contents system for data access

* Entities: Got most queries back to working order

* Entities: Reverted back to data from contents, fixed various issues

* Entities: Started addressing issues from tests

* Entities: Addressed further tests/issues

* Entities: Been through tests to get all passing in dev

Fixed issues and needed test changes along the way.

* Entities: Addressed phpstan errors

* Entities: Reviewed TODO notes

* Entities: Ensured book/shelf relation data removed on destroy

* Entities: Been through API responses & adjusted field visibility

* Entities: Added type index to massively improve query speed
2025-10-18 13:14:30 +01:00
Dan Brown
146a6c01cc Merge branch 'v25-07' into development 2025-10-05 15:28:29 +01:00
Dan Brown
f8e4ea82c6 Updated translator & dependency attribution before release v25.07.3 2025-10-05 15:26:37 +01:00
Dan Brown
047195c033 Updated translations with latest Crowdin changes (#5786) 2025-10-05 15:22:37 +01:00
Yugo Takano
a7b30c284c Add crossorigin attribute to manifest link 2025-10-05 15:18:40 +01:00
Dan Brown
c3412d8c1c Deps: Updated PHP package versions 2025-10-05 15:17:16 +01:00
Dan Brown
4db7135231 Updated translations with latest Crowdin changes (#5786) 2025-10-05 15:09:34 +01:00
Dan Brown
009d146185 Merge pull request #5820 from tfnh621/patch-1
Fix PWA manifest access behind authenticated proxies
2025-10-05 15:08:59 +01:00
Yugo Takano
fcef1a7948 Add crossorigin attribute to manifest link 2025-10-02 21:39:22 +09:00
Dan Brown
08dfff05f4 Sponsors: Updated diagrams.net sponsor level 2025-09-11 18:58:26 +01:00
Dan Brown
fc10520e10 Merge pull request #5793 from BookStackApp/role_permission_refactor
Permissions: Use of enum references and RolePermission cleanup
2025-09-10 12:16:40 +01:00
Dan Brown
a70c733f27 Permissions: Cleanup after review of enum implementation PR 2025-09-10 11:36:54 +01:00
Dan Brown
573d692a59 Permissions: Fixed check method to allow enum usage 2025-09-10 10:44:54 +01:00
Dan Brown
419dbadcfd Permissions: Updated use of helpers to use enums
Also added middlware method to Permission enum to allow easier usage
with controller middleware.
2025-09-09 09:48:19 +01:00
Dan Brown
33a0237f87 Permissions: Updated usage of controller methods to use enum 2025-09-08 18:14:38 +01:00
Dan Brown
5fc11d46d5 Permissions: Added enum usage to controller helpers
Also fixed various missing types or spelling/formatting points.
Added down action for role_permission table changes in migration.
2025-09-08 16:15:42 +01:00
Dan Brown
c8716df284 Permissions: Removed unused role-perm columns, added permission enum
Updated main permission check methods to support our new enum.
2025-09-08 15:59:25 +01:00
Dan Brown
1ac74099ca Merge pull request #5790 from BookStackApp/timezones
Timezones: Seperate display timezone and consistency update
2025-09-04 16:36:04 +01:00
Dan Brown
36cb243d5e Timezones: Updated date displays to use consistent formats 2025-09-04 16:11:35 +01:00
Dan Brown
579c1bf424 Timezones: Seperated out store & display timezones to two options 2025-09-04 15:06:58 +01:00
Dan Brown
242b7dfb1b Merge pull request #5785 from BookStackApp/phpstan_level2
PHPstan level 3
2025-09-03 15:53:11 +01:00
Dan Brown
7d1c316202 Maintenance: Updated larastan target level, fixed issues from tests 2025-09-03 15:42:50 +01:00
Dan Brown
318b486e0b Maintenance: Finished changes to meet phpstan level 3 2025-09-03 15:18:49 +01:00
Dan Brown
e05ec7da36 Maintenance: Addressed a range of phpstan level 3 issues 2025-09-03 10:47:45 +01:00
Dan Brown
cee23de6c5 Maintenance: Reached PHPstan level 2
Reworked some stuff around slugs to use interface in a better way.
Also standardised phpdoc to use @return instead of @returns
2025-09-02 16:02:52 +01:00
Dan Brown
1e34954554 Maintenance: Continued work towards PHPstan level 2
Updated html description code to be behind a proper interface.
Set new convention for mode traits/interfaces.
2025-09-02 11:10:47 +01:00
Dan Brown
5ea4e1e935 Maintenance: Removed unused comments text column
Has been redundant and unused for a about a year now.
Closes #4821
2025-09-02 10:20:10 +01:00
Dan Brown
a27ce6e915 Packages: Updated npm packages
Spent way too many hours debugging through issues from jsdom changes.
2025-08-30 22:18:09 +01:00
Dan Brown
64b06bcf61 Packages: Updated predis 2025-08-30 11:47:22 +01:00
Dan Brown
cdbac63b40 Framework: Updated to Laravel 12 2025-08-30 11:10:11 +01:00
Dan Brown
d6296ac7a5 Merge pull request #5749 from BookStackApp/admin_command_updates
Create Admin Command: New Flags
2025-08-30 10:47:14 +01:00
Dan Brown
481f356068 Updated translator & dependency attribution before release v25.07.2 2025-08-28 17:39:10 +01:00
Dan Brown
955837c9aa Packages: Upgraded php deps to latest versions 2025-08-28 15:02:26 +01:00
Dan Brown
c6e35c2e7c Merge pull request #5775 from BookStackApp/lexical_aug25
Lexical: August 2025 fixes
2025-08-28 15:00:16 +01:00
Dan Brown
0436ccfebf Updated translations with latest Crowdin changes (#5759) 2025-08-28 14:59:36 +01:00
Dan Brown
f5da31037d Lexical: Fixed details tests
Updated to use new test pattern while there.
2025-08-28 11:17:18 +01:00
Dan Brown
46613f76f6 Lexical: Added backspace handling for details
Allows more reliable removal of details block on backspace at first
child position with the details block.
2025-08-27 14:09:38 +01:00
Dan Brown
519acaf324 Lexical: Added better selection display for collapisble blocks 2025-08-27 12:51:36 +01:00
Dan Brown
849bc4d6c3 Lexical: Improved nested details interaction
- Set to open by default on insert.
- Updated selection handling not to always fully cascade to lowest
  editable child on selection, so parents can be reliably selected.
- Updated mouse handling to treat details panes like the root element,
  inserting within-details where relevant.
2025-08-26 14:45:15 +01:00
Dan Brown
ee994fa2b7 Testing: Addressed deprecation in test helper
Also updated version in phpunit config
2025-08-25 15:01:13 +01:00
Dan Brown
13a79b3f96 Shelves: Addressed book edits removing non-visible books
Tracks the non-visible existing books on change, to retain as part of
the assigned books sync.
Added test to cover.

For #5728
2025-08-25 14:17:55 +01:00
Dan Brown
7c79b10fb6 Imports: Fixed drawing IDs not being updated in content
Would leave imported content with inaccessible images in many cases (or
wrong references) although the drawing was still being uploaded &
related to the page.
Added test to cover.

For #5761
2025-08-24 14:02:21 +01:00
Dan Brown
bb08f62327 Vectors: Finished core fetch & display functionality 2025-08-22 12:59:32 +01:00
Dan Brown
8eef5a1ee7 Vectors: Updated query response to use server-side-events
Allowing the vector query results and the LLM response to each come back
over the same HTTP request at two different times via a somewhat
standard.

Uses a package for JS SSE client, since native browser client does not
support over POST, which is probably important for this endpoint as we
don't want crawlers or other bots abusing this via accidentally.
2025-08-21 16:03:55 +01:00
Dan Brown
88ccd9e5b9 Vectors: Split out vector search and llm query runs
Added a formal object type to carry across vector search results.
Added permission application and entity combining with vector search
results.
Also updated namespace from vectors to queries.
2025-08-21 12:14:52 +01:00
Dan Brown
2c3100e401 Vectors: Started front-end work, moved to own controller 2025-08-19 15:19:04 +01:00
Dan Brown
54f883e815 Improved vector text chunking 2025-08-19 11:04:14 +01:00
Dan Brown
e611b3239e Vectors: Added command to regenerate for all
Also made models configurable.
Tested system scales via 86k vector entries.
2025-08-17 09:43:07 +01:00
Dan Brown
b9ecf55e1f Vectors: Got basic LLM querying working using vector search context 2025-08-17 09:43:07 +01:00
Dan Brown
2d5548240a Vectors: Built content vector indexing system 2025-08-17 09:43:00 +01:00
Dan Brown
5c481b4282 Testing: Added more deprecation output 2025-08-15 12:42:44 +01:00
Dan Brown
9443682ae4 Maintenance: Addressed a range of deprecations
Updated deps to address deprecations fixed in newer Laravel framework
version.
2025-08-15 12:20:35 +01:00
Dan Brown
0311e3d2d7 Readme: Updated sponsor link
Was leading to a 404.
2025-08-14 16:00:46 +01:00
Dan Brown
a50a256939 ZIP Exports: Fixed reference handling for images
Recent changes could mean missed references for images in non-page
locations. This fixes that, and tries to ensure images are used if we
already have a page-based image as part of the ZIP, otherwise ensure we
have a page as part of the export to attach the image to.
2025-08-11 14:19:48 +01:00
Dan Brown
4830248a1e Release: Updated licenses and translator attribution 2025-08-11 13:41:31 +01:00
Dan Brown
1256b30ad4 Updated translations with latest Crowdin changes (#5740) 2025-08-11 13:38:47 +01:00
Dan Brown
777cca76da Deps: Bumped PHP composer deps again 2025-08-11 13:36:06 +01:00
Dan Brown
a2d13124af Testing: Added mail port to testing env options
Prevents conflict with potential user-set option.
For #5755
2025-08-11 13:33:57 +01:00
Dan Brown
bd966ef99e phpstan: Address a range of level 2 issues 2025-08-09 11:09:50 +01:00
Dan Brown
a6b5733ec2 Deps: Updated PHP packages via composer 2025-08-09 10:12:24 +01:00
Dan Brown
e899066e96 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2025-08-08 17:44:40 +01:00
Dan Brown
f4f2435856 Imports: Fixed errors causing user logout on import run
Fixes #5754
2025-08-08 17:43:58 +01:00
Dan Brown
fca4a0563e Merge pull request #5753 from BookStackApp/a11y_menu_updates
A11y: Improved menu tagging
2025-08-08 17:00:07 +01:00
Dan Brown
0bc9ddd780 A11y: Updated other dropdown menus with correct tagging
Made some form improvements at the same time.
2025-08-07 16:37:18 +01:00
Dan Brown
c66f3b2a37 A11y: Improved tagging of profile menu
- Swapped toggle out to actual button.
- Ensured menu items have proper menu item role.
- Added extra roles/labels where is makes sense.
2025-08-07 14:32:20 +01:00
Dan Brown
f36e6fb929 Commands: Updated create admin skip return
Return status for skipped --initial creation will now return 2, so that
it can be identified seperate from a creation and from an error.
2025-08-07 13:16:49 +01:00
Dan Brown
7bc0d54af1 Readme: Swapped codeclimate reference for custom phpmetrics 2025-08-05 22:00:55 +01:00
Dan Brown
2eefbd21c1 Commands: Added testing for initial admin changes
- Also changed first-admin to initial.
- Updated initial handling to not require email/name to be passed, using
  defaults instead.
- Adds missing existing email use check.
2025-08-05 16:43:06 +01:00
Dan Brown
a961552c23 Commands: Updated create admin comment to accept extra flags
Added flags to target changes to the first default admin user, and to
generate a password.
This is related to #4575.
2025-08-05 13:39:30 +01:00
Dan Brown
776ec7b9e7 Updated translations with latest Crowdin changes (#5696) 2025-07-30 09:36:34 +01:00
Dan Brown
8aa6bdc8ab Updated translator & dependency attribution before release v25.07 2025-07-30 09:27:17 +01:00
Dan Brown
4ab17157b1 API: Added ZIP export endpoint comments 2025-07-30 09:13:58 +01:00
Dan Brown
6d7ffab115 Deps: Updated PHP composer dependancy versions, fixed test namespaces 2025-07-27 11:24:54 +01:00
Dan Brown
c8cfec96dc Merge pull request #5731 from BookStackApp/lexical_jul25
New WYSIWYG editor changes for July 2025
2025-07-26 10:08:44 +01:00
Dan Brown
d145efb6f6 Lexical: Updated tests after link changes 2025-07-25 14:25:02 +01:00
Dan Brown
c54101c603 Lexical: Updated URL handling, added mouse handling
- Removed URL protocol allow-list to allow any as per old editor.
- Added mouse handling, so that clicks below many last hard-to-escape
  block types will add an empty new paragraph for easy escaping &
  editing.
2025-07-25 13:58:48 +01:00
Dan Brown
865e5aecc9 Lexical: Source code input changes
- Increased default source code view size.
- Updated HTML generation to output each top-level block on its own
  line.
2025-07-24 17:24:59 +01:00
Dan Brown
ae4d1d804a Lexical: Table cell bg and format setting fixes
- Updated table cell background color setting to be stable by
  specifically using the background property over the general styles.
- Updated format shorcuts to be correct header levels as per old editor
  and format menu.
- Updated format changes to properly update UI afterwards.
2025-07-24 16:51:11 +01:00
Dan Brown
5fc19b0edf Lexical: Fixed highlight format action, changed label 2025-07-24 13:48:00 +01:00
Dan Brown
0a73b70b64 Merge pull request #5725 from BookStackApp/md_plaintext
MarkDown Editor: TypeScript Conversion & Plaintext Editor
2025-07-23 15:48:10 +01:00
Dan Brown
2668aae09b TypeScript: Updated compile target, addressed issues 2025-07-23 15:41:55 +01:00
Dan Brown
3b9c0b34ae MD Editor: Fixed plaintext dark styles, updated npm packages 2025-07-23 14:59:26 +01:00
Dan Brown
53f32849a9 MD Editor: Last tests/check over plaintext use/switching 2025-07-23 14:49:41 +01:00
Dan Brown
7ca8bdc231 MD Editor: Added custom textarea undo/redo, updated positioning methods 2025-07-23 12:17:36 +01:00
Dan Brown
6621d55f3d MD Editor: Worked to improve/fix positioning code
Still pending testing. Old logic did not work when lines would wrap, so
changing things to a character/line measuring technique.
Fixed some other isues too while testing shortcuts.
2025-07-22 16:42:47 +01:00
Dan Brown
d55db06c01 MD Editor: Added plaintext/cm switching
Also aligned the construction of the inputs where possible.
2025-07-22 10:34:29 +01:00
Dan Brown
6b4b500a33 MD Editor: Added plaintext input implementation 2025-07-21 18:53:22 +01:00
Dan Brown
5ffec2c52d MD Editor: Updated actions to use input interface 2025-07-21 14:24:51 +01:00
Dan Brown
ec07793cda MD Editor: Started work on input interface
Created implementation for codemirror, yet to use it.
2025-07-21 11:49:58 +01:00
Dan Brown
61adc735c8 MD Editor: Finished conversion to Typescript 2025-07-20 15:05:19 +01:00
Dan Brown
7bbf591a7f MD Editor: Starting conversion to typescript 2025-07-20 12:33:22 +01:00
Dan Brown
61f8d18af5 Changelog: Tweaked spacing, count and element referencing
During review of #5663
2025-07-19 14:53:02 +01:00
Dan Brown
f786d25f2e Merge branch 'enhance-changelog-textarea' of github.com:shresthkapoor7/BookStack into shresthkapoor7-enhance-changelog-textarea 2025-07-19 14:39:57 +01:00
Dan Brown
e62f4426ea Merge pull request #5721 from BookStackApp/zip_export_api_endpoints
API: ZIP Import/Export
2025-07-18 16:34:10 +01:00
Dan Brown
32ba3a591f ZIP Imports: Added API examples, finished testing
Also updated some types on a couple of controllers.
2025-07-18 16:19:14 +01:00
Dan Brown
73025719a4 ZIP Imports: Added API test cases 2025-07-18 14:05:32 +01:00
Dan Brown
d55684531f API: Added zip export tests, reorganised tests
Extracted an extra method into helper for reuse.
2025-07-18 10:58:10 +01:00
Dan Brown
d15eb129b0 API: Initial review pass of zip import/export endpoints
Review of #5592
2025-07-18 09:54:49 +01:00
Dan Brown
3626a2265b Merge branch 'development' of github.com:LM-Nishant/BookStack into LM-Nishant-development 2025-07-18 09:19:32 +01:00
Dan Brown
d13abc7e1d Mail: Removed custom symfony/mailer fork
Moved to standard symfony mailer now that my patches have been
upstreamed. This changes the config to work with the symfony option,
following the same overall logic.
Also updated testing to allow test runs via mulitple custom env options.

Closes #5636
2025-07-15 15:24:31 +01:00
Dan Brown
2442829ef2 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2025-07-14 14:18:51 +01:00
Dan Brown
795b28162a Readme: Added SiteSpeakAI sponsor 2025-07-14 14:18:24 +01:00
Dan Brown
31706ea06b Merge pull request #5689 from BookStackApp/permission_table_locking
Better parallel permission gen handling
2025-07-09 18:02:15 +01:00
Dan Brown
4b9e6042d5 Merge pull request #5676 from BookStackApp/lexical_comments
New WYSIWYG editor for comments & descriptions
2025-07-09 18:01:25 +01:00
Dan Brown
d279b0830b Merge pull request #5685 from BookStackApp/sidebar_rejig
Tri-layout sidebar enhancements
2025-07-09 18:00:56 +01:00
Dan Brown
181ab91b1d Merge pull request #5681 from BookStackApp/parent_tag_classes
Parent tag classes
2025-07-09 17:58:13 +01:00
Dan Brown
306f41b6f0 Updated translator & dependency attribution before release v25.05.2 2025-07-07 14:59:07 +01:00
Dan Brown
c1d76d2571 Updated translations with latest Crowdin changes (#5695) 2025-07-07 14:51:45 +01:00
Dan Brown
f83074d50e Languages: Added Nepali as a language option 2025-07-07 14:43:21 +01:00
Dan Brown
2be892be70 Updated translations with latest Crowdin changes (#5659) 2025-07-07 14:35:19 +01:00
Dan Brown
c934b9319f PHP: Updated composer packages
Main intent was to get latest ssddanbrown/htmldiff version so better
handle non-ascii languages.
2025-07-07 14:24:04 +01:00
Dan Brown
35a51197ce Perms: Fixed some issues made when adding transactions 2025-07-06 22:52:06 +01:00
Dan Brown
47fd578edb Perms: Added transactions around permission effecting actions 2025-07-02 22:25:59 +01:00
Dan Brown
add091305c Perms: Removed entity perm regen on general update
Should not be needed here as this is not directly used for information
which should impact permissions.
Been through uses to ensure that this is the case.
2025-07-02 12:15:25 +01:00
Dan Brown
3d017594a8 Opensearch: Fixed XML declaration when php short tags enabled
For #5673
2025-07-01 11:29:16 +01:00
Dan Brown
0dcb2ec78c Layout: Converted tri-layout component to ts 2025-06-30 15:36:27 +01:00
Dan Brown
9186e77d27 Layout: Added scroll fade to the sidebars 2025-06-30 14:10:48 +01:00
Dan Brown
6045aff33a Layout: Improved sidebar sizing, and dropdown consideration
- Updated tri-layout sidebars to have less padding and to avoid cutting
  off content when in single-sidebar mode.
- Updated dropdown handling to consider the parent scroll container when
  deciding to drop upwards, to help prevent cut-off.
2025-06-30 13:19:45 +01:00
Dan Brown
dca9765d5d Customization: Added parent tag classes
For #5217
2025-06-28 22:27:28 +01:00
Dan Brown
a37d0c57dc Tests: Updated comment test to account for new editor usage 2025-06-27 10:33:28 +01:00
Dan Brown
054475135a Lexical: Added some styling and tweaks for basic editors 2025-06-27 10:19:45 +01:00
Dan Brown
02a35b6db4 Lexical: Added new WYSIWYG to chapter/book/shelf descriptions 2025-06-26 11:00:17 +01:00
Dan Brown
b80992ca59 Comments: Switched to lexical editor
Required a lot of changes to provide at least a decent attempt at proper
editor teardown control.
Also updates HtmlDescriptionFilter and testing to address issue with bad
child iteration which could lead to missed items.
Renamed editor version from comments to basic as it'll also be used for
item descriptions.
2025-06-25 14:16:01 +01:00
Dan Brown
c606970e38 Lexical: Started comment implementation
Refactors some UI and toolbar code for better abstract use across editor
versions.
2025-06-24 17:47:53 +01:00
Dan Brown
dfeca246a0 Merge pull request #5668 from bumperbox/patch-1
CommentDisplayTest correct namespace
2025-06-23 11:57:57 +01:00
bumperbox
3476d83ecc CommentDisplayTest correct namespace
Class Entity\CommentDisplayTest located in ./tests/Entity/CommentDisplayTest.php does not comply with psr-4 autoloading standard (rule: Tests\ => ./tests). Skipping.
2025-06-23 09:31:39 +12:00
Shresth Kapoor
3617ab1540 Enhance changelog input to textarea with character counter 2025-06-18 20:10:20 -04:00
Dan Brown
c4839c783a Updated translator & dependency attribution before release v25.05.1 2025-06-17 15:29:12 +01:00
Dan Brown
a5751a584c Updated translations with latest Crowdin changes (#5637) 2025-06-17 15:16:25 +01:00
Dan Brown
f518a3be37 Search: Updated indexer to handle non-breaking-spaces
Related to #5640
2025-06-17 14:00:13 +01:00
Dan Brown
0208f066c5 Comments: Fixed update notification text
For #5642
2025-06-17 13:42:25 +01:00
Dan Brown
2d0461b63a Merge pull request #5653 from BookStackApp/v25-05-1-lexical
Lexical Fixes for v25.05.1
2025-06-17 13:36:55 +01:00
Dan Brown
b913ae703d Lexical: Media form improvements
- Allowed re-editing of existing embed HTML code.
- Handled "src" form field when video is using child source tags.
2025-06-15 20:00:28 +01:00
Dan Brown
1611b0399f Lexical: Added a media toolbar, improved toolbars and media selection
- Updated toolbars to auto-refresh ui if it attempts to update targeting
  a DOM element which no longer exists.
- Removed MediaNode dom specific click handling which was causing
  selection issues, and did not seem to be needed now.
2025-06-15 15:22:27 +01:00
Dan Brown
8d4b8ff4f3 Lexical: Fixed media resize handling
- Updating height/width setting to clear any inline CSS width/height
  rules which would override and prevent resizes showing. This was
  common when switching media from old editor.
  Added test to cover.
- Updated resizer to track node so that it is retained & displayed
  across node DOM changes, which was previously causing the
  resizer/focus to disappear.
2025-06-15 13:55:42 +01:00
Dan Brown
77a88618c2 Lexical: Fixed double-bold text, updated tests
Double bold was due to text field exporting wrapping the output in <b>
tags when the main tag would already be strong.
2025-06-14 14:50:10 +01:00
Dan Brown
8b062d4795 Lexical: Fixed strange paragraph formatting behaviour
Formatting was not persisted on empty paragraphs, and was instead based
upon last format encountered in selection.
This was due to overly-hasty removal of other formatting code, which
this got caught it.
Restored required parts from prior codebase.

Also updated inline format button active indicator to reflect formats
using the above, so correct buttons are shown as active even when just
in an empty paragraph.
2025-06-13 19:40:13 +01:00
Dan Brown
717b516341 Lexical: Made table resize handles more efficent & less buggy
Fine mouse movement and handles will now only be active when actually
within a table, otherwise less frequent mouseovers are used to track if
in/out a table.
Hides handles when out of a table, preventing a range of issues with
stray handles floating about.
2025-06-13 16:38:53 +01:00
Dan Brown
fda242d3da Lexical: Fixed tiny image resizer on image insert
Added specific focus on image insert, and updated resize handler to
watch for load events and toggle a resize once loaded.
2025-06-13 15:58:59 +01:00
Dan Brown
aac547934c Deps: Bumped composer php package versions 2025-06-13 15:28:11 +01:00
Dan Brown
5c9b90ea0d Merge branch 'development' of github.com:BookStackApp/BookStack into development 2025-05-31 12:36:21 +01:00
Dan Brown
074f193e2f Updated translation attribution and licenses before release 2025-05-31 12:35:47 +01:00
Dan Brown
7f2604c8e8 Updated translations with latest Crowdin changes (#5622) 2025-05-31 12:15:16 +01:00
Dan Brown
b71b2a4376 Cleanup: Updated deps, fixed test, update issue templates
Also removed some unused imports.
2025-05-31 12:11:00 +01:00
Dan Brown
68df43e5a8 Merge pull request #5627 from BookStackApp/lexical_20250525
Lexical Editor: Further fixes
2025-05-28 22:53:03 +01:00
Dan Brown
c5ca865723 Lexical: Updated WYSIWYG editor status from alpha to beta 2025-05-28 22:52:09 +01:00
Dan Brown
b862f12a50 Lexical: Further improvements to table selection and captions
- Fixed errors with selection and range handling due to captions
  existing.
- Updated TableNode change handling to update existing DOM instead of
  re-creating, which avoids breaking an attached selection helper.
  - To support, Added function to handle node change detection and apply
    relevant dom updates for common properties.
2025-05-28 22:47:39 +01:00
Dan Brown
b0f8b11054 Comments: Fixed tab focus change & button placement on form usage
Fixes issue of tabs jumping back to active comments when stopping a
reply to an archived comment.
Fixes button placement looking odd due to wrong location and differing
styles depending on interaction path.
2025-05-28 22:00:24 +01:00
Dan Brown
7650ebf2f9 Deps: Updated composer/npm packages, fixed test namespace 2025-05-27 15:53:46 +01:00
Dan Brown
d9ea52522e Lexical: Fixed issues with recent changes 2025-05-26 19:06:36 +01:00
Dan Brown
2e718c12e1 Lexical: Changed table esacpe handling
Avoids misuse of selectPrevious/Next as per prior commit which was then
causing problems elsewhere, and is probably best to avoid creation in
those select methods anyway.
2025-05-26 18:47:51 +01:00
Dan Brown
a43a1832f5 Lexical: Added image insert via image link paste
Specifically added to align with existing TinyMCE behaviour which was
used by some users based upon new editor feedback.
2025-05-26 18:02:53 +01:00
Dan Brown
c4f7368c1c Lexical: Fixed table column resizing changes not appearing
Also fixed some resizer zindex issues.
2025-05-26 15:19:11 +01:00
Dan Brown
2a32475541 Lexical: Made a range of selection improvements
Updated up/down handling to create where a selection candidate does not
exist, to apply to a wider scenario via the selectPrevious/Next methods.

Updated DOM selection change handling to identify single selections
within decorated nodes to select them in full, instead of losing
selection due to partial selection of their contents.

Updated table selection handling so that our colgroups are ignored for
internal selection focus handling.
2025-05-26 14:51:03 +01:00
Dan Brown
1243108e0f Lexical: Updated dropdown handling to match tinymce behaviour
Now toolbars stay open on mouse-out, and close on other toolbar open,
outside click or an accepted action.
To support:
- Added new system to track and manage open dropdowns.
- Added way for buttons to optionally emit events upon actions.
- Added way to listen for events.
- Used the above to control when dropdowns should hide on action, since
  some dont (like overflow containers and split dropdown buttons).
2025-05-25 16:28:42 +01:00
Dan Brown
3280919370 Lexical: Improved diagram selection and keyboard usage
Fixes issues where drawings could not be removed via backspace or
delete.
2025-05-25 13:21:13 +01:00
Dan Brown
d149b809b1 Merge pull request #5626 from BookStackApp/rubentalstra-development
Review of #5429, OIDC avatar fetching
2025-05-24 18:14:18 +01:00
Dan Brown
eb47e11916 Avatars: Added redirect handling image fetching
Up to 3 times.
Can be needed based upon testing with Auth0.
Should be fine as long as it's something clearly documented.
Added test to cover.
2025-05-24 18:07:25 +01:00
Dan Brown
9d6bc1ad4d Testing: Updated tests to account for recent page redirect changes 2025-05-24 16:47:01 +01:00
Dan Brown
30bf0ce632 OIDC: Updated avatar fetching to run on each login
But only where the user does not already have an avatar assigned.
This aligns with the LDAP avatar fetching logic.
2025-05-24 16:34:36 +01:00
Dan Brown
b64c9b31d5 OIDC: Added testing coverage for picture fetching 2025-05-24 14:36:36 +01:00
Dan Brown
f9dbbe5d70 OIDC: Updated picture fetch implementation during review
Review of #5429
2025-05-24 14:02:37 +01:00
Dan Brown
05f7f4cb17 Merge branch 'development' of github.com:rubentalstra/BookStack into rubentalstra-development 2025-05-24 13:28:23 +01:00
Dan Brown
454b152b95 Pages: Redirect user to view if they can't edit
For #5568
2025-05-24 12:05:17 +01:00
Dan Brown
b29fe5c46d Merge pull request #5625 from BookStackApp/avif_images
AVIF image support
2025-05-23 17:30:24 +01:00
Dan Brown
131ac29df4 Images: Added testing to cover animated avif handling 2025-05-23 17:19:34 +01:00
Dan Brown
3a9d18a6cd Images: Added base avif support
Includes handling for animated avif images like apng.
2025-05-23 16:12:03 +01:00
Dan Brown
59e2c5e52a Merge pull request #5607 from BookStackApp/system_info_endpoint
API: System info endpoint
2025-05-22 17:31:32 +01:00
Dan Brown
d29b14ebfd Merge pull request #5584 from BookStackApp/content_comments
Content Comments
2025-05-22 16:58:36 +01:00
Dan Brown
cdd446ac73 Updated translations with latest Crowdin changes (#5608) 2025-05-17 12:04:25 +01:00
Dan Brown
1dd1024eba Merge pull request #5609 from BookStackApp/5605-folder-permissions
Images: Updated local disk to have open dir perms
2025-05-17 11:49:44 +01:00
Dan Brown
752cfe2f67 CLI: Updated CLI with fixes
- Updated php packages
- Added escaping for mysql options
2025-05-17 11:47:33 +01:00
Dan Brown
25baaa8189 Deps: Updated composer packages 2025-05-17 11:40:58 +01:00
Dan Brown
d2d0331782 Readme: Replaced discord/mastodon links, reformatted badges 2025-05-14 23:15:46 +01:00
Dan Brown
8121418e18 Readme: Added phamos as sponsor 2025-05-14 22:35:59 +01:00
Dan Brown
5ab31a8191 Images: Updated local disk to have open dir perms
Closes #5605
2025-05-14 18:15:20 +01:00
Dan Brown
0e69ab1938 API: Added test to cover system info endpoint 2025-05-13 20:46:11 +01:00
Dan Brown
058007109e API: Added system read endpoint
Standardised logic for reading app version to its own static class.
2025-05-13 20:38:08 +01:00
Joshua Booth
b6110ed3cd Add settings for number of books/shelves that will be displayed per page 2025-05-13 20:17:36 +01:00
Dan Brown
32b29fcdfc Comments: Fixed pointer display, Fixed translation test 2025-05-13 12:03:15 +01:00
Dan Brown
8f92b6f21b Comments: Fixed a range of TS errors + other
- Migrated toolbox component to TS
- Aligned how custom event types are managed
- Fixed PHP use of content_ref where not provided
2025-05-12 15:31:55 +01:00
Dan Brown
62f78f1c6d Comments: Split tests, added extra archive/reference tests 2025-05-12 14:26:09 +01:00
Dan Brown
f8c0aaff03 Comments: Checked content/arhived comment styles in dark mode
Also added default non-clickable styles for scenarios for references
which don't have an active content link.
2025-05-09 14:17:04 +01:00
Dan Brown
a27df485bb Comments: Fixed display, added archive list support for editor toolbox 2025-05-09 12:14:28 +01:00
Dan Brown
3e99ce4098 Deps: Updated PHP packages
Mainly to update termwind which was causing issues for users on Arch
where a more recent libxml version was in use.
2025-05-08 15:53:25 +01:00
Dan Brown
ce1e20501c Updated translator & dependency attribution before release v25.02.3 2025-05-05 18:14:18 +01:00
Dan Brown
295532fa7a Deps: Updated PHP packages 2025-05-05 18:09:49 +01:00
Dan Brown
642ba668b1 Merge pull request #5601 from BookStackApp/file_permissions
Images: Changed how new image permissions are set
2025-05-05 12:54:40 +01:00
Dan Brown
4f36cdd757 Updated translations with latest Crowdin changes (#5566) 2025-05-05 12:24:12 +01:00
Dan Brown
8821844c4a Exports: Fixed CSS file BOM mark breaking CSS variables in exports
Adds a dummy CSS rule to break as the first rule, instead of our
:root variables.
Fixes #5576
2025-05-05 12:21:32 +01:00
Dan Brown
1262083fcf Images: Changed how new image permissions are set
Removed default public visibility for images at the driver level,
leaving only doing this as a specific action in the logic.
Added try/catch around permission setting so that
permission-incompatible environments won't fatally fail, but instead
log a warning.

Tested via a google cloud storage bucket FUSE mount, mounted under another
user but with open 777 permissions.

Related to #5269
2025-05-03 20:30:50 +01:00
Dan Brown
c82fa33210 Comments: Further range of content reference ux improvements
- Added reference indicator to comment create form.
  - Added remove action.
- Extracted reference text to translations.
- Changed reference hash to be text-based instead of HTML based.
- Added reference display for newly added comments.
- Handled reference marker delete on comment delete.
2025-05-01 17:22:12 +01:00
Dan Brown
15c79c38db Comments: Addressed a range of edge cases and ux issues for references
Handles only display and handling references when they're in the active
tab, while handling proper removal when made not visible.
2025-05-01 16:33:42 +01:00
Dan Brown
e7dcc2dcdf Comments: Moved to tab UI, Converted tabs component to ts 2025-04-30 17:42:09 +01:00
Dan Brown
099f6104d0 Comments: Started archive display, created mode for tree node 2025-04-28 20:09:18 +01:00
Dan Brown
8bdf948743 Comments: Added archive endpoints, messages, Js actions and tests 2025-04-28 15:37:09 +01:00
Dan Brown
e8f44186a8 Comments: Split out page comment reference logic to own component
Started support for editor view.
Moved comment elements to be added relative to content area instad of
specific target reference element.
Added relocating on screen size change.
2025-04-27 16:51:24 +01:00
Dan Brown
ecda4e1d6f Comments: Added reference marker to comments 2025-04-26 21:05:54 +01:00
nchoudhary@logicmines.in
64da80cbf4 added routes for zip export 2025-04-25 13:00:06 +05:30
nchoudhary@logicmines.in
5fa728f28a Develop functionality to import ZIP files. Create an API controller and define a route entry for handling the import process. Implement logic to read the list of files within the ZIP, process the directory structure, and automatically create associated pages, chapters, and books based on the ZIP file's contents. 2025-04-25 12:48:34 +05:30
nchoudhary@logicmines.in
c61ce8dee4 Implement functionality to export a book, along with its pages and chapters, as a ZIP file. 2025-04-25 12:45:09 +05:30
Dan Brown
f656a82fe7 Comments: Styled content comments & improved interaction 2025-04-24 13:21:23 +01:00
Dan Brown
5bfba281fc Comments: Started inline comment display windows 2025-04-21 14:04:41 +01:00
Dan Brown
18ede9bbd3 Comments: Added inline comment marker/highlight logic 2025-04-19 14:07:52 +01:00
Dan Brown
2e7544a865 Comments: Converted comment component to TS 2025-04-19 12:46:47 +01:00
Dan Brown
5e3c3ad634 Comments: Added back-end content reference handling
Also added archived property, to be added.
2025-04-18 21:13:49 +01:00
Dan Brown
add238fe9f Comments & Pointer: Converted components to typescript
Made changes for dom and translation services for easier usage
considering types.
trans_choice updated to allow default count replacement data as per
Laravel's default behaviour.
2025-04-18 20:42:56 +01:00
Dan Brown
8d159f77e4 Comments: Started logic for content references
Adds button for comments to pointer.
Adds logic to generate a content reference point.
2025-04-18 15:01:57 +01:00
Dan Brown
fa566f156a Updated translator & dependency attribution before release v25.02.2 2025-04-02 17:30:43 +01:00
Dan Brown
78a0a2f519 Merge pull request #5558 from BookStackApp/lexical_round3
Lexical Fixes: Round 3
2025-04-02 17:23:38 +01:00
Dan Brown
42cbd6adef Updated translations with latest Crowdin changes (#5537) 2025-04-02 17:19:34 +01:00
Dan Brown
6117349893 Deps: Updated composer packages 2025-04-02 15:30:31 +01:00
Dan Brown
1256320c72 Merge branch 'bernardo-campos/development' into development 2025-04-02 15:18:31 +01:00
Dan Brown
1ba0d26fdd Sort Rules: Updated name comparison to not ignore non-ascii chars
Related to #5550 and #5542
2025-04-02 15:17:17 +01:00
Dan Brown
802f69cf35 Comments: Fixed missing comment timestamps
Due to deleted code during Laravel 11 upgrade.
Added test to cover.
Closes #5555
2025-03-30 17:36:48 +01:00
Dan Brown
bb44334224 Lexical: Added tests to cover recent changes
Also updated list tests to new test process.
2025-03-28 18:29:00 +00:00
Dan Brown
9bfcadd95f Lexical: Improved navigation around images/media
- Added specific handling to move/insert-up/down on arrow press.
- Prevented resize overlay from interrupting image node focus.
2025-03-28 14:30:03 +00:00
Dan Brown
62c8eb3357 Lexical: Made list selections & intendting more reliable
- Added handling to not include parent of top-most list range selection
  so that it's not also changed while not visually part of the
  selection range.
- Fixed issue where list items could be left over after unnesting, due
  to empty checks/removals occuring before all child handling.
- Added node sorting, applied to list items during nest operations so
  that selection range remains reliable.
2025-03-27 17:49:48 +00:00
Dan Brown
c03e44124a Lexical: Fixed task list parsing
Updated list DOM parsing to properly consider task list format set by
other MD/WYSIWYG editors.
2025-03-27 14:56:32 +00:00
Dan Brown
5c6671b3bf Lexical: Fixed issues with content not saving
Found that saving via Ctrl+Enter did not save as logic to load editor
output into form was bypassed, which this fixes by ensuring submit
events are raised during for this shortcut.

Submit handling also gets a timeout added since, at least in FF,
requestSubmit did not re-submit a form while in a submit event.
2025-03-27 14:13:18 +00:00
Bernardo Campos
abe7467ae5 Fix issue BookStackApp#5542 Sorting by name 2025-03-23 12:29:29 -03:00
Dan Brown
0ec0913846 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2025-03-16 12:44:42 +00:00
Dan Brown
e980564fd6 Updated translator & dependency attribution before release v25.02.1 2025-03-16 12:44:29 +00:00
Dan Brown
8a9215ecad Updated translations with latest Crowdin changes (#5505) 2025-03-16 12:25:53 +00:00
Dan Brown
304a1d8f91 Dependancies: Updated PHP composer deps 2025-03-16 12:04:19 +00:00
Dan Brown
dfbc78947f Revisions: Hid changes link for oldest revision
Just as a UX improvement to help avoid confusion, as the whole content
will be changes for this revision.

For #5454
2025-03-16 12:00:54 +00:00
Dan Brown
4f5ad171ac Config: Updated DB host to handle ipv6
Can be set via the square bracket format.
For #5464
2025-03-15 20:32:57 +00:00
Dan Brown
94b1cffa2d System CLI: Updated with new version
As per https://codeberg.org/bookstack/system-cli/pulls/21
dev/checksums folder added to support this new system.

Related to #161
2025-03-11 23:52:01 +00:00
Dan Brown
13dae24cbe Testing: Fixed issues during pre-release testing
- Updated locale list
- Fixed new name sorting not being case insensitive
- Updated license test to account for changed deps
2025-02-26 14:19:03 +00:00
Dan Brown
6211d6bcfc Updated translations with latest Crowdin changes (#5409) 2025-02-26 13:51:51 +00:00
Dan Brown
a384599cfa Meta: Updated licenses and translation attribution pre v25.02 2025-02-26 13:44:56 +00:00
Dan Brown
dca14feaaa Sorting: Fixes during testing of sort rules
- Fixed name numeric sorting not working as expected due to bad
  comparison.
- Added name numeric desc operation option.
- Added test to ensure each operating has a comparison function.
2025-02-24 16:58:59 +00:00
Dan Brown
d7ccb3ce6a Sorting: Updated text for sort rules
Removes 'Set' wording and notes application to books on change.
2025-02-23 14:41:26 +00:00
Dan Brown
6548ea4a12 JS: Upated npm deps, upgraded eslint, new eslint config
Upgraded eslint to 11, removed incompatible airbnb config as part of
process. ESlint config now in its own file.
2025-02-23 11:55:09 +00:00
Dan Brown
c3a1fabbf0 Deps & Tests: Updated PHP deps, fixed test namespaces 2025-02-23 11:30:10 +00:00
Dan Brown
d2542d6265 Merge pull request #5491 from BookStackApp/deprecations
Addressing PHP 8.4 Deprecations
2025-02-23 11:23:35 +00:00
Dan Brown
0e343c408f Merge pull request #5463 from BookStackApp/v24-12
v24-12 branch changes
2025-02-23 11:22:12 +00:00
Dan Brown
5c78f8352e Styles: Fixed breakpoint overlap
Alters common breakpoint utilities to not overlap at breakpoints which
would cause broken layout at those points.
For #5396
2025-02-23 11:19:11 +00:00
Dan Brown
35b45a2b8d LDAP: Fixed php type error when no cn provided for user
Changes default fallback for name to first DN part, otherwise the whole
DN, rather than leave as null which was causing a type error.

For #5443
2025-02-20 13:06:49 +00:00
Dan Brown
5050719ea3 PHP: Updated DOMPDF version 2025-02-17 13:37:58 +00:00
Dan Brown
5508c171db PHP: Addressed 8.4 deprecations within app itself 2025-02-17 12:45:37 +00:00
Dan Brown
3b4d3430a5 Tests: Updated failing license test 2025-02-17 12:07:23 +00:00
Dan Brown
213a86e3c0 Merge pull request #5415 from BookStackApp/more_lexical_fixes
Further Lexical Fixes
2025-02-16 15:28:55 +00:00
Dan Brown
2b746425c9 Lexical: Fixed code in lists, removed extra old alignment code
Code in lists could throw error on parse due to inner <code> tag being
parsed but not actually used within a <pre>, so this updates the
importDOM to disregard childdren for code blocks.

This also improves the invariant implementation to not be so
dev/debugger based, and to include vars in the output.
2025-02-16 15:09:33 +00:00
Dan Brown
5c15f4add2 Translations: Fixed a couple of errors in sorting en words 2025-02-16 11:27:49 +00:00
Dan Brown
92ad81429f Merge pull request #5488 from BookStackApp/search_index_updates
Search index improvements
2025-02-14 19:39:08 +00:00
Dan Brown
f1b8e857bf Searching: Added test for guillemets
To cover #5475
2025-02-14 19:30:25 +00:00
Dan Brown
c291d27c19 Merge branch 'inv-hareesh/development' into search_index_updates 2025-02-14 19:25:59 +00:00
Dan Brown
f4449928f8 Searching: Added custom tokenizer that considers soft delimiters.
This changes indexing so that a.b now indexes as "a", "b" AND "a.b"
instead of just the first two, for periods and hypens, so terms
containing those characters can be searched within.

Adds hypens as a delimiter - #2095
2025-02-14 19:01:51 +00:00
Dan Brown
45a15b4792 Searching: Split out search tests into their own dir 2025-02-14 13:24:39 +00:00
Dan Brown
2291d78382 Merge pull request #5470 from Silverlan/patch-1
Fix incorrect condition for displaying new books section
2025-02-12 18:14:28 +00:00
Dan Brown
7901ca9e6b Meta: Updated dev version and sponsor link 2025-02-11 15:52:35 +00:00
Dan Brown
a7de251876 Merge pull request #5457 from BookStackApp/sort_sets
Sort rules
2025-02-11 15:41:19 +00:00
Dan Brown
7bd89316bc Sorting: Updated sort set command, Changed sort timestamp handling
- Renamed AssignSortSetCommand to AssignSortRuleCommand, updated
  contents and testing.
- Updated sorting operations to not update timestamps if only priority
  is changed.
2025-02-11 15:29:16 +00:00
Dan Brown
b9306a9029 Sorting: Renamed sort set to sort rule
Renamed based on feedback from Tim and Script on Discord.
Also fixed flaky test
2025-02-11 14:36:25 +00:00
Dan Brown
a208c46b62 Sorting: Covered sort set management with tests 2025-02-10 17:19:49 +00:00
Dan Brown
a65701294e Sorting: Split out test class, added book autosort tests
Just for test view, actual functionality of autosort on change still
needs to be tested.
2025-02-10 13:33:10 +00:00
Dan Brown
69683d50ec Sorting: Added tests to cover AssignSortSetCommand 2025-02-09 23:24:36 +00:00
Dan Brown
37d020c083 Sorting: Addded command to apply sort sets 2025-02-09 17:44:24 +00:00
Dan Brown
ec79517493 Sorting: Added auto sort option to book sort UI
Includes indicator on books added to sort operation.
2025-02-09 15:16:18 +00:00
inv-hareesh
d938565839 Fix search issue for words inside Guillemets (« ») without spaces 2025-02-07 08:59:36 +05:30
Dan Brown
ccd94684eb Sorting: Improved sort set display, delete, added action on edit
- Changes to a sort set will now auto-apply to assinged books (basic
  chunck through all on save).
- Added book count indicator to sort set list items.
- Deletion now has confirmation and auto-handling of assigned
  books/settings.
2025-02-06 14:58:08 +00:00
Dan Brown
103a8a8e8e Meta: Updated sponsor list, licence year and readme 2025-02-05 21:17:48 +00:00
Dan Brown
c13ce18837 Sorting: Added book autosort logic 2025-02-05 16:52:20 +00:00
Dan Brown
7093daa49d Sorting: Connected up default sort setting for books 2025-02-05 14:33:46 +00:00
Dan Brown
b897af2ed0 Sorting: Finished main sort set CRUD work 2025-02-04 20:11:35 +00:00
Dan Brown
d28278bba6 Sorting: Added sort set form manager UI JS
Extracted much code to be shared with the shelf books management UI
2025-02-04 15:14:22 +00:00
Silverlan
12cc2f0689 Fix incorrect condition for displaying new books section 2025-02-03 19:01:08 +01:00
Dan Brown
bf8a84a8b1 Sorting: Started sort set routes and form 2025-02-03 16:48:57 +00:00
Dan Brown
4f5f7c10b1 Thumbnails: Fixed thumnail orientation
Prevents double rotation caused from both our own orientation handling
upon that invervention was auto-applying since v3.

Fixes #5462
2025-01-31 21:29:38 +00:00
Dan Brown
a34023f715 Sorting: Added content misses from last commit, started settings 2025-01-30 17:49:19 +00:00
Dan Brown
b2ac3e0834 Sorting: Added SortSet model & migration 2025-01-29 17:34:07 +00:00
Dan Brown
5b0cb3dd50 Sorting: Extracted URL sort helper to own class
Was only used in one place, so didn't make sense to have extra global
helper clutter.
2025-01-29 17:02:34 +00:00
Dan Brown
ac0cd9995d Sorting: Reorganised book sort code to its own directory 2025-01-29 16:40:11 +00:00
Dan Brown
7e03a973d8 Lexical: Ran a deeper check on translation use 2025-01-27 16:40:41 +00:00
Dan Brown
d89a2fdb15 Lexical: Added media src conversions
Only actuall added YT in the end.
Google had changed URL scheme, and Vimeo seems to just be something else
now, can't really browse video pages like before.
2025-01-27 14:28:27 +00:00
Dan Brown
958b537a49 Lexical: Linked table form to have caption toggle option 2025-01-22 20:39:15 +00:00
Dan Brown
8a66365d48 Lexical: Added support for table caption nodes
Needs linking up to the table form still.
2025-01-22 12:54:13 +00:00
Talstra Ruben SRSNL
da82e70ca3 Add optional OIDC avatar fetching from the “picture” claim 2025-01-20 17:21:46 +01:00
Dan Brown
04cca77ae6 Lexical: Added color picker/indicator to form fields 2025-01-18 11:12:43 +00:00
Dan Brown
c091f67db3 Lexical: Added color format custom color select
Includes tracking of selected colors via localstorage for display.
2025-01-17 11:17:51 +00:00
Dan Brown
7f5fd16dc6 Lexical: Added some general test guidance
Just to help remember the general layout/methods that we've added to
make testing easier.
2025-01-15 14:31:09 +00:00
Dan Brown
0d1a237f81 Lexical: Fixed auto-link issue
Added extra test helper to check the editor state directly via string
notation access rather than juggling types/objects to access deep
properties.
2025-01-15 14:15:58 +00:00
Dan Brown
786a434c03 Merge pull request #5405 from BookStackApp/public_theme_files
Theme System: Public serving of files
2025-01-14 14:56:43 +00:00
Dan Brown
25c4f4b02b Themes: Documented public file serving 2025-01-14 14:53:10 +00:00
Dan Brown
481580be17 Themes: Added testing and better mime sniffing for public serving
Existing mime sniffer wasn't great at distinguishing between plaintext
file types, so added a custom extension based mapping for common web
formats that may be expected to be used with this.
2025-01-13 16:51:07 +00:00
Dan Brown
593645acfe Themes: Added route to serve public theme files
Allows files to be placed within a "public" folder within a theme
directory which the contents of will served by BookStack for access.

- Only "web safe" content-types are provided.
- A static 1 day cache time it set on served files.

For #3904
2025-01-13 14:34:44 +00:00
Dan Brown
b9751807e7 Merge pull request #5400 from BookStackApp/laravel11
Laravel 11 Upgrade
2025-01-13 13:27:32 +00:00
Dan Brown
ee88832f1a Updated translations with latest Crowdin changes (#5399) 2025-01-13 13:26:04 +00:00
Dan Brown
dbda82ef92 Framework: Re-add updated patched symfony-mailer
https://github.com/ssddanbrown/symfony-mailer/commit/e9de8dccd76a63fc23475016e6574da6f5f12a2
2025-01-11 15:05:10 +00:00
Dan Brown
ad8bc5fe21 Framework: Updated phpunit to 11, updated migration test php versions 2025-01-11 13:50:01 +00:00
Dan Brown
5bf75786c6 Framework: Fixed Laravel 11 upgrade test issues, updated phpstan
- Fixed failing tests due to Laravel 11 changes
- Updated phpstan to 3.x branch
- Removed some seemingly redundant comment code, which was triggering
  phpstan.
2025-01-11 13:22:49 +00:00
Dan Brown
cf9ccfcd5b Framework: Performed Laravel 11 upgrade guide steps
Performed a little code cleanups when observed along the way.
Tested not yet ran.
2025-01-11 11:14:49 +00:00
Dan Brown
5116d83d38 PHP: Updated min version to 8.2
PHPStan config not yet compatible, but should work after moving to Laravel
11, which would allow using larastan 3.x.
2025-01-09 16:46:13 +00:00
Dan Brown
33b46882f3 Updated translations with latest Crowdin changes (#5370) 2025-01-04 21:46:35 +00:00
Dan Brown
9a5c287470 Deps: Updated composer packages 2025-01-04 21:45:36 +00:00
Dan Brown
6effc6d262 Merge pull request #5379 from BookStackApp/better_cleanup
Export limits and cleanup
2025-01-04 21:05:45 +00:00
Dan Brown
ff6c5aaecb Markdown Editor: Fixed scroll jump on image upload
For #5384
2025-01-04 21:01:28 +00:00
Dan Brown
1ff2826678 Exports: Added rate limits for UI exports
Just as a measure to prevent potential abuse of these potentially
longer-running endpoints.
Adds test to cover for ZIP exports, but applied to all formats.
2025-01-01 15:42:59 +00:00
Dan Brown
7e31725d48 Exports: Improved PDF command temp file cleanup 2025-01-01 15:19:11 +00:00
Dan Brown
6d7ff59a89 ZIP Exports: Improved temp file tracking & clean-up 2024-12-31 15:13:50 +00:00
Dan Brown
980a684b14 Updated translator & dependency attribution before release v24.12 2024-12-23 11:53:35 +00:00
Dan Brown
d56eea9279 Locales: Updated locale list with new languages 2024-12-23 11:27:58 +00:00
Dan Brown
2be504e0d2 Updated translations with latest Crowdin changes (#5345) 2024-12-23 11:23:44 +00:00
Dan Brown
c84d999456 ZIP Exports: Prevent book child page drafts from being included
Added test to cover
2024-12-22 12:43:26 +00:00
Dan Brown
01825ddb93 Dependancies: Bumped up composer dep versions 2024-12-21 15:48:46 +00:00
Dan Brown
1f88bc2a59 Merge pull request #5365 from BookStackApp/lexical_fixes
Range of fixes/updates for the new Lexical based editor
2024-12-20 14:51:57 +00:00
Dan Brown
ebe2ca7faf Lexical: Added about button/view
Re-used existing route and moved tinymce help to its own different
route. Added test to cover.
Added new external-content block to support in editor UI.
2024-12-17 22:40:28 +00:00
Dan Brown
f4005a139b Lexical: Adjusted handling of child/sibling list items on nesting
Sibling/child items will now remain at the same visual level during
nesting/un-nested, so only the selected item level is visually altered.

Also added new model-based editor content matching system for tests.
2024-12-17 18:07:46 +00:00
Dan Brown
fca8f928a3 Lexical: Aligned new empty item behaviour for nested lists
- Makes enter on empty nested list item un-nest instead of just creating
  new list items.
- Also updated existing lists tests to use newer helper setup.
2024-12-17 16:52:14 +00:00
Dan Brown
ace8af077d Lexical: Improved list tab handling, Improved test utils
- Made tab work on empty list items
- Improved select preservation on single list item tab
- Altered test context creation for more standard testing
2024-12-17 14:44:10 +00:00
Dan Brown
e50cd33277 Lexical: Added testing for some added shortcuts
Also:
- Added svg loading support (dummy stub) for jest.
- Updated headless test case due to node changes.
- Split out editor change detected to where appropriate.
- Added functions to help with testing, like mocking our context.
2024-12-16 16:27:44 +00:00
Dan Brown
8486775edf Lexical: Added mulitple methods to escape details block
Enter on empty last line, or down on last empty line, will focus on the
next node after details, or created a new paragraph to focus on if
needed.
2024-12-16 14:30:06 +00:00
Dan Brown
5887322178 Lexical: Added details toolbar
Includes unwrap and toggle open actions.
2024-12-15 18:13:49 +00:00
Dan Brown
3f86937f74 Lexical: Made summary part of details node
To provide more control of the summary as part of details.
To support, added a way to ignore elements during import DOM, allowing
up to read summaries when parsing details without duplicate nodes
involved.
2024-12-15 17:12:54 +00:00
Dan Brown
2f119d3033 Lexical: Adjusted modals and content area for mobile sizes 2024-12-15 15:29:00 +00:00
Dan Brown
5f07f31c9f Lexical: Added mobile toolbar support
Adds dynamic and fixed (out of DOM order) positioning with location
adjustment depending on space.
Also adds smarter hiding to prevent disappearing when mouse leaves but
within the same space as the toggle.
2024-12-15 14:03:08 +00:00
Dan Brown
a71aa241ad Lexical: Added dark mode styles, fixed autolink range 2024-12-14 15:17:33 +00:00
Dan Brown
97b201f61f Lexical: Added auto links on enter/space 2024-12-14 12:35:13 +00:00
Dan Brown
a8ef820443 Users: Hid lanuage preference for guest user
Hiding since it's not really used, and may mislead on how to set default
app language (which should be done via env options).
Updated test to cover.

For #5356
2024-12-13 15:19:28 +00:00
Dan Brown
7e1a8e5ec6 API: Added cover to book/shelf list endpoints
Aligns with what we provide in the UI.
Added/updated tests to cover, and updated API examples.

For 5180.
2024-12-13 14:21:04 +00:00
Dan Brown
19ee1c9be7 Notifications: Logged errors and prevented them blocking user
Failed notification sends could block the user action, whereas it's
probably more important that the user action takes places uninteruupted
than showing an error screen for the user to debug.
Logs notification errors so issues can still be debugged by admins.

Closes #5315
2024-12-12 21:47:39 +00:00
Dan Brown
fcf0bf79a9 Attachments: Hid edit/delete controls where lacking permission
Added test to cover.
Also migrated related ajax-delete-row component to ts.

For #5323
2024-12-11 20:38:30 +00:00
Dan Brown
0ece664475 CI: Added php8.4 to CI suites, bumped action/os versions 2024-12-11 18:50:10 +00:00
Dan Brown
509af2463d Search Index: Fixed SQL error when indexing large pages
Due to hitting statement placeholder limits (typically 65k)
when inserting index terms for single page.

Added test to cover.
Also added skipped tests for tests we don't always want to run.
For #5322
2024-12-11 15:55:19 +00:00
Dan Brown
5632fef621 Auth: Added specific guards against guest account login
Hardened things to enforce the intent that the guest account should not
be used for logins.
Currently this would not be allowed due to empty set password, and no
password fields on user edit forms, but an error could occur if the
login was attempted.

This adds:
- Handling to show normal invalid user warning on login instead of a
  hash check error.
- Prevention of guest user via main login route, in the event that
  inventive workarounds would be used by admins to set a password for
  this account.
- Test for guest user login.
2024-12-11 14:22:48 +00:00
Dan Brown
8ec26e8083 SASS: Updated to use modules and address deprecations
Changes the name of our spacing variables due to the prefixing -/_
meaning private in the use of new "use" rather than include.

All now modular too, so all variables/mixins are accessed via their
package.

Also renamed variables file to vars for simpler/cleaner access/writing.

eg. '$-m' is now 'vars.$m'
2024-12-09 13:25:35 +00:00
Dan Brown
617b2edea0 JS: Updated packages, fixed lint issue
Left eslint as old due to eslint-config-airbnb-base not yet being
comptible.
Some SASS deprecations to solve.
2024-12-09 13:07:39 +00:00
Dan Brown
55d074f1a5 Attachment API: Fixed error when name not provided in update
Fixes #5353
2024-12-09 11:32:15 +00:00
Dan Brown
7e6f6af463 Merge pull request #5349 from BookStackApp/lexical_reorg
Lexical: Merge of custom nodes & re-organisation of codebase
2024-12-04 20:06:39 +00:00
Dan Brown
d00cf6e1ba Lexical: Updated tests for node changes 2024-12-04 20:03:05 +00:00
Dan Brown
9fdd100f2d Lexical: Reorganised custom node code into lexical codebase
Also cleaned up old unused imports.
2024-12-04 18:53:59 +00:00
Dan Brown
57d8449660 Lexical: Merged custom table node code 2024-12-03 20:08:33 +00:00
Dan Brown
ebd4604f21 Lexical: Merged list nodes 2024-12-03 19:03:52 +00:00
Dan Brown
36a4d79120 Lexical: Extracted & merged heading & quote nodes 2024-12-03 17:04:50 +00:00
Dan Brown
f3fa63a5ae Lexical: Merged custom paragraph node, removed old format/indent refs
Start of work to merge custom nodes into lexical, removing old unused
format/indent core logic while extending common block elements where
possible.
2024-12-03 16:24:49 +00:00
Dan Brown
5164375b18 Merge branch 'rashadkhan359/development' into development 2024-12-03 13:52:38 +00:00
Dan Brown
fec44452cb Search API: Updated handling of parent detail, added testing
Review of #5280.

- Removed additional non-needed loads which could ignore permissions.
- Updated new formatter method name to be more specific on use.
- Added test case to cover changes.
- Updated API examples to align parent id/info in info to be
  representative.
2024-12-03 13:51:46 +00:00
Dan Brown
18ab38a87b Merge branch 'fix/markdown-export' into development 2024-12-02 11:50:15 +00:00
Dan Brown
0f9957bc03 MD Exports: Added HTML description conversion
Also updated tests to cover checking description use/conversion.
Made during review of #5313
2024-12-02 11:46:56 +00:00
Dan Brown
80f258c3c5 Merge branch 'fix-ldap-display-name' into development 2024-12-01 18:44:23 +00:00
Dan Brown
90341e0e00 LDAP: Review and testing of mulitple-display-name attr support
Review of #5295
Added test to cover functionality.
Moved splitting from config to service.
2024-12-01 18:42:54 +00:00
Dan Brown
3298374113 Merge branch 'docker-simplify' into development 2024-12-01 16:10:22 +00:00
Dan Brown
227c5e155b Dev Docker: Fixed missing gd jpeg handling, forced migrations
Migrations run without force could fail startup in certain environment
conditions (when testing production env).
Also updated paths permission handling to update more needed locations.
2024-12-01 16:10:05 +00:00
Dan Brown
fdbbcf2b8a Merge branch 'portazips' into development 2024-12-01 13:06:43 +00:00
Dan Brown
0a07b0d162 Merge pull request #5259 from BookStackApp/typescript-conversions
Conversion of Services to TypeScript
2024-12-01 13:04:59 +00:00
Dan Brown
94165cc18f Updated translator & dependency attribution before release v24.10.2 2024-11-29 13:46:37 +00:00
Dan Brown
f5ecd51461 Updated translations with latest Crowdin changes (#5331) 2024-11-29 13:40:09 +00:00
Dan Brown
e9f906ce56 Attachments: Fixed full range request handling
We were not responsing with a range request, where the requested range
was for the full extent of content. This changes things to always
provide a range request, even for the full range.

Change made since our existing logic could cause problems in chromium
browsers.

Elseif statement removed as its was likley redundant based upon other
existing checks.
This also changes responses for requested ranges beyond content, but I
think that's technically correct looking at the spec (416 are for when
there are no overlapping request/response ranges at all).

Updated tests to cover.
For #5342
2024-11-29 13:19:55 +00:00
Dan Brown
4630f07282 Code: Set base codemirror line height
Prevents difference in line height between light/dark mode.
For #5146
2024-11-29 12:57:53 +00:00
Dan Brown
978acecdcf Merge branch 'oidc-content-type-issue' into development 2024-11-28 16:58:55 +00:00
Dan Brown
bc1f1d92e5 OIDC: Added extra userinfo content-type normalisation and test
During review of #5337
2024-11-28 16:58:06 +00:00
Dan Brown
415cd6a360 Includes: Workaround for PHP 8.3.14 bug
Changed DOMText creation to be done via document so its document
reference is correct to avoid a bug in PHP 8.3.14.
Ref: https://github.com/php/php-src/issues/16967

Fixes #5341
2024-11-28 16:30:59 +00:00
Dan Brown
68ce340741 Depenencies: Updated PHP packages 2024-11-28 16:25:01 +00:00
Dan Brown
bdca9fc1ce ZIP Exports: Changed the instance id mechanism
Adds an instance id via app settings.
2024-11-27 16:30:19 +00:00
Dan Brown
edb684c72c ZIP Exports: Updated format doc with advisories regarding html/md 2024-11-26 17:53:20 +00:00
Wes Biggs
17f7afe12d Updates the OIDC userinfo endpoint request to allow for a Content-Type response header with optional parameters, like application/json; charset=utf-8. This was causing an issue when integrating with [node-oidc-provider](https://github.com/panva/node-oidc-provider). 2024-11-26 11:21:20 -06:00
Dan Brown
0a182a45ba ZIP Exports: Added detection/handling of images with external storage
Added test to cover.
2024-11-26 15:59:39 +00:00
Dan Brown
95d62e7f57 ZIP Imports/Exports: Fixed some lint and test issues
- Updated test handling to create imports folder when required.
- Updated some tests to delete created import zip files.
2024-11-25 16:30:56 +00:00
Dan Brown
9ecc91929a ZIP Import & Exports: Addressed issues during testing
- Handled links to within-zip page images found in chapter/book
  descriptions; Added test to cover.
- Fixed session showing unrelated success on failed import.

Tested import file-create undo on failure as part of this testing.
2024-11-25 15:54:15 +00:00
Dan Brown
f79c6aef8d ZIP Imports: Updated import form to show loading indicator
And disable button after submit.
Added here because the import could take some time, so it's best to show
an indicator to the user to show that something is happening, and help
prevent duplicate submission or re-submit attempts.
2024-11-22 21:36:42 +00:00
Dan Brown
c0dff6d4a6 ZIP Imports: Added book content ordering to import preview 2024-11-22 21:03:04 +00:00
Dan Brown
59cfc087e1 ZIP Imports: Added image type validation/handling
Images were missing their extension after import since it was
(potentially) not part of the import data.
This adds validation via mime sniffing (to match normal image upload
checks) and also uses the same logic to sniff out a correct extension.

Added tests to cover.
Also fixed some existing tests around zip functionality.
2024-11-18 17:42:49 +00:00
Dan Brown
e2f6e50df4 ZIP Exports: Added ID checks and testing to validator 2024-11-18 15:53:21 +00:00
Dan Brown
c2c64e207f ZIP Imports: Covered import runner with further testing 2024-11-16 19:52:20 +00:00
Dan Brown
8645aeaa4a ZIP Imports: Started testing core import logic
Fixed image size handling, and lack of attachment reference replacements
during testing.
2024-11-16 16:12:45 +00:00
Dan Brown
7681e32dca ZIP Imports: Added high level import run tests 2024-11-16 13:57:41 +00:00
Dan Brown
b7476a9e7f ZIP Import: Finished base import process & error handling
Added file creation reverting and DB rollback on error.
Added error display on failed import.
Extracted likely shown import form/error text to translation files.
2024-11-14 15:59:15 +00:00
Dan Brown
306b8774c2 Updated translations with latest Crowdin changes (#5317)
* New translations common.php (Ukrainian)

* New translations entities.php (Ukrainian)

* New translations errors.php (Ukrainian)

* New translations activities.php (Czech)

* New translations entities.php (Czech)
2024-11-13 11:59:03 +00:00
Dan Brown
c40ab4147e Dependencies: Updated composer packages 2024-11-13 11:39:04 +00:00
Dan Brown
48c101aa7a ZIP Imports: Finished off core import logic 2024-11-11 15:06:46 +00:00
Dan Brown
378f0d595f ZIP Imports: Built out reference parsing/updating logic 2024-11-10 16:03:50 +00:00
czemu
f12946d581 ExportFormatter: Add book description and check for empty book and chapter descriptions in markdown export 2024-11-10 09:39:33 +01:00
Dan Brown
d13e4d2eef ZIP imports: Started actual import logic 2024-11-09 14:01:24 +00:00
Dan Brown
ac27e18933 Languages: Added Turkmen to locale manager 2024-11-08 13:46:57 +00:00
Dan Brown
e5a6ccc4d4 Translators: Updated before patch release 2024-11-08 13:31:21 +00:00
Dan Brown
e42cdbe8e0 Updated translations with latest Crowdin changes (#5250) 2024-11-08 13:29:21 +00:00
Dan Brown
a6ba8dd68f Testing: Improved reliability
- Added extra column/value check for page revision test for accuracy.
- Changed search sort test to use more reliable values.
  - Change due to database seeding somtimes generating values that
    proceeded the test value, expected to be first, in sort results.
2024-11-08 11:35:18 +00:00
Dan Brown
7017a1cae5 Update URL Command: Added revisions table support
For #5292
Added test to cover.
2024-11-08 11:22:30 +00:00
Dan Brown
8120278b8c PHP Deps: Bumped up minor versions 2024-11-08 10:41:25 +00:00
Dan Brown
73babcbfe3 Merge pull request #5312 from BookStackApp/system_cli_update
System CLI update
2024-11-07 17:22:08 +00:00
Dan Brown
45189d9517 System CLI: Updated to 126de5599c state 2024-11-07 17:10:35 +00:00
Dan Brown
7b84558ca1 ZIP Imports: Added parent and permission check pre-import 2024-11-05 15:41:58 +00:00
Dan Brown
92cfde495e ZIP Imports: Added full contents view to import display
Reduced import data will now be stored on the import itself, instead of
storing a set of totals.
2024-11-05 13:17:31 +00:00
Dan Brown
14578c2257 ZIP Imports: Added parent selector for page/chapter imports 2024-11-04 16:21:22 +00:00
Dan Brown
8f6f81948e ZIP Imports: Fleshed out continue page, Added testing 2024-11-03 17:28:18 +00:00
Dan Brown
c6109c7087 ZIP Imports: Added listing, show view, delete, activity 2024-11-03 14:13:05 +00:00
Dan Brown
8ea3855e02 ZIP Import: Added upload handling
Split attachment service storage work out so it can be shared.
2024-11-02 20:48:21 +00:00
Dan Brown
74fce9640e ZIP Import: Added model+migration, and reader class 2024-11-02 17:17:34 +00:00
Dan Brown
259aa829d4 ZIP Imports: Added validation message display, added testing
Testing covers main UI access, and main non-successfull import actions.
Started planning stored import model.
Extracted some text to language files.
2024-11-02 14:51:04 +00:00
Dan Brown
c4ec50d437 ZIP Exports: Got zip format validation functionally complete 2024-10-30 15:26:23 +00:00
Dan Brown
b50b7b667d ZIP Exports: Started import validation 2024-10-30 13:13:41 +00:00
Zero
fbeb2e23d4 fix deprecated syntax 2024-10-29 23:07:15 +08:00
Zero
4b60c03caa re-write Dockerfile 2024-10-29 23:06:50 +08:00
Dan Brown
a56a28fbb7 ZIP Exports: Built out initial import view
Added syles for non-custom, non-image file inputs.
Started planning out back-end handling.
2024-10-29 14:21:32 +00:00
Dan Brown
4051d5b803 ZIP Exports: Added new import permission
Also updated new route/view to new non-book-specific flow.
Also fixed down migration of old export permissions migration.
2024-10-29 12:11:51 +00:00
Matthieu Leboeuf
87242ce6cb Adapt tests with displayName array 2024-10-28 22:27:15 +01:00
Matthieu Leboeuf
72d9ffd8b4 Added support for concatenating multiple LDAP attributes in displayName 2024-10-28 22:14:30 +01:00
Rashad
f606711463 respective book and chapter structure added. 2024-10-27 22:50:20 +05:30
Dan Brown
d1f69feb4a ZIP Exports: Tested each type and model of export 2024-10-27 14:33:43 +00:00
Dan Brown
e4ca3bf132 Merge pull request #5291 from LordSimal/development
fix tests namespace definition
2024-10-27 09:54:11 +00:00
Kevin Pfeifer
7aaf866064 fix tests namespace definition 2024-10-26 13:24:49 +02:00
Dan Brown
484342f26a ZIP Exports: Added entity cross refs, Started export tests 2024-10-23 15:59:58 +01:00
Dan Brown
42ada66fdd ZIP Exports: Added core logic for books/chapters 2024-10-23 11:30:32 +01:00
Dan Brown
f732ef05d5 ZIP Exports: Reorganised files, added page md parsing 2024-10-23 10:48:26 +01:00
Dan Brown
4fb4fe0931 ZIP Exports: Added working image handling/inclusion 2024-10-21 13:59:15 +01:00
Dan Brown
06ffd8ee72 Zip Exports: Added attachment/image link resolving & JSON null handling 2024-10-21 12:13:41 +01:00
Rashad
90a8070518 Eager loading for titles 2024-10-21 03:01:33 +05:30
Rashad
3e656efb00 Added include func for search api 2024-10-21 02:42:49 +05:30
Dan Brown
7c39dd5cba ZIP Export: Started building link/ref handling 2024-10-20 19:56:56 +01:00
Dan Brown
21ccfa97dd ZIP Export: Expanded page & added base attachment handling 2024-10-19 15:41:07 +01:00
Dan Brown
bf0262d7d1 Testing: Split export tests into multiple files 2024-10-19 13:59:42 +01:00
Dan Brown
42b9700673 ZIP Exports: Finished up format doc, move files, started builder
Moved all existing export related app files into their new own dir.
2024-10-15 16:14:11 +01:00
Dan Brown
42bd07d733 ZIP Export: Continued expanding format doc types 2024-10-15 13:57:16 +01:00
Dan Brown
6f1c54d018 Users: Changed name validation to min:1 instead of 2
Would cause scenarios where users could be created with 1 char, but then
fail to update due to validation differences.
Added test to cover.
For #5263
2024-10-15 11:07:41 +01:00
Dan Brown
1930af91ce ZIP Export: Started types in format doc 2024-10-13 22:56:22 +01:00
Dan Brown
e088d09e47 ZIP Export: Started defining format 2024-10-13 14:18:23 +01:00
Dan Brown
209fa04752 TS: Converted dom and keyboard nav services 2024-10-11 21:55:51 +01:00
Dan Brown
f41c02cbd7 TS: Converted app file and animations service
Extracted functions out of app file during changes to clean up.
Altered animation function to use normal css prop names instead of JS
CSS prop names.
2024-10-11 15:19:19 +01:00
Dan Brown
4dc75bad05 Settings: Added test to cover setting category by view 2024-10-11 13:33:07 +01:00
Lachlan Tripolone
a3d0f7478f Move settings category layouts into their own view folder 2024-10-11 10:42:48 +11:00
Lachlan Tripolone
b9b5003239 Refactor SettingController to validate categies by existing view files 2024-10-11 10:40:38 +11:00
Dan Brown
2e8d6ce7d9 TS: Coverted util service 2024-10-10 12:03:24 +01:00
1378 changed files with 54466 additions and 19189 deletions

View File

@@ -26,6 +26,13 @@ DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Storage system to use
# By default files are stored on the local filesystem, with images being placed in
# public web space so they can be efficiently served directly by the web-server.
# For other options with different security levels & considerations, refer to:
# https://www.bookstackapp.com/docs/admin/upload-config/
STORAGE_TYPE=local
# Mail system to use
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp

View File

@@ -36,10 +36,14 @@ APP_LANG=en
# APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true
# Application timezone
# Used where dates are displayed such as on exported content.
# Application timezones
# The first option is used to determine what timezone is used for date storage.
# Leaving that as "UTC" is advised.
# The second option is used to set the timezone which will be used for date
# formatting and display. This defaults to the "APP_TIMEZONE" value.
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
APP_TIMEZONE=UTC
APP_DISPLAY_TIMEZONE=UTC
# Application theme
# Used to specific a themes/<APP_THEME> folder where BookStack UI
@@ -56,6 +60,7 @@ APP_PROXIES=null
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
# An ipv6 address can be used via the square bracket format ([::1]).
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database

View File

@@ -42,6 +42,7 @@ body:
label: Log Content
description: If the issue has produced an error, provide any [BookStack or server log](https://www.bookstackapp.com/docs/admin/debugging/) content below.
placeholder: Be sure to remove any confidential details in your logs
render: text
validations:
required: false
- type: textarea

View File

@@ -0,0 +1,9 @@
name: Blank Request (Maintainers Only)
description: For maintainers only - Start a blank request
body:
- type: markdown
attributes:
value: "**This blank request option is only for existing official maintainers of the project!** Please instead use a different request option. If you use this your issue will be closed off."
- type: textarea
attributes:
label: Description

View File

@@ -177,7 +177,7 @@ Alexander Predl (Harveyhase68) :: German
Rem (Rem9000) :: Dutch
Michał Stelmach (stelmach-web) :: Polish
arniom :: French
REMOVED_USER :: French; Dutch; Portuguese, Brazilian; Portuguese; Turkish;
REMOVED_USER :: French; German; Dutch; Portuguese, Brazilian; Portuguese; Turkish;
林祖年 (contagion) :: Chinese Traditional
Siamak Guodarzi (siamakgoudarzi88) :: Persian
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
@@ -222,7 +222,7 @@ SmokingCrop :: Dutch
Maciej Lebiest (Szwendacz) :: Polish
DiscordDigital :: German; German Informal
Gábor Marton (dodver) :: Hungarian
Jasell :: Swedish
Jakob Åsell (Jasell) :: Swedish
Ghost_chu (ghostchu) :: Chinese Simplified
Ravid Shachar (ravidshachar) :: Hebrew
Helga Guchshenskaya (guchshenskaya) :: Russian
@@ -438,7 +438,7 @@ javadataherian :: Persian
Ludo-code :: French
hollsten :: Swedish
Ngoc Lan Phung (lanpncz) :: Vietnamese
Worive :: Catalan
Worive :: Catalan; French
Илья Скаба (skabailya) :: Russian
Irjan Olsen (Irch) :: Norwegian Bokmal
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
@@ -449,3 +449,75 @@ Avishay Rapp (AvishayRapp) :: Hebrew
matthias4217 :: French
Berke BOYLU2 (berkeboylu2) :: Turkish
etwas7B :: German
Mohammed srhiri (m.sghiri20) :: Arabic
YongMin Kim (kym0118) :: Korean
Rivo Zängov (Eraser) :: Estonian
Francisco Rafael Fonseca (chicoraf) :: Portuguese, Brazilian
ИEØ_ΙΙØZ (NEO_IIOZ) :: Chinese Traditional
madnjpn (madnjpn.) :: Georgian
Ásgeir Shiny Ásgeirsson (AsgeirShiny) :: Icelandic
Mohammad Aftab Uddin (chirohorit) :: Bengali
Yannis Karlaftis (meliseus) :: Greek
felixxx :: German Informal
randi (randi65535) :: Korean
test65428 :: Greek
zeronell :: Chinese Simplified
julien Vinber (julienVinber) :: French
Hyunwoo Park (oksure) :: Korean
aram.rafeq.7 (aramrafeq2) :: Kurdish
Raphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian
yn (user99) :: Arabic
Pavel Zlatarov (pzlatarov) :: Bulgarian
ingelres :: French
mabdullah :: Arabic
Skrabák Csaba (kekcsi) :: Hungarian
Evert Meulie (Evert) :: Norwegian Bokmal
Jasper Backer (jasperb) :: Dutch
Alexandar Cavdarovski (ace.200112) :: Swedish
구닥다리TV (yjj8353) :: Korean
Onur Oskay (o.oskay) :: Turkish
Sébastien Merveille (SebastienMerv) :: French
Maxim Kouznetsov (masya.work) :: Hebrew
neodvisnost :: Slovenian
Soubi Agatsuma (bisouya) :: Hebrew
Ilya Shaulov (ishaulov) :: Russian
Konstantin Bobkov (b.konstantv) :: Russian
Ruben Sutter (rubensutter) :: German
jellium :: French
Qxlkdr :: Swedish
Hari (muhhari) :: Indonesian
仙君御 (xjy) :: Chinese Simplified
TapioM :: Finnish
lingb58 :: Chinese Traditional
Angel Pandey (angel-pandey) :: Nepali
Supriya Shrestha (supriyashrestha) :: Nepali
gprabhat :: Nepali
CellCat :: Chinese Simplified
Al Desrahim (aldesrahim) :: Indonesian
ahmad abbaspour (deshneh.dar.diss) :: Persian
Erjon K. (ekr) :: Albanian
LiZerui (iamzrli) :: Chinese Traditional
Ticker (ticker.com) :: Hebrew
CrazyComputer :: Chinese Simplified
Firr (FirrV) :: Russian
João Faro (FaroJoaoFaro) :: Portuguese
Danilo dos Santos Barbosa (bozochegou) :: Portuguese, Brazilian
Chris (furesoft) :: German
Silvia Isern (eiendragon) :: Catalan
Dennis Kron Pedersen (ahjdp) :: Danish
iamwhoiamwhoami :: Swedish
Grogui :: French
MrCharlesIII :: Arabic
David Olsen (dawin) :: Danish
ltnzr :: French
Frank Holler (holler.frank) :: German; German Informal
Korab Arifi (korabidev) :: Albanian
Petr Husák (petrhusak) :: Czech
Bernardo Maia (bernardo.bmaia2) :: Portuguese, Brazilian
Amr (amr3k) :: Arabic
Tahsin Ahmed (tahsinahmed2012) :: Bengali
bojan_che :: Serbian (Cyrillic)
setiawan setiawan (culture.setiawan) :: Indonesian
Donald Mac Kenzie (kiuman) :: Norwegian Bokmal
Gabriel Silver (GabrielBSilver) :: Hebrew
Tomas Darius Davainis (Tomasdd) :: Lithuanian

View File

@@ -11,9 +11,9 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -11,14 +11,14 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.1
php-version: 8.3
tools: phpcs
- name: Run formatting check

View File

@@ -13,12 +13,12 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
php: ['8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

View File

@@ -13,12 +13,12 @@ on:
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
runs-on: ubuntu-24.04
strategy:
matrix:
php: ['8.1', '8.2', '8.3']
php: ['8.2', '8.3', '8.4', '8.5']
steps:
- uses: actions/checkout@v1
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2

1
.gitignore vendored
View File

@@ -32,3 +32,4 @@ webpack-stats.json
phpstan.neon
esbuild-meta.json
.phpactor.json
/*.zip

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2024, Dan Brown and the BookStack Project contributors.
Copyright (c) 2015-2026, Dan Brown and the BookStack project contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -9,11 +9,9 @@ use Illuminate\Http\Request;
class OidcController extends Controller
{
protected OidcService $oidcService;
public function __construct(OidcService $oidcService)
{
$this->oidcService = $oidcService;
public function __construct(
protected OidcService $oidcService
) {
$this->middleware('guard:oidc');
}
@@ -30,7 +28,7 @@ class OidcController extends Controller
return redirect('/login');
}
session()->flash('oidc_state', $loginDetails['state']);
session()->put('oidc_state', time() . ':' . $loginDetails['state']);
return redirect($loginDetails['url']);
}
@@ -41,10 +39,16 @@ class OidcController extends Controller
*/
public function callback(Request $request)
{
$storedState = session()->pull('oidc_state');
$responseState = $request->query('state');
$splitState = explode(':', session()->pull('oidc_state', ':'), 2);
if (count($splitState) !== 2) {
$splitState = [null, null];
}
if ($storedState !== $responseState) {
[$storedStateTime, $storedState] = $splitState;
$threeMinutesAgo = time() - 3 * 60;
if (!$storedState || $storedState !== $responseState || intval($storedStateTime) < $threeMinutesAgo) {
$this->showErrorNotification(trans('errors.oidc_fail_authed', ['system' => config('oidc.name')]));
return redirect('/login');
@@ -62,7 +66,7 @@ class OidcController extends Controller
}
/**
* Log the user out then start the OIDC RP-initiated logout process.
* Log the user out, then start the OIDC RP-initiated logout process.
*/
public function logout()
{

View File

@@ -2,60 +2,26 @@
namespace BookStack\Access;
use BookStack\Users\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Database\Eloquent\Model;
class ExternalBaseUserProvider implements UserProvider
{
/**
* The user model.
*
* @var string
*/
protected $model;
/**
* LdapUserProvider constructor.
*/
public function __construct(string $model)
{
$this->model = $model;
}
/**
* Create a new instance of the model.
*
* @return Model
*/
public function createModel()
{
$class = '\\' . ltrim($this->model, '\\');
return new $class();
}
/**
* Retrieve a user by their unique identifier.
*
* @param mixed $identifier
*
* @return Authenticatable|null
*/
public function retrieveById($identifier)
public function retrieveById(mixed $identifier): ?Authenticatable
{
return $this->createModel()->newQuery()->find($identifier);
return User::query()->find($identifier);
}
/**
* Retrieve a user by their unique identifier and "remember me" token.
*
* @param mixed $identifier
* @param string $token
*
* @return Authenticatable|null
*/
public function retrieveByToken($identifier, $token)
public function retrieveByToken(mixed $identifier, $token): null
{
return null;
}
@@ -75,32 +41,25 @@ class ExternalBaseUserProvider implements UserProvider
/**
* Retrieve a user by the given credentials.
*
* @param array $credentials
*
* @return Authenticatable|null
*/
public function retrieveByCredentials(array $credentials)
public function retrieveByCredentials(array $credentials): ?Authenticatable
{
// Search current user base by looking up a uid
$model = $this->createModel();
return $model->newQuery()
return User::query()
->where('external_auth_id', $credentials['external_auth_id'])
->first();
}
/**
* Validate a user against the given credentials.
*
* @param Authenticatable $user
* @param array $credentials
*
* @return bool
*/
public function validateCredentials(Authenticatable $user, array $credentials)
public function validateCredentials(Authenticatable $user, array $credentials): bool
{
// Should be done in the guard.
return false;
}
public function rehashPasswordIfRequired(Authenticatable $user, #[\SensitiveParameter] array $credentials, bool $force = false)
{
// No action to perform, any passwords are external in the auth system
}
}

View File

@@ -3,23 +3,18 @@
namespace BookStack\Access\Guards;
/**
* Saml2 Session Guard.
* External Auth Session Guard.
*
* The saml2 login process is async in nature meaning it does not fit very well
* into the default laravel 'Guard' auth flow. Instead most of the logic is done
* via the Saml2 controller & Saml2Service. This class provides a safer, thin
* version of SessionGuard.
* The login process for external auth (SAML2/OIDC) is async in nature, meaning it does not fit very well
* into the default laravel 'Guard' auth flow. Instead, most of the logic is done via the relevant
* controller and services. This class provides a safer, thin version of SessionGuard.
*/
class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
{
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @return bool
*/
public function validate(array $credentials = [])
public function validate(array $credentials = []): bool
{
return false;
}
@@ -27,12 +22,9 @@ class AsyncExternalBaseSessionGuard extends ExternalBaseSessionGuard
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
*
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
public function attempt(array $credentials = [], $remember = false): bool
{
return false;
}

View File

@@ -4,7 +4,7 @@ namespace BookStack\Access\Guards;
use BookStack\Access\RegistrationService;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\StatefulGuard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Contracts\Session\Session;
@@ -24,43 +24,31 @@ class ExternalBaseSessionGuard implements StatefulGuard
* The name of the Guard. Typically "session".
*
* Corresponds to guard name in authentication configuration.
*
* @var string
*/
protected $name;
protected readonly string $name;
/**
* The user we last attempted to retrieve.
*
* @var \Illuminate\Contracts\Auth\Authenticatable
*/
protected $lastAttempted;
protected Authenticatable|null $lastAttempted;
/**
* The session used by the guard.
*
* @var \Illuminate\Contracts\Session\Session
*/
protected $session;
protected Session $session;
/**
* Indicates if the logout method has been called.
*
* @var bool
*/
protected $loggedOut = false;
protected bool $loggedOut = false;
/**
* Service to handle common registration actions.
*
* @var RegistrationService
*/
protected $registrationService;
protected RegistrationService $registrationService;
/**
* Create a new authentication guard.
*
* @return void
*/
public function __construct(string $name, UserProvider $provider, Session $session, RegistrationService $registrationService)
{
@@ -72,13 +60,11 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Get the currently authenticated user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function user()
public function user(): Authenticatable|null
{
if ($this->loggedOut) {
return;
return null;
}
// If we've already retrieved the user for the current request we can just
@@ -101,13 +87,11 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Get the ID for the currently authenticated user.
*
* @return int|null
*/
public function id()
public function id(): int|null
{
if ($this->loggedOut) {
return;
return null;
}
return $this->user()
@@ -117,12 +101,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Log a user into the application without sessions or cookies.
*
* @param array $credentials
*
* @return bool
*/
public function once(array $credentials = [])
public function once(array $credentials = []): bool
{
if ($this->validate($credentials)) {
$this->setUser($this->lastAttempted);
@@ -135,12 +115,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Log the given user ID into the application without sessions or cookies.
*
* @param mixed $id
*
* @return \Illuminate\Contracts\Auth\Authenticatable|false
*/
public function onceUsingId($id)
public function onceUsingId($id): Authenticatable|false
{
if (!is_null($user = $this->provider->retrieveById($id))) {
$this->setUser($user);
@@ -153,38 +129,26 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @return bool
*/
public function validate(array $credentials = [])
public function validate(array $credentials = []): bool
{
return false;
}
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
*
* @return bool
* @param bool $remember
*/
public function attempt(array $credentials = [], $remember = false)
public function attempt(array $credentials = [], $remember = false): bool
{
return false;
}
/**
* Log the given user ID into the application.
*
* @param mixed $id
* @param bool $remember
*
* @return \Illuminate\Contracts\Auth\Authenticatable|false
*/
public function loginUsingId($id, $remember = false)
public function loginUsingId(mixed $id, $remember = false): Authenticatable|false
{
// Always return false as to disable this method,
// Logins should route through LoginService.
@@ -194,12 +158,9 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Log a user into the application.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
* @param bool $remember
*
* @return void
* @param bool $remember
*/
public function login(AuthenticatableContract $user, $remember = false)
public function login(Authenticatable $user, $remember = false): void
{
$this->updateSession($user->getAuthIdentifier());
@@ -208,12 +169,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Update the session with the given ID.
*
* @param string $id
*
* @return void
*/
protected function updateSession($id)
protected function updateSession(string|int $id): void
{
$this->session->put($this->getName(), $id);
@@ -222,10 +179,8 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Log the user out of the application.
*
* @return void
*/
public function logout()
public function logout(): void
{
$this->clearUserDataFromStorage();
@@ -239,62 +194,48 @@ class ExternalBaseSessionGuard implements StatefulGuard
/**
* Remove the user data from the session and cookies.
*
* @return void
*/
protected function clearUserDataFromStorage()
protected function clearUserDataFromStorage(): void
{
$this->session->remove($this->getName());
}
/**
* Get the last user we attempted to authenticate.
*
* @return \Illuminate\Contracts\Auth\Authenticatable
*/
public function getLastAttempted()
public function getLastAttempted(): Authenticatable
{
return $this->lastAttempted;
}
/**
* Get a unique identifier for the auth session value.
*
* @return string
*/
public function getName()
public function getName(): string
{
return 'login_' . $this->name . '_' . sha1(static::class);
}
/**
* Determine if the user was authenticated via "remember me" cookie.
*
* @return bool
*/
public function viaRemember()
public function viaRemember(): bool
{
return false;
}
/**
* Return the currently cached user.
*
* @return \Illuminate\Contracts\Auth\Authenticatable|null
*/
public function getUser()
public function getUser(): Authenticatable|null
{
return $this->user;
}
/**
* Set the current user.
*
* @param \Illuminate\Contracts\Auth\Authenticatable $user
*
* @return $this
*/
public function setUser(AuthenticatableContract $user)
public function setUser(Authenticatable $user): self
{
$this->user = $user;

View File

@@ -35,13 +35,9 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
/**
* Validate a user's credentials.
*
* @param array $credentials
*
* @throws LdapException
*
* @return bool
*/
public function validate(array $credentials = [])
public function validate(array $credentials = []): bool
{
$userDetails = $this->ldapService->getUserDetails($credentials['username']);
@@ -57,16 +53,13 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
/**
* Attempt to authenticate a user using the given credentials.
*
* @param array $credentials
* @param bool $remember
*
* @throws LdapException*@throws \BookStack\Exceptions\JsonDebugException
* @throws LdapException
* @throws LoginAttemptException
* @throws JsonDebugException
*
* @return bool
*/
public function attempt(array $credentials = [], $remember = false)
public function attempt(array $credentials = [], $remember = false): bool
{
$username = $credentials['username'];
$userDetails = $this->ldapService->getUserDetails($username);

View File

@@ -54,7 +54,7 @@ class Ldap
*
* @return \LDAP\Result|array|false
*/
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null)
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = [])
{
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
}
@@ -66,7 +66,7 @@ class Ldap
*
* @return \LDAP\Result|array|false
*/
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = null)
public function read($ldapConnection, string $baseDn, string $filter, array $attributes = [])
{
return ldap_read($ldapConnection, $baseDn, $filter, $attributes);
}
@@ -87,7 +87,7 @@ class Ldap
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = null): array|false
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = []): array|false
{
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
@@ -99,7 +99,7 @@ class Ldap
*
* @param resource|\LDAP\Connection $ldapConnection
*/
public function bind($ldapConnection, string $bindRdn = null, string $bindPassword = null): bool
public function bind($ldapConnection, ?string $bindRdn = null, ?string $bindPassword = null): bool
{
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
}

View File

@@ -71,6 +71,26 @@ class LdapService
return $users[0];
}
/**
* Build the user display name from the (potentially multiple) attributes defined by the configuration.
*/
protected function getUserDisplayName(array $userDetails, array $displayNameAttrs, string $defaultValue): string
{
$displayNameParts = [];
foreach ($displayNameAttrs as $dnAttr) {
$dnComponent = $this->getUserResponseProperty($userDetails, $dnAttr, null);
if ($dnComponent) {
$displayNameParts[] = $dnComponent;
}
}
if (empty($displayNameParts)) {
return $defaultValue;
}
return implode(' ', $displayNameParts);
}
/**
* Get the details of a user from LDAP using the given username.
* User found via configurable user filter.
@@ -81,21 +101,25 @@ class LdapService
{
$idAttr = $this->config['id_attribute'];
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$displayNameAttrs = explode('|', $this->config['display_name_attribute']);
$thumbnailAttr = $this->config['thumbnail_attribute'];
$user = $this->getUserWithAttributes($userName, array_filter([
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
'cn', 'dn', $idAttr, $emailAttr, ...$displayNameAttrs, $thumbnailAttr,
]));
if (is_null($user)) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
$nameDefault = $this->getUserResponseProperty($user, 'cn', null);
if (is_null($nameDefault)) {
$nameDefault = ldap_explode_dn($user['dn'], 1)[0] ?? $user['dn'];
}
$formatted = [
'uid' => $this->getUserResponseProperty($user, $idAttr, $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'name' => $this->getUserDisplayName($user, $displayNameAttrs, $nameDefault),
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'avatar' => $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,

View File

@@ -5,9 +5,11 @@ namespace BookStack\Access;
use BookStack\Access\Mfa\MfaSession;
use BookStack\Activity\ActivityType;
use BookStack\Exceptions\LoginAttemptException;
use BookStack\Exceptions\LoginAttemptInvalidUserException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Facades\Activity;
use BookStack\Facades\Theme;
use BookStack\Permissions\Permission;
use BookStack\Theming\ThemeEvents;
use BookStack\Users\Models\User;
use Exception;
@@ -29,10 +31,14 @@ class LoginService
* a reason to (MFA or Unconfirmed Email).
* Returns a boolean to indicate the current login result.
*
* @throws StoppedAuthenticationException
* @throws StoppedAuthenticationException|LoginAttemptInvalidUserException
*/
public function login(User $user, string $method, bool $remember = false): void
{
if ($user->isGuest()) {
throw new LoginAttemptInvalidUserException('Login not allowed for guest user');
}
if ($this->awaitingEmailConfirmation($user) || $this->needsMfaVerification($user)) {
$this->setLastLoginAttemptedForUser($user, $method, $remember);
@@ -45,7 +51,7 @@ class LoginService
Theme::dispatch(ThemeEvents::AUTH_LOGIN, $method, $user);
// Authenticate on all session guards if a likely admin
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
if ($user->can(Permission::UsersManage) && $user->can(Permission::UserRolesManage)) {
$guards = ['standard', 'ldap', 'saml2', 'oidc'];
foreach ($guards as $guard) {
auth($guard)->login($user);
@@ -58,7 +64,7 @@ class LoginService
*
* @throws Exception
*/
public function reattemptLoginFor(User $user)
public function reattemptLoginFor(User $user): void
{
if ($user->id !== ($this->getLastLoginAttemptUser()->id ?? null)) {
throw new Exception('Login reattempt user does align with current session state');
@@ -90,7 +96,7 @@ class LoginService
{
$value = session()->get(self::LAST_LOGIN_ATTEMPTED_SESSION_KEY);
if (!$value) {
return ['user_id' => null, 'method' => null];
return ['user_id' => null, 'method' => null, 'remember' => false];
}
[$id, $method, $remember, $time] = explode(':', $value);
@@ -98,18 +104,18 @@ class LoginService
if ($time < $hourAgo) {
$this->clearLastLoginAttempted();
return ['user_id' => null, 'method' => null];
return ['user_id' => null, 'method' => null, 'remember' => false];
}
return ['user_id' => $id, 'method' => $method, 'remember' => boolval($remember)];
}
/**
* Set the last login attempted user.
* Set the last login-attempted user.
* Must be only used when credentials are correct and a login could be
* achieved but a secondary factor has stopped the login.
* achieved, but a secondary factor has stopped the login.
*/
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember)
protected function setLastLoginAttemptedForUser(User $user, string $method, bool $remember): void
{
session()->put(
self::LAST_LOGIN_ATTEMPTED_SESSION_KEY,
@@ -152,16 +158,40 @@ class LoginService
*/
public function attempt(array $credentials, string $method, bool $remember = false): bool
{
if ($this->areCredentialsForGuest($credentials)) {
return false;
}
$result = auth()->attempt($credentials, $remember);
if ($result) {
$user = auth()->user();
auth()->logout();
$this->login($user, $method, $remember);
try {
$this->login($user, $method, $remember);
} catch (LoginAttemptInvalidUserException $e) {
// Catch and return false for non-login accounts
// so it looks like a normal invalid login.
return false;
}
}
return $result;
}
/**
* Check if the given credentials are likely for the system guest account.
*/
protected function areCredentialsForGuest(array $credentials): bool
{
if (isset($credentials['email'])) {
return User::query()->where('email', '=', $credentials['email'])
->where('system_name', '=', 'public')
->exists();
}
return false;
}
/**
* Logs the current user out of the application.
* Returns an app post-redirect path.

View File

@@ -11,7 +11,6 @@ class MfaSession
*/
public function isRequiredForUser(User $user): bool
{
// TODO - Test both these cases
return $user->mfaValues()->exists() || $this->userRoleEnforcesMfa($user);
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Access\Mfa;
use BookStack\Users\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
/**
@@ -16,6 +17,8 @@ use Illuminate\Database\Eloquent\Model;
*/
class MfaValue extends Model
{
use HasFactory;
protected static $unguarded = true;
const METHOD_TOTP = 'totp';

View File

@@ -14,10 +14,9 @@ use PragmaRX\Google2FA\Support\Constants;
class TotpService
{
protected $google2fa;
public function __construct(Google2FA $google2fa)
{
public function __construct(
protected Google2FA $google2fa
) {
$this->google2fa = $google2fa;
// Use SHA1 as a default, Personal testing of other options in 2021 found
// many apps lack support for other algorithms yet still will scan
@@ -35,7 +34,7 @@ class TotpService
}
/**
* Generate a TOTP URL from secret key.
* Generate a TOTP URL from a secret key.
*/
public function generateUrl(string $secret, User $user): string
{

View File

@@ -11,6 +11,7 @@ use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Http\HttpRequestService;
use BookStack\Theming\ThemeEvents;
use BookStack\Uploads\UserAvatars;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@@ -26,7 +27,8 @@ class OidcService
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpRequestService $http,
protected GroupSyncService $groupService
protected GroupSyncService $groupService,
protected UserAvatars $userAvatars
) {
}
@@ -220,6 +222,10 @@ class OidcService
throw new OidcException($exception->getMessage());
}
if ($this->config()['fetch_avatar'] && !$user->avatar()->exists() && $userDetails->picture) {
$this->userAvatars->assignToUserFromUrl($user, $userDetails->picture);
}
if ($this->shouldSyncGroups()) {
$detachExisting = $this->config()['remove_from_groups'];
$this->groupService->syncUserWithFoundGroups($user, $userDetails->groups ?? [], $detachExisting);

View File

@@ -11,6 +11,7 @@ class OidcUserDetails
public ?string $email = null,
public ?string $name = null,
public ?array $groups = null,
public ?string $picture = null,
) {
}
@@ -40,15 +41,16 @@ class OidcUserDetails
$this->email = $claims->getClaim('email') ?? $this->email;
$this->name = static::getUserDisplayName($displayNameClaims, $claims) ?? $this->name;
$this->groups = static::getUserGroups($groupsClaim, $claims) ?? $this->groups;
$this->picture = static::getPicture($claims) ?: $this->picture;
}
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $token): string
protected static function getUserDisplayName(string $displayNameClaims, ProvidesClaims $claims): string
{
$displayNameClaimParts = explode('|', $displayNameClaims);
$displayName = [];
foreach ($displayNameClaimParts as $claim) {
$component = $token->getClaim(trim($claim)) ?? '';
$component = $claims->getClaim(trim($claim)) ?? '';
if ($component !== '') {
$displayName[] = $component;
}
@@ -57,13 +59,13 @@ class OidcUserDetails
return implode(' ', $displayName);
}
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $token): ?array
protected static function getUserGroups(string $groupsClaim, ProvidesClaims $claims): ?array
{
if (empty($groupsClaim)) {
return null;
}
$groupsList = Arr::get($token->getAllClaims(), $groupsClaim);
$groupsList = Arr::get($claims->getAllClaims(), $groupsClaim);
if (!is_array($groupsList)) {
return null;
}
@@ -72,4 +74,14 @@ class OidcUserDetails
return is_string($val);
}));
}
protected static function getPicture(ProvidesClaims $claims): ?string
{
$picture = $claims->getClaim('picture');
if (is_string($picture) && str_starts_with($picture, 'http')) {
return $picture;
}
return null;
}
}

View File

@@ -11,7 +11,9 @@ class OidcUserinfoResponse implements ProvidesClaims
public function __construct(ResponseInterface $response, string $issuer, array $keys)
{
$contentType = $response->getHeader('Content-Type')[0];
$contentTypeHeaderValue = $response->getHeader('Content-Type')[0] ?? '';
$contentType = strtolower(trim(explode(';', $contentTypeHeaderValue, 2)[0]));
if ($contentType === 'application/json') {
$this->claims = json_decode($response->getBody()->getContents(), true);
}

View File

@@ -51,7 +51,7 @@ class Saml2Service
* Returns the SAML2 request ID, and the URL to redirect the user to.
*
* @throws Error
* @returns array{url: string, id: ?string}
* @return array{url: string, id: ?string}
*/
public function logout(User $user): array
{

View File

@@ -5,18 +5,23 @@ namespace BookStack\Access;
use BookStack\Activity\Models\Loggable;
use BookStack\App\Model;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class SocialAccount.
*
* @property string $driver
* @property User $user
*/
class SocialAccount extends Model implements Loggable
{
protected $fillable = ['user_id', 'driver', 'driver_id', 'timestamps'];
use HasFactory;
public function user()
protected $fillable = ['user_id', 'driver', 'driver_id'];
/**
* @return BelongsTo<User, $this>
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}

View File

@@ -55,7 +55,7 @@ class SocialDriverManager
/**
* Gets the names of the active social drivers, keyed by driver id.
* @returns array<string, string>
* @return array<string, string>
*/
public function getActive(): array
{
@@ -92,7 +92,7 @@ class SocialDriverManager
string $driverName,
array $config,
string $socialiteHandler,
callable $configureForRedirect = null
?callable $configureForRedirect = null
) {
$this->validDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\MixedEntityListLoader;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Database\Eloquent\Relations\Relation;
class ActivityQueries
@@ -67,6 +68,7 @@ class ActivityQueries
$activity = $query->orderBy('created_at', 'desc')
->with(['loggable' => function (Relation $query) {
/** @var MorphTo<Entity, Activity> $query */
$query->withTrashed();
}, 'user.avatar'])
->skip($count * ($page - 1))

View File

@@ -67,6 +67,14 @@ class ActivityType
const WEBHOOK_UPDATE = 'webhook_update';
const WEBHOOK_DELETE = 'webhook_delete';
const IMPORT_CREATE = 'import_create';
const IMPORT_RUN = 'import_run';
const IMPORT_DELETE = 'import_delete';
const SORT_RULE_CREATE = 'sort_rule_create';
const SORT_RULE_UPDATE = 'sort_rule_update';
const SORT_RULE_DELETE = 'sort_rule_delete';
/**
* Get all the possible values.
*/

View File

@@ -4,8 +4,11 @@ namespace BookStack\Activity;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity as ActivityService;
use BookStack\Util\HtmlDescriptionFilter;
use Illuminate\Database\Eloquent\Builder;
class CommentRepo
{
@@ -17,11 +20,46 @@ class CommentRepo
return Comment::query()->findOrFail($id);
}
/**
* Get a comment by ID, ensuring it is visible to the user based upon access to the page
* which the comment is attached to.
*/
public function getVisibleById(int $id): Comment
{
return $this->getQueryForVisible()->findOrFail($id);
}
/**
* Start a query for comments visible to the user.
* @return Builder<Comment>
*/
public function getQueryForVisible(): Builder
{
return Comment::query()->scopes('visible');
}
/**
* Create a new comment on an entity.
*/
public function create(Entity $entity, string $html, ?int $parent_id): Comment
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
{
// Prevent comments being added to draft pages
if ($entity instanceof Page && $entity->draft) {
throw new \Exception(trans('errors.cannot_add_comment_to_draft'));
}
// Validate parent ID
if ($parentId !== null) {
$parentCommentExists = Comment::query()
->where('commentable_id', '=', $entity->id)
->where('commentable_type', '=', $entity->getMorphClass())
->where('local_id', '=', $parentId)
->exists();
if (!$parentCommentExists) {
$parentId = null;
}
}
$userId = user()->id;
$comment = new Comment();
@@ -29,12 +67,14 @@ class CommentRepo
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id;
$comment->parent_id = $parentId;
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
$entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
ActivityService::add(ActivityType::COMMENTED_ON, $entity);
$comment->refresh()->unsetRelations();
return $comment;
}
@@ -52,6 +92,45 @@ class CommentRepo
return $comment;
}
/**
* Archive an existing comment.
*/
public function archive(Comment $comment, bool $log = true): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
}
$comment->archived = true;
$comment->save();
if ($log) {
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
}
return $comment;
}
/**
* Un-archive an existing comment.
*/
public function unarchive(Comment $comment, bool $log = true): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
}
$comment->archived = false;
$comment->save();
if ($log) {
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
}
return $comment;
}
/**
* Delete a comment from the system.
*/

View File

@@ -4,6 +4,7 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\Models\Activity;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
class AuditLogApiController extends ApiController
{
@@ -16,8 +17,8 @@ class AuditLogApiController extends ApiController
*/
public function list()
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::UsersManage);
$query = Activity::query()->with(['user']);

View File

@@ -5,6 +5,8 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Sorting\SortUrl;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@@ -12,8 +14,8 @@ class AuditLogController extends Controller
{
public function index(Request $request)
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::UsersManage);
$sort = $request->get('sort', 'activity_date');
$order = $request->get('order', 'desc');
@@ -65,6 +67,7 @@ class AuditLogController extends Controller
'filters' => $filters,
'listOptions' => $listOptions,
'activityTypes' => $types,
'filterSortUrl' => new SortUrl('settings/audit', array_filter($request->except('page')))
]);
}
}

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
/**
* The comment data model has a 'local_id' property, which is a unique integer ID
* scoped to the page which the comment is on. The 'parent_id' is used for replies
* and refers to the 'local_id' of the parent comment on the same page, not the main
* globally unique 'id'.
*
* If you want to get all comments for a page in a tree-like structure, as reflected in
* the UI, then that is provided on pages-read API responses.
*/
class CommentApiController extends ApiController
{
protected array $rules = [
'create' => [
'page_id' => ['required', 'integer'],
'reply_to' => ['nullable', 'integer'],
'html' => ['required', 'string'],
'content_ref' => ['string'],
],
'update' => [
'html' => ['string'],
'archived' => ['boolean'],
]
];
public function __construct(
protected CommentRepo $commentRepo,
protected PageQueries $pageQueries,
) {
}
/**
* Get a listing of comments visible to the user.
*/
public function list(): JsonResponse
{
$query = $this->commentRepo->getQueryForVisible();
return $this->apiListingResponse($query, [
'id', 'commentable_id', 'commentable_type', 'parent_id', 'local_id', 'content_ref', 'created_by', 'updated_by', 'created_at', 'updated_at'
]);
}
/**
* Create a new comment on a page.
* If commenting as a reply to an existing comment, the 'reply_to' parameter
* should be provided, set to the 'local_id' of the comment being replied to.
*/
public function create(Request $request): JsonResponse
{
$this->checkPermission(Permission::CommentCreateAll);
$input = $this->validate($request, $this->rules()['create']);
$page = $this->pageQueries->findVisibleByIdOrFail($input['page_id']);
$comment = $this->commentRepo->create(
$page,
$input['html'],
$input['reply_to'] ?? null,
$input['content_ref'] ?? '',
);
return response()->json($comment);
}
/**
* Read the details of a single comment, along with its direct replies.
*/
public function read(string $id): JsonResponse
{
$comment = $this->commentRepo->getVisibleById(intval($id));
$comment->load('createdBy', 'updatedBy');
$replies = $this->commentRepo->getQueryForVisible()
->where('parent_id', '=', $comment->local_id)
->where('commentable_id', '=', $comment->commentable_id)
->where('commentable_type', '=', $comment->commentable_type)
->get();
/** @var Comment[] $toProcess */
$toProcess = [$comment, ...$replies];
foreach ($toProcess as $commentToProcess) {
$commentToProcess->setAttribute('html', $commentToProcess->safeHtml());
$commentToProcess->makeVisible('html');
}
$comment->setRelation('replies', $replies);
return response()->json($comment);
}
/**
* Update the content or archived status of an existing comment.
*
* Only provide a new archived status if needing to actively change the archive state.
* Only top-level comments (non-replies) can be archived or unarchived.
*/
public function update(Request $request, string $id): JsonResponse
{
$comment = $this->commentRepo->getVisibleById(intval($id));
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
$input = $this->validate($request, $this->rules()['update']);
$hasHtml = isset($input['html']);
if (isset($input['archived'])) {
if ($input['archived']) {
$this->commentRepo->archive($comment, !$hasHtml);
} else {
$this->commentRepo->unarchive($comment, !$hasHtml);
}
}
if ($hasHtml) {
$comment = $this->commentRepo->update($comment, $input['html']);
}
return response()->json($comment);
}
/**
* Delete a single comment from the system.
*/
public function delete(string $id): Response
{
$comment = $this->commentRepo->getVisibleById(intval($id));
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
$this->commentRepo->delete($comment);
return response('', 204);
}
}

View File

@@ -3,8 +3,11 @@
namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\CommentTreeNode;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -19,13 +22,14 @@ class CommentController extends Controller
/**
* Save a new comment for a Page.
*
* @throws ValidationException
* @throws ValidationException|\Exception
*/
public function savePageComment(Request $request, int $pageId)
{
$input = $this->validate($request, [
'html' => ['required', 'string'],
'parent_id' => ['nullable', 'integer'],
'content_ref' => ['string'],
]);
$page = $this->pageQueries->findVisibleById($pageId);
@@ -33,21 +37,14 @@ class CommentController extends Controller
return response('Not found', 404);
}
// Prevent adding comments to draft pages
if ($page->draft) {
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
}
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
$this->checkPermission(Permission::CommentCreateAll);
$contentRef = $input['content_ref'] ?? '';
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => [
'comment' => $comment,
'children' => [],
]
'branch' => new CommentTreeNode($comment, 0, []),
]);
}
@@ -63,8 +60,8 @@ class CommentController extends Controller
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
$this->checkOwnablePermission(Permission::CommentUpdate, $comment);
$comment = $this->commentRepo->update($comment, $input['html']);
@@ -74,13 +71,53 @@ class CommentController extends Controller
]);
}
/**
* Mark a comment as archived.
*/
public function archive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
$this->showPermissionError();
}
$this->commentRepo->archive($comment);
$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}
/**
* Unmark a comment as archived.
*/
public function unarchive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission(Permission::PageView, $comment->entity);
if (!userCan(Permission::CommentUpdate, $comment) && !userCan(Permission::CommentDelete, $comment)) {
$this->showPermissionError();
}
$this->commentRepo->unarchive($comment);
$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}
/**
* Delete a comment from the system.
*/
public function destroy(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('comment-delete', $comment);
$this->checkOwnablePermission(Permission::CommentDelete, $comment);
$this->commentRepo->delete($comment);

View File

@@ -5,13 +5,14 @@ namespace BookStack\Activity\Controllers;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Tools\MixedEntityRequestHelper;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
class WatchController extends Controller
{
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
{
$this->checkPermission('receive-notifications');
$this->checkPermission(Permission::ReceiveNotifications);
$this->preventGuestAccess();
$requestData = $this->validate($request, array_merge([

View File

@@ -6,6 +6,7 @@ use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Webhook;
use BookStack\Activity\Queries\WebhooksAllPaginatedAndSorted;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@@ -14,7 +15,7 @@ class WebhookController extends Controller
public function __construct()
{
$this->middleware([
'can:settings-manage',
Permission::SettingsManage->middleware()
]);
}

View File

@@ -6,6 +6,7 @@ use BookStack\App\Model;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -24,6 +25,8 @@ use Illuminate\Support\Str;
*/
class Activity extends Model
{
use HasFactory;
/**
* Get the loggable model related to this activity.
* Currently only used for entities (previously entity_[id/type] columns).

View File

@@ -3,47 +3,68 @@
namespace BookStack\Activity\Models;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\HasCreatorAndUpdater;
use BookStack\Users\Models\OwnableInterface;
use BookStack\Util\HtmlContentFilter;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
* @property string $text - Deprecated & now unused (#4821)
* @property string $html
* @property int|null $parent_id - Relates to local_id, not id
* @property int $local_id
* @property string $entity_type
* @property int $entity_id
* @property int $created_by
* @property int $updated_by
* @property string $commentable_type
* @property int $commentable_id
* @property string $content_ref
* @property bool $archived
*/
class Comment extends Model implements Loggable
class Comment extends Model implements Loggable, OwnableInterface
{
use HasFactory;
use HasCreatorAndUpdater;
protected $fillable = ['parent_id'];
protected $appends = ['created', 'updated'];
protected $hidden = ['html'];
protected $casts = [
'archived' => 'boolean',
];
/**
* Get the entity that this comment belongs to.
*/
public function entity(): MorphTo
{
return $this->morphTo('entity');
// We specifically define null here to avoid the different name (commentable)
// being used by Laravel eager loading instead of the method name, which it was doing
// in some scenarios like when deserialized when going through the queue system.
// So we instead specify the type and id column names to use.
// Related to:
// https://github.com/laravel/framework/pull/24815
// https://github.com/laravel/framework/issues/27342
// https://github.com/laravel/framework/issues/47953
// (and probably more)
// Ultimately, we could just align the method name to 'commentable' but that would be a potential
// breaking change and not really worthwhile in a patch due to the risk of creating extra problems.
return $this->morphTo(null, 'commentable_type', 'commentable_id');
}
/**
* Get the parent comment this is in reply to (if existing).
* @return BelongsTo<Comment, $this>
*/
public function parent(): BelongsTo
{
return $this->belongsTo(Comment::class, 'parent_id', 'local_id', 'parent')
->where('entity_type', '=', $this->entity_type)
->where('entity_id', '=', $this->entity_id);
->where('commentable_type', '=', $this->commentable_type)
->where('commentable_id', '=', $this->commentable_id);
}
/**
@@ -54,29 +75,29 @@ class Comment extends Model implements Loggable
return $this->updated_at->timestamp > $this->created_at->timestamp;
}
/**
* Get created date as a relative diff.
*/
public function getCreatedAttribute(): string
{
return $this->created_at->diffForHumans();
}
/**
* Get updated date as a relative diff.
*/
public function getUpdatedAttribute(): string
{
return $this->updated_at->diffForHumans();
}
public function logDescriptor(): string
{
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->commentable_type} (ID: {$this->commentable_id})";
}
public function safeHtml(): string
{
return HtmlContentFilter::removeScriptsFromHtmlString($this->html ?? '');
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'commentable_id')
->whereColumn('joint_permissions.entity_type', '=', 'comments.commentable_type');
}
/**
* Scope the query to just the comments visible to the user based upon the
* user visibility of what has been commented on.
*/
public function scopeVisible(Builder $query): Builder
{
return app()->make(PermissionApplicator::class)
->restrictEntityRelationQuery($query, 'comments', 'commentable_id', 'commentable_type');
}
}

View File

@@ -4,11 +4,14 @@ namespace BookStack\Activity\Models;
use BookStack\App\Model;
use BookStack\Permissions\Models\JointPermission;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
use HasFactory;
protected $fillable = ['user_id'];
/**

View File

@@ -0,0 +1,20 @@
<?php
namespace BookStack\Activity\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Carbon;
/**
* @property int $id
* @property string $mentionable_type
* @property int $mentionable_id
* @property int $from_user_id
* @property int $to_user_id
* @property Carbon $created_at
* @property Carbon $updated_at
*/
class MentionHistory extends Model
{
protected $table = 'mention_history';
}

View File

@@ -12,6 +12,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $id
* @property string $name
* @property string $value
* @property int $entity_id
* @property string $entity_type
* @property int $order
*/
class Tag extends Model

View File

@@ -5,6 +5,7 @@ namespace BookStack\Activity\Models;
use BookStack\Activity\WatchLevels;
use BookStack\Permissions\Models\JointPermission;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
@@ -20,6 +21,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
*/
class Watch extends Model
{
use HasFactory;
protected $guarded = [];
public function watchable(): MorphTo

View File

@@ -5,8 +5,10 @@ namespace BookStack\Activity\Notifications\Handlers;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
use BookStack\Entities\Models\Entity;
use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Users\Models\User;
use Illuminate\Support\Facades\Log;
abstract class BaseNotificationHandler implements NotificationHandler
{
@@ -18,6 +20,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
{
$users = User::query()->whereIn('id', array_unique($userIds))->get();
/** @var User $user */
foreach ($users as $user) {
// Prevent sending to the user that initiated the activity
if ($user->id === $initiator->id) {
@@ -25,7 +28,7 @@ abstract class BaseNotificationHandler implements NotificationHandler
}
// Prevent sending of the user does not have notification permissions
if (!$user->can('receive-notifications')) {
if (!$user->can(Permission::ReceiveNotifications)) {
continue;
}
@@ -36,7 +39,11 @@ abstract class BaseNotificationHandler implements NotificationHandler
}
// Send the notification
$user->notify(new $notification($detail, $initiator));
try {
$user->notify(new $notification($detail, $initiator));
} catch (\Exception $exception) {
Log::error("Failed to send email notification to user [id:{$user->id}] with error: {$exception->getMessage()}");
}
}
}
}

View File

@@ -27,7 +27,7 @@ class CommentCreationNotificationHandler extends BaseNotificationHandler
$watcherIds = $watchers->getWatcherUserIds();
// Page owner if user preferences allow
if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
if ($page->owned_by && !$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
$userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
if ($userNotificationPrefs->notifyOnOwnPageComments()) {
$watcherIds[] = $page->owned_by;
@@ -36,7 +36,7 @@ class CommentCreationNotificationHandler extends BaseNotificationHandler
// Parent comment creator if preferences allow
$parentComment = $detail->parent()->first();
if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
if ($parentComment && $parentComment->created_by && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
$parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
$watcherIds[] = $parentComment->created_by;

View File

@@ -0,0 +1,85 @@
<?php
namespace BookStack\Activity\Notifications\Handlers;
use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Comment;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Models\MentionHistory;
use BookStack\Activity\Notifications\Messages\CommentMentionNotification;
use BookStack\Activity\Tools\MentionParser;
use BookStack\Entities\Models\Page;
use BookStack\Settings\UserNotificationPreferences;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Carbon;
class CommentMentionNotificationHandler extends BaseNotificationHandler
{
public function handle(Activity $activity, Loggable|string $detail, User $user): void
{
if (!($detail instanceof Comment) || !($detail->entity instanceof Page)) {
throw new \InvalidArgumentException("Detail for comment mention notifications must be a comment on a page");
}
/** @var Page $page */
$page = $detail->entity;
$parser = new MentionParser();
$mentionedUserIds = $parser->parseUserIdsFromHtml($detail->html);
$realMentionedUsers = User::whereIn('id', $mentionedUserIds)->get();
$receivingNotifications = $realMentionedUsers->filter(function (User $user) {
$prefs = new UserNotificationPreferences($user);
return $prefs->notifyOnCommentMentions();
});
$receivingNotificationsUserIds = $receivingNotifications->pluck('id')->toArray();
$userMentionsToLog = $realMentionedUsers;
// When an edit, we check our history to see if we've already notified the user about this comment before
// so that we can filter them out to avoid double notifications.
if ($activity->type === ActivityType::COMMENT_UPDATE) {
$previouslyNotifiedUserIds = $this->getPreviouslyNotifiedUserIds($detail);
$receivingNotificationsUserIds = array_values(array_diff($receivingNotificationsUserIds, $previouslyNotifiedUserIds));
$userMentionsToLog = $userMentionsToLog->filter(function (User $user) use ($previouslyNotifiedUserIds) {
return !in_array($user->id, $previouslyNotifiedUserIds);
});
}
$this->logMentions($userMentionsToLog, $detail, $user);
$this->sendNotificationToUserIds(CommentMentionNotification::class, $receivingNotificationsUserIds, $user, $detail, $page);
}
/**
* @param Collection<User> $mentionedUsers
*/
protected function logMentions(Collection $mentionedUsers, Comment $comment, User $fromUser): void
{
$mentions = [];
$now = Carbon::now();
foreach ($mentionedUsers as $mentionedUser) {
$mentions[] = [
'mentionable_type' => $comment->getMorphClass(),
'mentionable_id' => $comment->id,
'from_user_id' => $fromUser->id,
'to_user_id' => $mentionedUser->id,
'created_at' => $now,
'updated_at' => $now,
];
}
MentionHistory::query()->insert($mentions);
}
protected function getPreviouslyNotifiedUserIds(Comment $comment): array
{
return MentionHistory::query()
->where('mentionable_id', $comment->id)
->where('mentionable_type', $comment->getMorphClass())
->pluck('to_user_id')
->toArray();
}
}

View File

@@ -20,7 +20,8 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
}
// Get last update from activity
// Get the last update from activity
/** @var ?Activity $lastUpdate */
$lastUpdate = $detail->activity()
->where('type', '=', ActivityType::PAGE_UPDATE)
->where('id', '!=', $activity->id)
@@ -38,8 +39,8 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
$watchers = new EntityWatchers($detail, WatchLevels::UPDATES);
$watcherIds = $watchers->getWatcherUserIds();
// Add page owner if preferences allow
if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
// Add the page owner if preferences allow
if ($detail->owned_by && !$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
$userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
if ($userNotificationPrefs->notifyOnOwnPageChanges()) {
$watcherIds[] = $detail->owned_by;

View File

@@ -0,0 +1,37 @@
<?php
namespace BookStack\Activity\Notifications\Messages;
use BookStack\Activity\Models\Comment;
use BookStack\Activity\Notifications\MessageParts\EntityLinkMessageLine;
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
use BookStack\Entities\Models\Page;
use BookStack\Users\Models\User;
use Illuminate\Notifications\Messages\MailMessage;
class CommentMentionNotification extends BaseActivityNotification
{
public function toMail(User $notifiable): MailMessage
{
/** @var Comment $comment */
$comment = $this->detail;
/** @var Page $page */
$page = $comment->entity;
$locale = $notifiable->getLocale();
$listLines = array_filter([
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
$locale->trans('notifications.detail_commenter') => $this->user->name,
$locale->trans('notifications.detail_comment') => strip_tags($comment->html),
]);
return $this->newMailMessage($locale)
->subject($locale->trans('notifications.comment_mention_subject', ['pageName' => $page->getShortName()]))
->line($locale->trans('notifications.comment_mention_intro', ['appName' => setting('app-name')]))
->line(new ListMessageLine($listLines))
->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))
->line($this->buildReasonFooterLine($locale));
}
}

View File

@@ -6,6 +6,7 @@ use BookStack\Activity\ActivityType;
use BookStack\Activity\Models\Activity;
use BookStack\Activity\Models\Loggable;
use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
use BookStack\Activity\Notifications\Handlers\CommentMentionNotificationHandler;
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
@@ -48,5 +49,7 @@ class NotificationManager
$this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);
$this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentMentionNotificationHandler::class);
$this->registerHandler(ActivityType::COMMENT_UPDATE, CommentMentionNotificationHandler::class);
}
}

View File

@@ -4,14 +4,20 @@ namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Permission;
class CommentTree
{
/**
* The built nested tree structure array.
* @var array{comment: Comment, depth: int, children: array}[]
* @var CommentTreeNode[]
*/
protected array $tree;
/**
* A linear array of loaded comments.
* @var Comment[]
*/
protected array $comments;
public function __construct(
@@ -28,7 +34,7 @@ class CommentTree
public function empty(): bool
{
return count($this->tree) === 0;
return count($this->getActive()) === 0;
}
public function count(): int
@@ -36,15 +42,41 @@ class CommentTree
return count($this->comments);
}
public function get(): array
public function getActive(): array
{
return $this->tree;
return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived));
}
public function activeThreadCount(): int
{
return count($this->getActive());
}
public function getArchived(): array
{
return array_values(array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived));
}
public function archivedThreadCount(): int
{
return count($this->getArchived());
}
public function getCommentNodeForId(int $commentId): ?CommentTreeNode
{
foreach ($this->tree as $node) {
if ($node->comment->id === $commentId) {
return $node;
}
}
return null;
}
public function canUpdateAny(): bool
{
foreach ($this->comments as $comment) {
if (userCan('comment-update', $comment)) {
if (userCan(Permission::CommentUpdate, $comment)) {
return true;
}
}
@@ -52,8 +84,17 @@ class CommentTree
return false;
}
public function loadVisibleHtml(): void
{
foreach ($this->comments as $comment) {
$comment->setAttribute('html', $comment->safeHtml());
$comment->makeVisible('html');
}
}
/**
* @param Comment[] $comments
* @return CommentTreeNode[]
*/
protected function createTree(array $comments): array
{
@@ -77,28 +118,27 @@ class CommentTree
$tree = [];
foreach ($childMap[0] ?? [] as $childId) {
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
}
return $tree;
}
protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
{
$childIds = $childMap[$id] ?? [];
$children = [];
foreach ($childIds as $childId) {
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
}
return [
'comment' => $byId[$id],
'depth' => $depth,
'children' => $children,
];
return new CommentTreeNode($byId[$id], $depth, $children);
}
/**
* @return Comment[]
*/
protected function loadComments(): array
{
if (!$this->enabled()) {

View File

@@ -0,0 +1,23 @@
<?php
namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Comment;
class CommentTreeNode
{
public Comment $comment;
public int $depth;
/**
* @var CommentTreeNode[]
*/
public array $children;
public function __construct(Comment $comment, int $depth, array $children)
{
$this->comment = $comment;
$this->depth = $depth;
$this->children = $children;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace BookStack\Activity\Tools;
use BookStack\Util\HtmlDocument;
use DOMElement;
class MentionParser
{
public function parseUserIdsFromHtml(string $html): array
{
$doc = new HtmlDocument($html);
$ids = [];
$mentionLinks = $doc->queryXPath('//a[@data-mention-user-id]');
foreach ($mentionLinks as $link) {
if ($link instanceof DOMElement) {
$id = intval($link->getAttribute('data-mention-user-id'));
if ($id > 0) {
$ids[] = $id;
}
}
}
return array_values(array_unique($ids));
}
}

View File

@@ -3,17 +3,16 @@
namespace BookStack\Activity\Tools;
use BookStack\Activity\Models\Tag;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Permission;
class TagClassGenerator
{
protected array $tags;
/**
* @param Tag[] $tags
*/
public function __construct(array $tags)
{
$this->tags = $tags;
public function __construct(
protected Entity $entity
) {
}
/**
@@ -22,14 +21,23 @@ class TagClassGenerator
public function generate(): array
{
$classes = [];
$tags = $this->entity->tags->all();
foreach ($this->tags as $tag) {
$name = $this->normalizeTagClassString($tag->name);
$value = $this->normalizeTagClassString($tag->value);
$classes[] = 'tag-name-' . $name;
if ($value) {
$classes[] = 'tag-value-' . $value;
$classes[] = 'tag-pair-' . $name . '-' . $value;
foreach ($tags as $tag) {
array_push($classes, ...$this->generateClassesForTag($tag));
}
if ($this->entity instanceof BookChild && userCan(Permission::BookView, $this->entity->book)) {
$bookTags = $this->entity->book->tags;
foreach ($bookTags as $bookTag) {
array_push($classes, ...$this->generateClassesForTag($bookTag, 'book-'));
}
}
if ($this->entity instanceof Page && $this->entity->chapter && userCan(Permission::ChapterView, $this->entity->chapter)) {
$chapterTags = $this->entity->chapter->tags;
foreach ($chapterTags as $chapterTag) {
array_push($classes, ...$this->generateClassesForTag($chapterTag, 'chapter-'));
}
}
@@ -41,6 +49,22 @@ class TagClassGenerator
return implode(' ', $this->generate());
}
/**
* @return string[]
*/
protected function generateClassesForTag(Tag $tag, string $prefix = ''): array
{
$classes = [];
$name = $this->normalizeTagClassString($tag->name);
$value = $this->normalizeTagClassString($tag->value);
$classes[] = "{$prefix}tag-name-{$name}";
if ($value) {
$classes[] = "{$prefix}tag-value-{$value}";
$classes[] = "{$prefix}tag-pair-{$name}-{$value}";
}
return $classes;
}
protected function normalizeTagClassString(string $value): string
{
$value = str_replace(' ', '', strtolower($value));

View File

@@ -7,6 +7,7 @@ use BookStack\Activity\WatchLevels;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Permissions\Permission;
use BookStack\Users\Models\User;
use Illuminate\Database\Eloquent\Builder;
@@ -22,7 +23,7 @@ class UserEntityWatchOptions
public function canWatch(): bool
{
return $this->user->can('receive-notifications') && !$this->user->isGuest();
return $this->user->can(Permission::ReceiveNotifications) && !$this->user->isGuest();
}
public function getWatchLevel(): string

View File

@@ -50,7 +50,7 @@ class WebhookFormatter
}
if ($this->detail instanceof Model) {
$data['related_item'] = $this->formatModel();
$data['related_item'] = $this->formatModel($this->detail);
}
return $data;
@@ -83,10 +83,8 @@ class WebhookFormatter
);
}
protected function formatModel(): array
protected function formatModel(Model $model): array
{
/** @var Model $model */
$model = $this->detail;
$model->unsetRelations();
foreach ($this->modelFormatters as $formatter) {

View File

@@ -36,7 +36,7 @@ class WatchLevels
/**
* Get all the possible values as an option_name => value array.
* @returns array<string, int>
* @return array<string, int>
*/
public static function all(): array
{
@@ -50,7 +50,7 @@ class WatchLevels
/**
* Get the watch options suited for the given entity.
* @returns array<string, int>
* @return array<string, int>
*/
public static function allSuitedFor(Entity $entity): array
{

View File

@@ -2,6 +2,7 @@
namespace BookStack\Api;
use BookStack\App\AppVersion;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
@@ -25,7 +26,7 @@ class ApiDocsGenerator
*/
public static function generateConsideringCache(): Collection
{
$appVersion = trim(file_get_contents(base_path('version')));
$appVersion = AppVersion::get();
$cacheKey = 'api-docs::' . $appVersion;
$isProduction = config('app.env') === 'production';
$cacheVal = $isProduction ? Cache::get($cacheKey) : null;
@@ -82,11 +83,19 @@ class ApiDocsGenerator
protected function loadDetailsFromControllers(Collection $routes): Collection
{
return $routes->map(function (array $route) {
$class = $this->getReflectionClass($route['controller']);
$method = $this->getReflectionMethod($route['controller'], $route['controller_method']);
$comment = $method->getDocComment();
$route['description'] = $comment ? $this->parseDescriptionFromMethodComment($comment) : null;
$route['description'] = $comment ? $this->parseDescriptionFromDocBlockComment($comment) : null;
$route['body_params'] = $this->getBodyParamsFromClass($route['controller'], $route['controller_method']);
// Load class description for the model
// Not ideal to have it here on each route, but adding it in a more structured manner would break
// docs resulting JSON format and therefore be an API break.
// Save refactoring for a more significant set of changes.
$classComment = $class->getDocComment();
$route['model_description'] = $classComment ? $this->parseDescriptionFromDocBlockComment($classComment) : null;
return $route;
});
}
@@ -139,7 +148,7 @@ class ApiDocsGenerator
/**
* Parse out the description text from a class method comment.
*/
protected function parseDescriptionFromMethodComment(string $comment): string
protected function parseDescriptionFromDocBlockComment(string $comment): string
{
$matches = [];
preg_match_all('/^\s*?\*\s?($|((?![\/@\s]).*?))$/m', $comment, $matches);
@@ -154,6 +163,16 @@ class ApiDocsGenerator
* @throws ReflectionException
*/
protected function getReflectionMethod(string $className, string $methodName): ReflectionMethod
{
return $this->getReflectionClass($className)->getMethod($methodName);
}
/**
* Get a reflection class from the given class name.
*
* @throws ReflectionException
*/
protected function getReflectionClass(string $className): ReflectionClass
{
$class = $this->reflectionClasses[$className] ?? null;
if ($class === null) {
@@ -161,7 +180,7 @@ class ApiDocsGenerator
$this->reflectionClasses[$className] = $class;
}
return $class->getMethod($methodName);
return $class;
}
/**

View File

@@ -2,7 +2,9 @@
namespace BookStack\Api;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
class ApiEntityListFormatter
{
@@ -20,8 +22,16 @@ class ApiEntityListFormatter
* @var array<string|int, string|callable>
*/
protected array $fields = [
'id', 'name', 'slug', 'book_id', 'chapter_id', 'draft',
'template', 'priority', 'created_at', 'updated_at',
'id',
'name',
'slug',
'book_id',
'chapter_id',
'draft',
'template',
'priority',
'created_at',
'updated_at',
];
public function __construct(array $list)
@@ -62,6 +72,28 @@ class ApiEntityListFormatter
return $this;
}
/**
* Include parent book/chapter info in the formatted data.
*/
public function withParents(): self
{
$this->withField('book', function (Entity $entity) {
if ($entity instanceof BookChild && $entity->book) {
return $entity->book->only(['id', 'name', 'slug']);
}
return null;
});
$this->withField('chapter', function (Entity $entity) {
if ($entity instanceof Page && $entity->chapter) {
return $entity->chapter->only(['id', 'name', 'slug']);
}
return null;
});
return $this;
}
/**
* Format the data and return an array of formatted content.
* @return array[]

View File

@@ -4,6 +4,7 @@ namespace BookStack\Api;
use BookStack\Access\LoginService;
use BookStack\Exceptions\ApiAuthException;
use BookStack\Permissions\Permission;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\Guard;
@@ -146,7 +147,7 @@ class ApiTokenGuard implements Guard
throw new ApiAuthException(trans('errors.api_user_token_expired'), 403);
}
if (!$token->user->can('access-api')) {
if (!$token->user->can(Permission::AccessApi)) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Api;
use BookStack\Activity\ActivityType;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Users\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
@@ -16,8 +17,8 @@ class UserApiTokenController extends Controller
*/
public function create(Request $request, int $userId)
{
$this->checkPermission('access-api');
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$this->checkPermission(Permission::AccessApi);
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
$this->updateContext($request);
$user = User::query()->findOrFail($userId);
@@ -35,8 +36,8 @@ class UserApiTokenController extends Controller
*/
public function store(Request $request, int $userId)
{
$this->checkPermission('access-api');
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$this->checkPermission(Permission::AccessApi);
$this->checkPermissionOrCurrentUser(Permission::UsersManage, $userId);
$this->validate($request, [
'name' => ['required', 'max:250'],
@@ -143,8 +144,8 @@ class UserApiTokenController extends Controller
*/
protected function checkPermissionAndFetchUserToken(int $userId, int $tokenId): array
{
$this->checkPermissionOr('users-manage', function () use ($userId) {
return $userId === user()->id && userCan('access-api');
$this->checkPermissionOr(Permission::UsersManage, function () use ($userId) {
return $userId === user()->id && userCan(Permission::AccessApi);
});
$user = User::query()->findOrFail($userId);

24
app/App/AppVersion.php Normal file
View File

@@ -0,0 +1,24 @@
<?php
namespace BookStack\App;
class AppVersion
{
protected static string $version = '';
/**
* Get the application's version number from its top-level `version` text file.
*/
public static function get(): string
{
if (!empty(static::$version)) {
return static::$version;
}
$versionFile = base_path('version');
$version = trim(file_get_contents($versionFile));
static::$version = $version;
return $version;
}
}

View File

@@ -83,7 +83,7 @@ class HomeController extends Controller
if ($homepageOption === 'bookshelves') {
$shelves = $this->queries->shelves->visibleForListWithCover()
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
->paginate(18);
->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('home.shelves', $data);
@@ -92,7 +92,7 @@ class HomeController extends Controller
if ($homepageOption === 'books') {
$books = $this->queries->books->visibleForListWithCover()
->orderBy($commonData['listOptions']->getSort(), $commonData['listOptions']->getOrder())
->paginate(18);
->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));
$data = array_merge($commonData, ['books' => $books]);
return view('home.books', $data);

View File

@@ -8,7 +8,7 @@ class Model extends EloquentModel
{
/**
* Provides public access to get the raw attribute value from the model.
* Used in areas where no mutations are required but performance is critical.
* Used in areas where no mutations are required, but performance is critical.
*
* @return mixed
*/

View File

@@ -3,6 +3,7 @@
namespace BookStack\App\Providers;
use BookStack\Access\SocialDriverManager;
use BookStack\Activity\Models\Comment;
use BookStack\Activity\Tools\ActivityLogger;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
@@ -73,6 +74,7 @@ class AppServiceProvider extends ServiceProvider
'book' => Book::class,
'chapter' => Chapter::class,
'page' => Page::class,
'comment' => Comment::class,
]);
}
}

View File

@@ -59,8 +59,8 @@ class AuthServiceProvider extends ServiceProvider
*/
public function register(): void
{
Auth::provider('external-users', function ($app, array $config) {
return new ExternalBaseUserProvider($config['model']);
Auth::provider('external-users', function () {
return new ExternalBaseUserProvider();
});
// Bind and provide the default system user as a singleton to the app instance when needed.

View File

@@ -15,7 +15,7 @@ class EventServiceProvider extends ServiceProvider
/**
* The event listener mappings for the application.
*
* @var array<class-string, array<int, class-string>>
* @var array<class-string, array<int, string>>
*/
protected $listen = [
SocialiteWasCalled::class => [
@@ -42,4 +42,12 @@ class EventServiceProvider extends ServiceProvider
{
return false;
}
/**
* Overrides the registration of Laravel's default email verification system
*/
protected function configureEmailVerification(): void
{
//
}
}

View File

@@ -85,5 +85,12 @@ class RouteServiceProvider extends ServiceProvider
RateLimiter::for('public', function (Request $request) {
return Limit::perMinute(10)->by($request->ip());
});
RateLimiter::for('exports', function (Request $request) {
$user = user();
$attempts = $user->isGuest() ? 4 : 10;
$key = $user->isGuest() ? $request->ip() : $user->id;
return Limit::perMinute($attempts)->by($key);
});
}
}

View File

@@ -3,6 +3,7 @@
namespace BookStack\App\Providers;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Util\DateFormatter;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
@@ -10,6 +11,15 @@ use Illuminate\Support\ServiceProvider;
class ViewTweaksServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(DateFormatter::class, function ($app) {
return new DateFormatter(
$app['config']->get('app.display_timezone'),
);
});
}
/**
* Bootstrap services.
*/
@@ -21,6 +31,9 @@ class ViewTweaksServiceProvider extends ServiceProvider
// View Composers
View::composer('entities.breadcrumbs', BreadcrumbsViewComposer::class);
// View Globals
View::share('dates', $this->app->make(DateFormatter::class));
// Custom blade view directives
Blade::directive('icon', function ($expression) {
return "<?php echo (new \BookStack\Util\SvgIcon($expression))->toHtml(); ?>";

View File

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

View File

@@ -0,0 +1,13 @@
<?php
namespace BookStack\App;
/**
* Assigned to models that can have slugs.
* Must have the below properties.
*
* @property string $slug
*/
interface SluggableInterface
{
}

View File

@@ -0,0 +1,31 @@
<?php
namespace BookStack\App;
use BookStack\Http\ApiController;
use Illuminate\Http\JsonResponse;
class SystemApiController extends ApiController
{
/**
* Read details regarding the BookStack instance.
* Some details may be null where not set, like the app logo for example.
*/
public function read(): JsonResponse
{
$logoSetting = setting('app-logo', '');
if ($logoSetting === 'none') {
$logo = null;
} else {
$logo = $logoSetting ? url($logoSetting) : url('/logo.png');
}
return response()->json([
'version' => AppVersion::get(),
'instance_id' => setting('instance-id'),
'app_name' => setting('app-name'),
'app_logo' => $logo,
'base_url' => url('/'),
]);
}
}

View File

@@ -1,6 +1,9 @@
<?php
use BookStack\App\AppVersion;
use BookStack\App\Model;
use BookStack\Facades\Theme;
use BookStack\Permissions\Permission;
use BookStack\Permissions\PermissionApplicator;
use BookStack\Settings\SettingService;
use BookStack\Users\Models\User;
@@ -12,12 +15,7 @@ use BookStack\Users\Models\User;
*/
function versioned_asset(string $file = ''): string
{
static $version = null;
if (is_null($version)) {
$versionFile = base_path('version');
$version = trim(file_get_contents($versionFile));
}
$version = AppVersion::get();
$additional = '';
if (config('app.env') === 'development') {
@@ -42,9 +40,9 @@ function user(): User
* Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item.
*/
function userCan(string $permission, Model $ownable = null): bool
function userCan(string|Permission $permission, ?Model $ownable = null): bool
{
if ($ownable === null) {
if (is_null($ownable)) {
return user()->can($permission);
}
@@ -58,7 +56,7 @@ function userCan(string $permission, Model $ownable = null): bool
* Check if the current user can perform the given action on any items in the system.
* Can be provided the class name of an entity to filter ability to that specific entity type.
*/
function userCanOnAny(string $action, string $entityClass = ''): bool
function userCanOnAny(string|Permission $action, string $entityClass = ''): bool
{
$permissions = app()->make(PermissionApplicator::class);
@@ -70,7 +68,7 @@ function userCanOnAny(string $action, string $entityClass = ''): bool
*
* @return mixed|SettingService
*/
function setting(string $key = null, $default = null)
function setting(?string $key = null, mixed $default = null): mixed
{
$settingService = app()->make(SettingService::class);
@@ -88,43 +86,10 @@ function setting(string $key = null, $default = null)
*/
function theme_path(string $path = ''): ?string
{
$theme = config('view.theme');
$theme = Theme::getTheme();
if (!$theme) {
return null;
}
return base_path('themes/' . $theme . ($path ? DIRECTORY_SEPARATOR . $path : $path));
}
/**
* Generate a URL with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
*/
function sortUrl(string $path, array $data, array $overrideData = []): string
{
$queryStringSections = [];
$queryData = array_merge($data, $overrideData);
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} elseif (isset($overrideData['sort'])) {
$queryData['order'] = 'asc';
}
foreach ($queryData as $name => $value) {
$trimmedVal = trim($value);
if ($trimmedVal === '') {
continue;
}
$queryStringSections[] = urlencode($name) . '=' . urlencode($trimmedVal);
}
if (count($queryStringSections) === 0) {
return url($path);
}
return url($path . '?' . implode('&', $queryStringSections));
}

View File

@@ -70,8 +70,8 @@ return [
// A list of the sources/hostnames that can be reached by application SSR calls.
// This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
// Host-specific functionality (usually controlled via other options) like auth
// or user avatars for example, won't use this list.
// Space seperated if multiple. Can use '*' as a wildcard.
// or user avatars, for example, won't use this list.
// Space separated if multiple. Can use '*' as a wildcard.
// Values will be compared prefix-matched, case-insensitive, against called SSR urls.
// Defaults to allow all hosts.
'ssr_hosts' => env('ALLOWED_SSR_HOSTS', '*'),
@@ -80,8 +80,10 @@ return [
// Integer value between 0 (IP hidden) to 4 (Full IP usage)
'ip_address_precision' => env('IP_ADDRESS_PRECISION', 4),
// Application timezone for back-end date functions.
// Application timezone for stored date/time values.
'timezone' => env('APP_TIMEZONE', 'UTC'),
// Application timezone for displayed date/time values in the UI.
'display_timezone' => env('APP_DISPLAY_TIMEZONE', env('APP_TIMEZONE', 'UTC')),
// Default locale to use
// A default variant is also stored since Laravel can overwrite

View File

@@ -1,37 +0,0 @@
<?php
/**
* Broadcasting configuration options.
*
* Changes to these config files are not supported by BookStack and may break upon updates.
* Configuration should be altered via the `.env` file or environment variables.
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
return [
// Default Broadcaster
// This option controls the default broadcaster that will be used by the
// framework when an event needs to be broadcast. This can be set to
// any of the connections defined in the "connections" array below.
'default' => 'null',
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
// to broadcast events to other systems or over websockets. Samples of
// each available type of connection are provided inside this array.
'connections' => [
// Default options removed since we don't use broadcasting.
'log' => [
'driver' => 'log',
],
'null' => [
'driver' => 'null',
],
],
];

View File

@@ -35,10 +35,6 @@ return [
// Available caches stores
'stores' => [
'apc' => [
'driver' => 'apc',
],
'array' => [
'driver' => 'array',
'serialize' => false,
@@ -49,6 +45,7 @@ return [
'table' => 'cache',
'connection' => null,
'lock_connection' => null,
'lock_table' => null,
],
'file' => [
@@ -88,6 +85,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
'prefix' => env('CACHE_PREFIX', 'bookstack_cache_'),
];

View File

@@ -40,12 +40,16 @@ if (env('REDIS_SERVERS', false)) {
// MYSQL
// Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);
if (count($mysql_host_exploded) > 1) {
$mysql_host = $mysql_host_exploded[0];
$mysql_port = intval($mysql_host_exploded[1]);
$mysqlHost = env('DB_HOST', 'localhost');
$mysqlHostExploded = explode(':', $mysqlHost);
$mysqlPort = env('DB_PORT', 3306);
$mysqlHostIpv6 = str_starts_with($mysqlHost, '[');
if ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) {
$mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1));
$mysqlPort = intval(end($mysqlHostExploded));
} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) {
$mysqlHost = $mysqlHostExploded[0];
$mysqlPort = intval($mysqlHostExploded[1]);
}
return [
@@ -61,23 +65,24 @@ return [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => $mysql_host,
'host' => $mysqlHost,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'port' => $mysql_port,
'port' => $mysqlPort,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
// Prefixes are only semi-supported and may be unstable
// since they are not tested as part of our automated test suite.
// If used, the prefix should not be changed otherwise you will likely receive errors.
// If used, the prefix should not be changed; otherwise you will likely receive errors.
'prefix' => env('DB_TABLE_PREFIX', ''),
'prefix_indexes' => true,
'strict' => false,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
// @phpstan-ignore class.notFound
(PHP_VERSION_ID >= 80500 ? \Pdo\Mysql::ATTR_SSL_CA : \PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
@@ -88,7 +93,7 @@ return [
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
'port' => $mysql_port,
'port' => $mysqlPort,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
@@ -99,9 +104,7 @@ return [
],
// Migration Repository Table
// This table keeps track of all the migrations that have already run for
// your application. Using this information, we can determine which of
// the migrations on disk haven't actually been run in the database.
// This table keeps track of all the migrations that have already run for the application.
'migrations' => 'migrations',
// Redis configuration to use if set

View File

@@ -114,6 +114,7 @@ return [
* @var array
*/
'allowed_protocols' => [
"data://" => ["rules" => []],
'file://' => ['rules' => []],
'http://' => ['rules' => []],
'https://' => ['rules' => []],

View File

@@ -11,7 +11,7 @@
return [
// Default Filesystem Disk
// Options: local, local_secure, s3
// Options: local, local_secure, local_secure_restricted, s3
'default' => env('STORAGE_TYPE', 'local'),
// Filesystem to use specifically for image uploads.
@@ -32,20 +32,22 @@ return [
'local' => [
'driver' => 'local',
'root' => public_path(),
'visibility' => 'public',
'serve' => false,
'throw' => true,
'directory_visibility' => 'public',
],
'local_secure_attachments' => [
'driver' => 'local',
'root' => storage_path('uploads/files/'),
'serve' => false,
'throw' => true,
],
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
'visibility' => 'public',
'serve' => false,
'throw' => true,
],

View File

@@ -11,6 +11,7 @@
// Configured mail encryption method.
// STARTTLS should still be attempted, but tls/ssl forces TLS usage.
$mailEncryption = env('MAIL_ENCRYPTION', null);
$mailPort = intval(env('MAIL_PORT', 587));
return [
@@ -33,13 +34,13 @@ return [
'transport' => 'smtp',
'scheme' => null,
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'port' => $mailPort,
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'verify_peer' => env('MAIL_VERIFY_SSL', true),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
'tls_required' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl'),
'local_domain' => null,
'require_tls' => ($mailEncryption === 'tls' || $mailEncryption === 'ssl' || $mailPort === 465),
],
'sendmail' => [
@@ -64,12 +65,4 @@ return [
],
],
],
// Email markdown configuration
'markdown' => [
'theme' => 'default',
'paths' => [
resource_path('views/vendor/mail'),
],
],
];

View File

@@ -47,6 +47,12 @@ return [
// Multiple values can be provided comma seperated.
'additional_scopes' => env('OIDC_ADDITIONAL_SCOPES', null),
// Enable fetching of the user's avatar from the 'picture' claim on login.
// Will only be fetched if the user doesn't already have an avatar image assigned.
// This can be a security risk due to performing server-side fetching (with up to 3 redirects) of
// data from external URLs. Only enable if you trust the OIDC auth provider to provide safe URLs for user images.
'fetch_avatar' => env('OIDC_FETCH_AVATAR', false),
// Group sync options
// Enable syncing, upon login, of OIDC groups to BookStack roles
'user_to_groups' => env('OIDC_USER_TO_GROUPS', false),

View File

@@ -23,6 +23,7 @@ return [
'database' => [
'driver' => 'database',
'connection' => null,
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,

View File

@@ -22,6 +22,18 @@ return [
// Callback URL for social authentication methods
'callback_url' => env('APP_URL', false),
// LLM Service
// Options: openai
'llm' => env('LLM_SERVICE', ''),
// OpenAI API-compatible service details
'openai' => [
'endpoint' => env('OPENAI_ENDPOINT', 'https://api.openai.com'),
'key' => env('OPENAI_KEY', ''),
'embedding_model' => env('OPENAI_EMBEDDING_MODEL', 'text-embedding-3-small'),
'query_model' => env('OPENAI_QUERY_MODEL', 'gpt-4o'),
],
'github' => [
'client_id' => env('GITHUB_APP_ID', false),
'client_secret' => env('GITHUB_APP_SECRET', false),

View File

@@ -41,6 +41,7 @@ return [
'bookshelves_view_type' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
'bookshelf_view_type' => env('APP_VIEWS_BOOKSHELF', 'grid'),
'books_view_type' => env('APP_VIEWS_BOOKS', 'grid'),
'notifications#comment-mentions' => true,
],
];

View File

@@ -0,0 +1,99 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Entities\Models\Book;
use BookStack\Sorting\BookSorter;
use BookStack\Sorting\SortRule;
use Illuminate\Console\Command;
class AssignSortRuleCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:assign-sort-rule
{sort-rule=0: ID of the sort rule to apply}
{--all-books : Apply to all books in the system}
{--books-without-sort : Apply to only books without a sort rule already assigned}
{--books-with-sort= : Apply to only books with the sort rule of given id}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Assign a sort rule to content in the system';
/**
* Execute the console command.
*/
public function handle(BookSorter $sorter): int
{
$sortRuleId = intval($this->argument('sort-rule')) ?? 0;
if ($sortRuleId === 0) {
return $this->listSortRules();
}
$rule = SortRule::query()->find($sortRuleId);
if ($this->option('all-books')) {
$query = Book::query();
} else if ($this->option('books-without-sort')) {
$query = Book::query()->whereNull('sort_rule_id');
} else if ($this->option('books-with-sort')) {
$sortId = intval($this->option('books-with-sort')) ?: 0;
if (!$sortId) {
$this->error("Provided --books-with-sort option value is invalid");
return 1;
}
$query = Book::query()->where('sort_rule_id', $sortId);
} else {
$this->error("No option provided to specify target. Run with the -h option to see all available options.");
return 1;
}
if (!$rule) {
$this->error("Sort rule of provided id {$sortRuleId} not found!");
return 1;
}
$count = $query->clone()->count();
$this->warn("This will apply sort rule [{$rule->id}: {$rule->name}] to {$count} book(s) and run the sort on each.");
$confirmed = $this->confirm("Are you sure you want to continue?");
if (!$confirmed) {
return 1;
}
$processed = 0;
$query->chunkById(10, function ($books) use ($rule, $sorter, $count, &$processed) {
$max = min($count, ($processed + 10));
$this->info("Applying to {$processed}-{$max} of {$count} books");
foreach ($books as $book) {
$book->sort_rule_id = $rule->id;
$book->save();
$sorter->runBookAutoSort($book);
}
$processed = $max;
});
$this->info("Sort applied to {$processed} book(s)!");
return 0;
}
protected function listSortRules(): int
{
$rules = SortRule::query()->orderBy('id', 'asc')->get();
$this->error("Sort rule ID required!");
$this->warn("\nAvailable sort rules:");
foreach ($rules as $rule) {
$this->info("{$rule->id}: {$rule->name}");
}
return 1;
}
}

View File

@@ -8,7 +8,6 @@ use Illuminate\Console\Command;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password;
use Illuminate\Validation\Rules\Unique;
class CreateAdminCommand extends Command
{
@@ -21,7 +20,9 @@ class CreateAdminCommand extends Command
{--email= : The email address for the new admin user}
{--name= : The name of the new admin user}
{--password= : The password to assign to the new admin user}
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}';
{--external-auth-id= : The external authentication system id for the new admin user (SAML2/LDAP/OIDC)}
{--generate-password : Generate a random password for the new admin user}
{--initial : Indicate if this should set/update the details of the initial admin user}';
/**
* The console command description.
@@ -35,26 +36,12 @@ class CreateAdminCommand extends Command
*/
public function handle(UserRepo $userRepo): int
{
$details = $this->snakeCaseOptions();
if (empty($details['email'])) {
$details['email'] = $this->ask('Please specify an email address for the new admin user');
}
if (empty($details['name'])) {
$details['name'] = $this->ask('Please specify a name for the new admin user');
}
if (empty($details['password'])) {
if (empty($details['external_auth_id'])) {
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
} else {
$details['password'] = Str::random(32);
}
}
$initialAdminOnly = $this->option('initial');
$shouldGeneratePassword = $this->option('generate-password');
$details = $this->gatherDetails($shouldGeneratePassword, $initialAdminOnly);
$validator = Validator::make($details, [
'email' => ['required', 'email', 'min:5', new Unique('users', 'email')],
'email' => ['required', 'email', 'min:5'],
'name' => ['required', 'min:2'],
'password' => ['required_without:external_auth_id', Password::default()],
'external_auth_id' => ['required_without:password'],
@@ -68,16 +55,101 @@ class CreateAdminCommand extends Command
return 1;
}
$adminRole = Role::getSystemRole('admin');
if ($initialAdminOnly) {
$handled = $this->handleInitialAdminIfExists($userRepo, $details, $shouldGeneratePassword, $adminRole);
if ($handled !== null) {
return $handled;
}
}
$emailUsed = $userRepo->getByEmail($details['email']) !== null;
if ($emailUsed) {
$this->error("Could not create admin account.");
$this->error("An account with the email address \"{$details['email']}\" already exists.");
return 1;
}
$user = $userRepo->createWithoutActivity($validator->validated());
$user->attachRole(Role::getSystemRole('admin'));
$user->attachRole($adminRole);
$user->email_confirmed = true;
$user->save();
$this->info("Admin account with email \"{$user->email}\" successfully created!");
if ($shouldGeneratePassword) {
$this->line($details['password']);
} else {
$this->info("Admin account with email \"{$user->email}\" successfully created!");
}
return 0;
}
/**
* Handle updates to the original admin account if it exists.
* Returns an int return status if handled, otherwise returns null if not handled (new user to be created).
*/
protected function handleInitialAdminIfExists(UserRepo $userRepo, array $data, bool $generatePassword, Role $adminRole): int|null
{
$defaultAdmin = $userRepo->getByEmail('admin@admin.com');
if ($defaultAdmin && $defaultAdmin->hasSystemRole('admin')) {
if ($defaultAdmin->email !== $data['email'] && $userRepo->getByEmail($data['email']) !== null) {
$this->error("Could not create admin account.");
$this->error("An account with the email address \"{$data['email']}\" already exists.");
return 1;
}
$userRepo->updateWithoutActivity($defaultAdmin, $data, true);
if ($generatePassword) {
$this->line($data['password']);
} else {
$this->info("The default admin user has been updated with the provided details!");
}
return 0;
} else if ($adminRole->users()->count() > 0) {
$this->warn('Non-default admin user already exists. Skipping creation of new admin user.');
return 2;
}
return null;
}
protected function gatherDetails(bool $generatePassword, bool $initialAdmin): array
{
$details = $this->snakeCaseOptions();
if (empty($details['email'])) {
if ($initialAdmin) {
$details['email'] = 'admin@example.com';
} else {
$details['email'] = $this->ask('Please specify an email address for the new admin user');
}
}
if (empty($details['name'])) {
if ($initialAdmin) {
$details['name'] = 'Admin';
} else {
$details['name'] = $this->ask('Please specify a name for the new admin user');
}
}
if (empty($details['password'])) {
if (empty($details['external_auth_id'])) {
if ($generatePassword) {
$details['password'] = Str::random(32);
} else {
$details['password'] = $this->ask('Please specify a password for the new admin user (8 characters min)');
}
} else {
$details['password'] = Str::random(32);
}
}
return $details;
}
protected function snakeCaseOptions(): array
{
$returnOpts = [];

View File

@@ -0,0 +1,46 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Search\Queries\SearchVector;
use BookStack\Search\Queries\StoreEntityVectorsJob;
use Illuminate\Console\Command;
class RegenerateVectorsCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-vectors';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Re-index vectors for all content in the system';
/**
* Execute the console command.
*/
public function handle(EntityProvider $entityProvider)
{
// TODO - Add confirmation before run regarding deletion/time/effort/api-cost etc...
SearchVector::query()->delete();
$types = $entityProvider->all();
foreach ($types as $type => $typeInstance) {
$this->info("Creating jobs to store vectors for {$type} data...");
/** @var Entity[] $entities */
$typeInstance->newQuery()->chunkById(100, function ($entities) {
foreach ($entities as $entity) {
dispatch(new StoreEntityVectorsJob($entity));
}
});
}
}
}

View File

@@ -45,13 +45,12 @@ class UpdateUrlCommand extends Command
$columnsToUpdateByTable = [
'attachments' => ['path'],
'pages' => ['html', 'text', 'markdown'],
'chapters' => ['description_html'],
'books' => ['description_html'],
'bookshelves' => ['description_html'],
'entity_page_data' => ['html', 'text', 'markdown'],
'entity_container_data' => ['description_html'],
'page_revisions' => ['html', 'text', 'markdown'],
'images' => ['url'],
'settings' => ['value'],
'comments' => ['html', 'text'],
'comments' => ['html'],
];
foreach ($columnsToUpdateByTable as $table => $columns) {
@@ -77,6 +76,12 @@ class UpdateUrlCommand extends Command
$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.');
if (!str_starts_with($newUrl, url('/'))) {
$this->warn('You still need to update your APP_URL env value. This is currently set to:');
$this->warn(url('/'));
}
$this->info('============================================================================');
return 0;

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -30,6 +31,7 @@ class BookApiController extends ApiController
{
$books = $this->queries
->visibleForList()
->with(['cover:id,name,url'])
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($books, [
@@ -46,7 +48,7 @@ class BookApiController extends ApiController
*/
public function create(Request $request)
{
$this->checkPermission('book-create-all');
$this->checkPermission(Permission::BookCreateAll);
$requestData = $this->validate($request, $this->rules()['create']);
$book = $this->bookRepo->create($requestData);
@@ -56,7 +58,7 @@ class BookApiController extends ApiController
/**
* View the details of a single book.
* The response data will contain 'content' property listing the chapter and pages directly within, in
* The response data will contain a 'content' property listing the chapter and pages directly within, in
* the same structure as you'd see within the BookStack interface when viewing a book. Top-level
* contents will have a 'type' property to distinguish between pages & chapters.
*/
@@ -91,7 +93,7 @@ class BookApiController extends ApiController
public function update(Request $request, string $id)
{
$book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$requestData = $this->validate($request, $this->rules()['update']);
$book = $this->bookRepo->update($book, $requestData);
@@ -108,7 +110,7 @@ class BookApiController extends ApiController
public function delete(string $id)
{
$book = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('book-delete', $book);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->bookRepo->destroy($book);
@@ -120,9 +122,10 @@ class BookApiController extends ApiController
$book = clone $book;
$book->unsetRelations()->refresh();
$book->load(['tags', 'cover']);
$book->makeVisible('description_html')
->setAttribute('description_html', $book->descriptionHtml());
$book->load(['tags']);
$book->makeVisible(['cover', 'description_html'])
->setAttribute('description_html', $book->descriptionInfo()->getHtml())
->setAttribute('cover', $book->coverInfo()->getImage());
return $book;
}

View File

@@ -8,6 +8,7 @@ use BookStack\Activity\Models\View;
use BookStack\Activity\Tools\UserEntityWatchOptions;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\Cloner;
@@ -17,7 +18,9 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -29,6 +32,7 @@ class BookController extends Controller
protected ShelfContext $shelfContext,
protected BookRepo $bookRepo,
protected BookQueries $queries,
protected EntityQueries $entityQueries,
protected BookshelfQueries $shelfQueries,
protected ReferenceFetcher $referenceFetcher,
) {
@@ -48,7 +52,7 @@ class BookController extends Controller
$books = $this->queries->visibleForListWithCover()
->orderBy($listOptions->getSort(), $listOptions->getOrder())
->paginate(18);
->paginate(setting()->getInteger('lists-page-count-books', 18, 1, 1000));
$recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->take(4)->get() : false;
$popular = $this->queries->popularForList()->take(4)->get();
$new = $this->queries->visibleForList()->orderBy('created_at', 'desc')->take(4)->get();
@@ -70,14 +74,14 @@ class BookController extends Controller
/**
* Show the form for creating a new book.
*/
public function create(string $shelfSlug = null)
public function create(?string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$this->checkPermission(Permission::BookCreateAll);
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
}
$this->setPageTitle(trans('entities.books_create'));
@@ -93,9 +97,9 @@ class BookController extends Controller
* @throws ImageUploadException
* @throws ValidationException
*/
public function store(Request $request, string $shelfSlug = null)
public function store(Request $request, ?string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$this->checkPermission(Permission::BookCreateAll);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -107,7 +111,7 @@ class BookController extends Controller
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->shelfQueries->findVisibleBySlugOrFail($shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $bookshelf);
}
$book = $this->bookRepo->create($validated);
@@ -125,7 +129,16 @@ class BookController extends Controller
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$book = $this->queries->findVisibleBySlugOrFail($slug);
try {
$book = $this->queries->findVisibleBySlugOrFail($slug);
} catch (NotFoundException $exception) {
$book = $this->entityQueries->findVisibleByOldSlugs('book', $slug);
if (is_null($book)) {
throw $exception;
}
return redirect($book->getUrl());
}
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->scopes('visible')->get();
@@ -153,7 +166,7 @@ class BookController extends Controller
public function edit(string $slug)
{
$book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName' => $book->getShortName()]));
return view('books.edit', ['book' => $book, 'current' => $book]);
@@ -169,7 +182,7 @@ class BookController extends Controller
public function update(Request $request, string $slug)
{
$book = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
@@ -196,7 +209,7 @@ class BookController extends Controller
public function showDelete(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName' => $book->getShortName()]));
return view('books.delete', ['book' => $book, 'current' => $book]);
@@ -210,7 +223,7 @@ class BookController extends Controller
public function destroy(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->bookRepo->destroy($book);
@@ -225,7 +238,7 @@ class BookController extends Controller
public function showCopy(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-view', $book);
$this->checkOwnablePermission(Permission::BookView, $book);
session()->flashInput(['name' => $book->name]);
@@ -242,8 +255,8 @@ class BookController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-view', $book);
$this->checkPermission('book-create-all');
$this->checkOwnablePermission(Permission::BookView, $book);
$this->checkPermission(Permission::BookCreateAll);
$newName = $request->get('name') ?: $book->name;
$bookCopy = $cloner->cloneBook($book, $newName);
@@ -258,12 +271,14 @@ class BookController extends Controller
public function convertToShelf(HierarchyTransformer $transformer, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$this->checkOwnablePermission('book-delete', $book);
$this->checkPermission('bookshelf-create-all');
$this->checkPermission('book-create-all');
$this->checkOwnablePermission(Permission::BookUpdate, $book);
$this->checkOwnablePermission(Permission::BookDelete, $book);
$this->checkPermission(Permission::BookshelfCreateAll);
$this->checkPermission(Permission::BookCreateAll);
$shelf = $transformer->transformBookToShelf($book);
$shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
return $transformer->transformBookToShelf($book);
}))->run();
return redirect($shelf->getUrl());
}

View File

@@ -1,71 +0,0 @@
<?php
namespace BookStack\Entities\Controllers;
use BookStack\Activity\ActivityType;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\BookSortMap;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
class BookSortController extends Controller
{
public function __construct(
protected BookQueries $queries,
) {
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
*/
public function show(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = (new BookContents($book))->getTree(false);
$this->setPageTitle(trans('entities.books_sort_named', ['bookName' => $book->getShortName()]));
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Shows the sort box for a single book.
* Used via AJAX when loading in extra books to a sort.
*/
public function showItem(string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$bookChildren = (new BookContents($book))->getTree();
return view('books.parts.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
* Sorts a book using a given mapping array.
*/
public function update(Request $request, string $bookSlug)
{
$book = $this->queries->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('book-update', $book);
// Return if no map sent
if (!$request->filled('sort-tree')) {
return redirect($book->getUrl());
}
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$bookContents = new BookContents($book);
$booksInvolved = $bookContents->sortUsingMap($sortMap);
// Rebuild permissions and add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
}
return redirect($book->getUrl());
}
}

View File

@@ -6,6 +6,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
@@ -26,6 +27,7 @@ class BookshelfApiController extends ApiController
{
$shelves = $this->queries
->visibleForList()
->with(['cover:id,name,url'])
->addSelect(['created_by', 'updated_by']);
return $this->apiListingResponse($shelves, [
@@ -44,7 +46,7 @@ class BookshelfApiController extends ApiController
*/
public function create(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$this->checkPermission(Permission::BookshelfCreateAll);
$requestData = $this->validate($request, $this->rules()['create']);
$bookIds = $request->get('books', []);
@@ -83,7 +85,7 @@ class BookshelfApiController extends ApiController
public function update(Request $request, string $id)
{
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$requestData = $this->validate($request, $this->rules()['update']);
$bookIds = $request->get('books', null);
@@ -102,7 +104,7 @@ class BookshelfApiController extends ApiController
public function delete(string $id)
{
$shelf = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
$this->bookshelfRepo->destroy($shelf);
@@ -114,9 +116,10 @@ class BookshelfApiController extends ApiController
$shelf = clone $shelf;
$shelf->unsetRelations()->refresh();
$shelf->load(['tags', 'cover']);
$shelf->makeVisible('description_html')
->setAttribute('description_html', $shelf->descriptionHtml());
$shelf->load(['tags']);
$shelf->makeVisible(['cover', 'description_html'])
->setAttribute('description_html', $shelf->descriptionInfo()->getHtml())
->setAttribute('cover', $shelf->coverInfo()->getImage());
return $shelf;
}

View File

@@ -6,11 +6,13 @@ use BookStack\Activity\ActivityQueries;
use BookStack\Activity\Models\View;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Queries\BookshelfQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\SimpleListOptions;
use Exception;
@@ -22,6 +24,7 @@ class BookshelfController extends Controller
public function __construct(
protected BookshelfRepo $shelfRepo,
protected BookshelfQueries $queries,
protected EntityQueries $entityQueries,
protected BookQueries $bookQueries,
protected ShelfContext $shelfContext,
protected ReferenceFetcher $referenceFetcher,
@@ -42,7 +45,7 @@ class BookshelfController extends Controller
$shelves = $this->queries->visibleForListWithCover()
->orderBy($listOptions->getSort(), $listOptions->getOrder())
->paginate(18);
->paginate(setting()->getInteger('lists-page-count-shelves', 18, 1, 1000));
$recents = $this->isSignedIn() ? $this->queries->recentlyViewedForCurrentUser()->get() : false;
$popular = $this->queries->popularForList()->get();
$new = $this->queries->visibleForList()
@@ -68,7 +71,7 @@ class BookshelfController extends Controller
*/
public function create()
{
$this->checkPermission('bookshelf-create-all');
$this->checkPermission(Permission::BookshelfCreateAll);
$books = $this->bookQueries->visibleForList()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));
@@ -83,7 +86,7 @@ class BookshelfController extends Controller
*/
public function store(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$this->checkPermission(Permission::BookshelfCreateAll);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -104,8 +107,17 @@ class BookshelfController extends Controller
*/
public function show(Request $request, ActivityQueries $activities, string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-view', $shelf);
try {
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
} catch (NotFoundException $exception) {
$shelf = $this->entityQueries->findVisibleByOldSlugs('bookshelf', $slug);
if (is_null($shelf)) {
throw $exception;
}
return redirect($shelf->getUrl());
}
$this->checkOwnablePermission(Permission::BookshelfView, $shelf);
$listOptions = SimpleListOptions::fromRequest($request, 'shelf_books')->withSortOptions([
'default' => trans('common.sort_default'),
@@ -115,6 +127,7 @@ class BookshelfController extends Controller
]);
$sort = $listOptions->getSort();
$sortedVisibleShelfBooks = $shelf->visibleBooks()
->reorder($sort === 'default' ? 'order' : $sort, $listOptions->getOrder())
->get()
@@ -143,7 +156,7 @@ class BookshelfController extends Controller
public function edit(string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
$books = $this->bookQueries->visibleForList()
@@ -169,7 +182,7 @@ class BookshelfController extends Controller
public function update(Request $request, string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$this->checkOwnablePermission(Permission::BookshelfUpdate, $shelf);
$validated = $this->validate($request, [
'name' => ['required', 'string', 'max:255'],
'description_html' => ['string', 'max:2000'],
@@ -195,7 +208,7 @@ class BookshelfController extends Controller
public function showDelete(string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
@@ -210,7 +223,7 @@ class BookshelfController extends Controller
public function destroy(string $slug)
{
$shelf = $this->queries->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->checkOwnablePermission(Permission::BookshelfDelete, $shelf);
$this->shelfRepo->destroy($shelf);

View File

@@ -2,19 +2,20 @@
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected $rules = [
protected array $rules = [
'create' => [
'book_id' => ['required', 'integer'],
'name' => ['required', 'string', 'max:255'],
@@ -65,7 +66,7 @@ class ChapterApiController extends ApiController
$bookId = $request->get('book_id');
$book = $this->entityQueries->books->findVisibleByIdOrFail(intval($bookId));
$this->checkOwnablePermission('chapter-create', $book);
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$chapter = $this->chapterRepo->create($requestData, $book);
@@ -101,10 +102,10 @@ class ChapterApiController extends ApiController
{
$requestData = $this->validate($request, $this->rules()['update']);
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
if ($request->has('book_id') && $chapter->book_id !== intval($requestData['book_id'])) {
$this->checkOwnablePermission('chapter-delete', $chapter);
if ($request->has('book_id') && $chapter->book_id !== (intval($requestData['book_id']) ?: null)) {
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
try {
$this->chapterRepo->move($chapter, "book:{$requestData['book_id']}");
@@ -129,7 +130,7 @@ class ChapterApiController extends ApiController
public function delete(string $id)
{
$chapter = $this->queries->findVisibleByIdOrFail(intval($id));
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->chapterRepo->destroy($chapter);
@@ -143,8 +144,11 @@ class ChapterApiController extends ApiController
$chapter->load(['tags']);
$chapter->makeVisible('description_html');
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
$chapter->setAttribute('book_slug', $chapter->book()->first()->slug);
$chapter->setAttribute('description_html', $chapter->descriptionInfo()->getHtml());
/** @var Book $book */
$book = $chapter->book()->first();
$chapter->setAttribute('book_slug', $book->slug);
return $chapter;
}

View File

@@ -17,7 +17,9 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -38,7 +40,7 @@ class ChapterController extends Controller
public function create(string $bookSlug)
{
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$this->setPageTitle(trans('entities.chapters_create'));
@@ -63,7 +65,7 @@ class ChapterController extends Controller
]);
$book = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->checkOwnablePermission(Permission::ChapterCreate, $book);
$chapter = $this->chapterRepo->create($validated, $book);
@@ -75,8 +77,15 @@ class ChapterController extends Controller
*/
public function show(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
try {
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
} catch (NotFoundException $exception) {
$chapter = $this->entityQueries->findVisibleByOldSlugs('chapter', $chapterSlug, $bookSlug);
if (is_null($chapter)) {
throw $exception;
}
return redirect($chapter->getUrl());
}
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $this->entityQueries->pages->visibleForChapterList($chapter->id)->get();
@@ -105,7 +114,7 @@ class ChapterController extends Controller
public function edit(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
@@ -127,9 +136,9 @@ class ChapterController extends Controller
]);
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->chapterRepo->update($chapter, $validated);
$chapter = $this->chapterRepo->update($chapter, $validated);
return redirect($chapter->getUrl());
}
@@ -142,7 +151,7 @@ class ChapterController extends Controller
public function showDelete(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
@@ -158,7 +167,7 @@ class ChapterController extends Controller
public function destroy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->chapterRepo->destroy($chapter);
@@ -174,8 +183,8 @@ class ChapterController extends Controller
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
return view('chapters.move', [
'chapter' => $chapter,
@@ -191,8 +200,8 @@ class ChapterController extends Controller
public function move(Request $request, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -220,7 +229,6 @@ class ChapterController extends Controller
public function showCopy(string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
session()->flashInput(['name' => $chapter->name]);
@@ -239,7 +247,6 @@ class ChapterController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-view', $chapter);
$entitySelection = $request->get('entity_selection') ?: null;
$newParentBook = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $chapter->getParent();
@@ -250,7 +257,7 @@ class ChapterController extends Controller
return redirect($chapter->getUrl('/copy'));
}
$this->checkOwnablePermission('chapter-create', $newParentBook);
$this->checkOwnablePermission(Permission::ChapterCreate, $newParentBook);
$newName = $request->get('name') ?: $chapter->name;
$chapterCopy = $cloner->cloneChapter($chapter, $newParentBook, $newName);
@@ -265,11 +272,13 @@ class ChapterController extends Controller
public function convertToBook(HierarchyTransformer $transformer, string $bookSlug, string $chapterSlug)
{
$chapter = $this->queries->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
$this->checkOwnablePermission(Permission::ChapterUpdate, $chapter);
$this->checkOwnablePermission(Permission::ChapterDelete, $chapter);
$this->checkPermission(Permission::BookCreateAll);
$book = $transformer->transformChapterToBook($chapter);
$book = (new DatabaseTransaction(function () use ($chapter, $transformer) {
return $transformer->transformChapterToBook($chapter);
}))->run();
return redirect($book->getUrl());
}

View File

@@ -2,17 +2,19 @@
namespace BookStack\Entities\Controllers;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Entities\Queries\EntityQueries;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use BookStack\Permissions\Permission;
use Exception;
use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected $rules = [
protected array $rules = [
'create' => [
'book_id' => ['required_without:chapter_id', 'integer'],
'chapter_id' => ['required_without:book_id', 'integer'],
@@ -76,7 +78,7 @@ class PageApiController extends ApiController
} else {
$parent = $this->entityQueries->books->findVisibleByIdOrFail(intval($request->get('book_id')));
}
$this->checkOwnablePermission('page-create', $parent);
$this->checkOwnablePermission(Permission::PageCreate, $parent);
$draft = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($draft, $request->only(array_keys($this->rules['create'])));
@@ -87,21 +89,32 @@ class PageApiController extends ApiController
/**
* View the details of a single page.
* Pages will always have HTML content. They may have markdown content
* if the markdown editor was used to last update the page.
* if the Markdown editor was used to last update the page.
*
* The 'html' property is the fully rendered & escaped HTML content that BookStack
* The 'html' property is the fully rendered and escaped HTML content that BookStack
* would show on page view, with page includes handled.
* The 'raw_html' property is the direct database stored HTML content, which would be
* what BookStack shows on page edit.
*
* See the "Content Security" section of these docs for security considerations when using
* the page content returned from this endpoint.
*
* Comments for the page are provided in a tree-structure representing the hierarchy of top-level
* comments and replies, for both archived and active comments.
*/
public function read(string $id)
{
$page = $this->queries->findVisibleByIdOrFail($id);
return response()->json($page->forJsonDisplay());
$page = $page->forJsonDisplay();
$commentTree = (new CommentTree($page));
$commentTree->loadVisibleHtml();
$page->setAttribute('comments', [
'active' => $commentTree->getActive(),
'archived' => $commentTree->getArchived(),
]);
return response()->json($page);
}
/**
@@ -116,7 +129,7 @@ class PageApiController extends ApiController
$requestData = $this->validate($request, $this->rules['update']);
$page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$parent = null;
if ($request->has('chapter_id')) {
@@ -126,7 +139,7 @@ class PageApiController extends ApiController
}
if ($parent && !$parent->matches($page->getParent())) {
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
try {
$this->pageRepo->move($page, $parent->getType() . ':' . $parent->id);
@@ -151,7 +164,7 @@ class PageApiController extends ApiController
public function delete(string $id)
{
$page = $this->queries->findVisibleByIdOrFail($id);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->pageRepo->destroy($page);

View File

@@ -19,6 +19,7 @@ use BookStack\Entities\Tools\PageEditorData;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\References\ReferenceFetcher;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
@@ -41,7 +42,7 @@ class PageController extends Controller
*
* @throws Throwable
*/
public function create(string $bookSlug, string $chapterSlug = null)
public function create(string $bookSlug, ?string $chapterSlug = null)
{
if ($chapterSlug) {
$parent = $this->entityQueries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
@@ -49,7 +50,7 @@ class PageController extends Controller
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
}
$this->checkOwnablePermission('page-create', $parent);
$this->checkOwnablePermission(Permission::PageCreate, $parent);
// Redirect to draft edit screen if signed in
if ($this->isSignedIn()) {
@@ -69,7 +70,7 @@ class PageController extends Controller
*
* @throws ValidationException
*/
public function createAsGuest(Request $request, string $bookSlug, string $chapterSlug = null)
public function createAsGuest(Request $request, string $bookSlug, ?string $chapterSlug = null)
{
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
@@ -81,7 +82,7 @@ class PageController extends Controller
$parent = $this->entityQueries->books->findVisibleBySlugOrFail($bookSlug);
}
$this->checkOwnablePermission('page-create', $parent);
$this->checkOwnablePermission(Permission::PageCreate, $parent);
$page = $this->pageRepo->getNewDraftPage($parent);
$this->pageRepo->publishDraft($page, [
@@ -99,7 +100,7 @@ class PageController extends Controller
public function editDraft(Request $request, string $bookSlug, int $pageId)
{
$draft = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-create', $draft->getParent());
$this->checkOwnablePermission(Permission::PageCreate, $draft->getParent());
$editorData = new PageEditorData($draft, $this->entityQueries, $request->query('editor', ''));
$this->setPageTitle(trans('entities.pages_edit_draft'));
@@ -118,8 +119,9 @@ class PageController extends Controller
$this->validate($request, [
'name' => ['required', 'string', 'max:255'],
]);
$draftPage = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-create', $draftPage->getParent());
$this->checkOwnablePermission(Permission::PageCreate, $draftPage->getParent());
$page = $this->pageRepo->publishDraft($draftPage, $request->all());
@@ -137,9 +139,7 @@ class PageController extends Controller
try {
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
} catch (NotFoundException $e) {
$revision = $this->entityQueries->revisions->findLatestVersionBySlugs($bookSlug, $pageSlug);
$page = $revision->page ?? null;
$page = $this->entityQueries->findVisibleByOldSlugs('page', $pageSlug, $bookSlug);
if (is_null($page)) {
throw $e;
}
@@ -147,8 +147,6 @@ class PageController extends Controller
return redirect($page->getUrl());
}
$this->checkOwnablePermission('page-view', $page);
$pageContent = (new PageContent($page));
$page->html = $pageContent->render();
$pageNav = $pageContent->getNavigation($page->html);
@@ -196,7 +194,7 @@ class PageController extends Controller
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page, $page->getUrl());
$editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
if ($editorData->getWarnings()) {
@@ -220,7 +218,7 @@ class PageController extends Controller
'name' => ['required', 'string', 'max:255'],
]);
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->pageRepo->update($page, $request->all());
@@ -235,7 +233,7 @@ class PageController extends Controller
public function saveDraft(Request $request, int $pageId)
{
$page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
if (!$this->isSignedIn()) {
return $this->jsonError(trans('errors.guests_cannot_save_drafts'), 500);
@@ -272,7 +270,7 @@ class PageController extends Controller
public function showDelete(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate =
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
@@ -294,7 +292,7 @@ class PageController extends Controller
public function showDeleteDraft(string $bookSlug, int $pageId)
{
$page = $this->queries->findVisibleByIdOrFail($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
$usedAsTemplate =
$this->entityQueries->books->start()->where('default_template_id', '=', $page->id)->count() > 0 ||
@@ -317,7 +315,7 @@ class PageController extends Controller
public function destroy(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$parent = $page->getParent();
$this->pageRepo->destroy($page);
@@ -336,13 +334,13 @@ class PageController extends Controller
$page = $this->queries->findVisibleByIdOrFail($pageId);
$book = $page->book;
$chapter = $page->chapter;
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->pageRepo->destroy($page);
$this->showSuccessNotification(trans('entities.pages_delete_draft_success'));
if ($chapter && userCan('view', $chapter)) {
if ($chapter && userCan(Permission::ChapterView, $chapter)) {
return redirect($chapter->getUrl());
}
@@ -383,8 +381,8 @@ class PageController extends Controller
public function showMove(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
return view('pages.move', [
'book' => $page->book,
@@ -401,8 +399,8 @@ class PageController extends Controller
public function move(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -430,7 +428,6 @@ class PageController extends Controller
public function showCopy(string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages.copy', [
@@ -448,7 +445,7 @@ class PageController extends Controller
public function copy(Request $request, Cloner $cloner, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-view', $page);
$this->checkOwnablePermission(Permission::PageView, $page);
$entitySelection = $request->get('entity_selection') ?: null;
$newParent = $entitySelection ? $this->entityQueries->findVisibleByStringIdentifier($entitySelection) : $page->getParent();
@@ -459,7 +456,7 @@ class PageController extends Controller
return redirect($page->getUrl('/copy'));
}
$this->checkOwnablePermission('page-create', $newParent);
$this->checkOwnablePermission(Permission::PageCreate, $newParent);
$newName = $request->get('name') ?: $page->name;
$pageCopy = $cloner->clonePage($page, $newParent, $newName);

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\PageContent;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Ssddanbrown\HtmlDiff\Diff;
@@ -43,7 +44,6 @@ class PageRevisionController extends Controller
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
->reorder('id', $listOptions->getOrder())
->reorder('created_at', $listOptions->getOrder())
->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
@@ -52,6 +52,7 @@ class PageRevisionController extends Controller
'revisions' => $revisions,
'page' => $page,
'listOptions' => $listOptions,
'oldestRevisionId' => $page->revisions()->min('id'),
]);
}
@@ -98,7 +99,7 @@ class PageRevisionController extends Controller
throw new NotFoundException();
}
$prev = $revision->getPrevious();
$prev = $revision->getPreviousRevision();
$prevContent = $prev->html ?? '';
$diff = Diff::excecute($prevContent, $revision->html);
@@ -124,7 +125,7 @@ class PageRevisionController extends Controller
public function restore(string $bookSlug, string $pageSlug, int $revisionId)
{
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission(Permission::PageUpdate, $page);
$page = $this->pageRepo->restoreRevision($page, $revisionId);
@@ -139,7 +140,7 @@ class PageRevisionController extends Controller
public function destroy(string $bookSlug, string $pageSlug, int $revId)
{
$page = $this->pageQueries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->checkOwnablePermission(Permission::PageDelete, $page);
$revision = $page->revisions()->where('id', '=', $revId)->first();
if ($revision === null) {

View File

@@ -6,18 +6,20 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Deletion;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Http\ApiController;
use Closure;
use BookStack\Permissions\Permission;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\HasMany;
class RecycleBinApiController extends ApiController
{
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::RestrictionsManageAll);
return $next($request);
});
@@ -40,7 +42,7 @@ class RecycleBinApiController extends ApiController
'updated_at',
'deletable_type',
'deletable_id',
], [Closure::fromCallable([$this, 'listFormatter'])]);
], [$this->listFormatter(...)]);
}
/**
@@ -69,10 +71,9 @@ class RecycleBinApiController extends ApiController
/**
* Load some related details for the deletion listing.
*/
protected function listFormatter(Deletion $deletion)
protected function listFormatter(Deletion $deletion): void
{
$deletable = $deletion->deletable;
$withTrashedQuery = fn (Builder $query) => $query->withTrashed();
if ($deletable instanceof BookChild) {
$parent = $deletable->getParent();
@@ -81,11 +82,19 @@ class RecycleBinApiController extends ApiController
}
if ($deletable instanceof Book || $deletable instanceof Chapter) {
$countsToLoad = ['pages' => $withTrashedQuery];
$countsToLoad = ['pages' => static::withTrashedQuery(...)];
if ($deletable instanceof Book) {
$countsToLoad['chapters'] = $withTrashedQuery;
$countsToLoad['chapters'] = static::withTrashedQuery(...);
}
$deletable->loadCount($countsToLoad);
}
}
/**
* @param Builder<Chapter|Page> $query
*/
protected static function withTrashedQuery(Builder $query): void
{
$query->withTrashed();
}
}

View File

@@ -8,6 +8,7 @@ use BookStack\Entities\Models\Entity;
use BookStack\Entities\Repos\DeletionRepo;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Http\Controller;
use BookStack\Permissions\Permission;
class RecycleBinController extends Controller
{
@@ -20,8 +21,8 @@ class RecycleBinController extends Controller
public function __construct()
{
$this->middleware(function ($request, $next) {
$this->checkPermission('settings-manage');
$this->checkPermission('restrictions-manage-all');
$this->checkPermission(Permission::SettingsManage);
$this->checkPermission(Permission::RestrictionsManageAll);
return $next($request);
});

View File

@@ -0,0 +1,20 @@
<?php
namespace BookStack\Entities;
use Illuminate\Validation\Rules\Exists;
class EntityExistsRule implements \Stringable
{
public function __construct(
protected string $type,
) {
}
public function __toString()
{
$existsRule = (new Exists('entities', 'id'))
->where('type', $this->type);
return $existsRule->__toString();
}
}

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