Compare commits

..

175 Commits

Author SHA1 Message Date
Dan Brown
ed96aa820e Updated version and assets for release v23.05.1 2023-05-08 16:05:50 +01:00
Dan Brown
63ec079b7b Merge branch 'development' into release 2023-05-08 16:04:51 +01:00
Dan Brown
c17906c758 Updated translator attribution before release v23.05.1 2023-05-08 16:04:02 +01:00
Dan Brown
62d5701578 Merge pull request #4229 from BookStackApp/cli-update
Updated system CLI
2023-05-08 15:21:04 +01:00
Dan Brown
9f1a6947ab Updated system CLI
- Fixed wrong env details being used on restore.
- Updated update-url on restore actually work.
- Added better support for symlinked locations.
- Added warning against updating in docker-like (non git controlled)
  environments.
2023-05-08 15:16:30 +01:00
Dan Brown
ae90776927 Updated translations with latest Crowdin changes (#4211) 2023-05-08 14:49:01 +01:00
Dan Brown
4489f65371 Fixed code block line-number bar showing in exports
Also fixed in print view.
Likely crept in during CM6 changes.

For #4215
2023-05-08 14:45:45 +01:00
Dan Brown
ee1e047964 Updated php deps, formatted command changes 2023-05-08 14:37:01 +01:00
Dan Brown
8846f7d255 Prevented shorcuts activating when in codemirror areas
For #4227
2023-05-08 14:28:03 +01:00
Dan Brown
2523cee0e2 WYSWIYG code blocks: copied head styles into shadow root
Currently only link-based styles are made available in the shadow root
code editor environment, this adds normal styles to apply any user-added
via custom head content.

Fixes #4228
2023-05-08 12:21:53 +01:00
Dan Brown
b5cc0a8e38 Fixed added padding around hr tags in details blocks
Due to manual handling & wrapping of non-block content in details block
not taking hr elements into account.
For #3963
2023-05-08 12:01:52 +01:00
Dan Brown
3bcbf6b9c5 Added WYSWIYG editor code editor cancel focus return
Focus now returns to the editor properly when you quit out the code
editor without saving.
This also sets the return location to be correct on normal saving (Would
sometimes jump to the end of the document).

For #4109.
2023-05-07 19:36:10 +01:00
Dan Brown
573bc3ec45 Added force option for update-url command
Includes test to cover.
Closes #4223
2023-05-06 23:05:25 +01:00
Dan Brown
d485fcb3db Updated version and assets for release v23.05 2023-05-03 11:05:33 +01:00
Dan Brown
0f895668a4 Merge branch 'development' into release 2023-05-03 11:03:29 +01:00
Dan Brown
57bdd83d8c Added mostodon badge in readme, updated CLI 2023-05-03 10:57:09 +01:00
Dan Brown
ce0b75294f Set page include limit to be 3 as expected instead of 4 2023-05-02 12:44:55 +01:00
Dan Brown
4bb2b31bc9 Updated translator attribution pre v23.05 release 2023-05-01 19:39:20 +01:00
Dan Brown
9d74508ae3 Updated translations with latest Crowdin changes (#4163) 2023-05-01 19:37:49 +01:00
Dan Brown
c41baa1b76 Updated CLI & PHP deps, added gitignore for local composer 2023-05-01 18:44:46 +01:00
Dan Brown
cd32597d4d Fixed broken favourites in code editor 2023-05-01 18:43:03 +01:00
Dan Brown
8594656f6e Merge pull request #4206 from BookStackApp/system_cli
Added System CLI
2023-04-28 19:17:38 +01:00
Dan Brown
0aca1c2332 Added system cli, and created backups directory 2023-04-28 19:08:45 +01:00
Dan Brown
8c738aedee Added sessionindex to SAML2 single logout request to idp
related to  #3936
2023-04-28 13:55:25 +01:00
Dan Brown
f64ce71afc Added oidc_id_token_pre_validate logical theme event
For #4200
2023-04-27 23:40:14 +01:00
Dan Brown
277d5392fb Merge branch 'esakkiraja100116/development' into development 2023-04-27 16:34:14 +01:00
Dan Brown
23c35af9ef Review of #4202, Rolled out to other searches, added testing 2023-04-27 16:33:24 +01:00
esakkiraja100116
78fecdfcb0 suggesstion issue fix (#4175) 2023-04-27 16:32:39 +01:00
SnowCode
a9d952560d Adding a video { width: 100%; } (#4204)
* Adding a video { width: 100%; }

This is to prevent that videos included in pages don't exceed the page border

* Reverting precedent commit

* Adding a video { max-width: 100% } instead
2023-04-27 15:58:35 +01:00
Dan Brown
56f234d1ee Review of #4192, Fixed formatting and added test 2023-04-27 15:52:16 +01:00
jasonF1000
011800d425 changed PageContent.php to accept nested includes (#4192)
* changed app/Entities/Tools/PageContent.php to accept nested include levels. Tested it and it works.

* changed recommendations

This loop is now only around parsePageIncludes and bugfixes the space indentation.

* Update PageContent.php

fix spaces
2023-04-27 15:51:46 +01:00
Dan Brown
647ce6c237 Fixed sort urls with no params not building full path
The provided partial path would be return which may not resolve to the
full URL when used on systems like those hosting BookStack on a
sub-path.
Fixes #4201
2023-04-27 13:49:22 +01:00
Dan Brown
607da73109 Merge pull request #4193 from BookStackApp/custom_dropzone
Custom dropzone implementation
2023-04-27 13:43:38 +01:00
Dan Brown
1135d477ba Fixed linting and failing test issues from dropzone work 2023-04-27 13:31:03 +01:00
Dan Brown
a4a96a3df7 Dropzone: Adjusted styles for dark mode 2023-04-27 12:55:05 +01:00
Dan Brown
38e8a96dcd Removed dropzone from package and attribution list 2023-04-26 23:35:25 +01:00
Dan Brown
9a17656f88 dropzone: Addressed existing todos, cleaned attachment ux
Updated dom layout of attahcments to prevent nested dropzones (No issue
but potential to be one) and updated edit form dropzone handling so the
dropzone item card was not as distracting.
2023-04-26 23:31:38 +01:00
Dan Brown
e36cdaad0d Updated attachments to work with new dropzone
- Fixes existing broken attachment edit tabs.
- Redesigns area to move away from old tabbed interface.
- Integrates new dropzone system, for both addition and edit.
2023-04-26 16:41:34 +01:00
Dan Brown
722c38d576 Image manager: fix upload control for drawing, updated styles
- Tightened image manager styles to address things that looked akward.
- Prevented visiblity/use of upload controls for drawings.
- Updated dropzone to use error handling from validation messages.
2023-04-26 14:25:56 +01:00
Dan Brown
8cd6c797e8 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2023-04-26 01:43:16 +01:00
Dan Brown
dff45e2c5d Fixed broken shortcut hint overlay
Also updated event handler usage to use abort controller while there.
2023-04-26 01:42:12 +01:00
Dan Brown
61d2ea6ac7 Dropzone: Polished image manager elements
- Added file placeholder for non-image uploads.
- Added use of upload limits.
- Removed upload timeout variable.
- Added pass-through and usage of filetypes.
- Extracted some view text to language files and made use of existing
  text.
2023-04-25 16:41:39 +01:00
Esakkiraja
752562d23d .vscode folder is added in .gitignore file (#4197)
Squash of 7 commits.

---------

Co-authored-by: esakkiraja100116 <esakkiraja100116@gmai.com>
2023-04-25 15:25:31 +01:00
Dan Brown
b21a9007c5 Dropzone: Developed ux further
- Added image manager button for uploads.
- Added image manager placeholder sidebar text for guidance.
- Improved dropzone layer styling.
- Removed old dropzone styles.
- Got success events and auto-hide working.
- Updated upload items to animate out.
2023-04-25 13:10:25 +01:00
Dan Brown
a8fc29a31e Dropzone: started on design/ui of uploading
- Added new wider target handling.
- Updated upload item dom with design and seperate "landing" zone.
- Added new helper for simple dom element creation.
2023-04-24 23:24:58 +01:00
Dan Brown
36116a45d4 Dropzone: Swapped fetch for XHR for progress tracking 2023-04-24 18:18:08 +01:00
Dan Brown
23915c3b1a Started custom dropzone implementation 2023-04-24 16:19:20 +01:00
Dan Brown
55af22b487 Merge pull request #4191 from tigsikram/fix-api-docs-timestamp
Fix timestamp in API docs example response
2023-04-24 14:46:40 +01:00
Mark Weiler
01f3f4d315 Fix timestamp in API docs example response 2023-04-24 11:19:00 +02:00
Dan Brown
58cadce052 Merge branch 'feature/mail-verify-peer' into development 2023-04-23 15:05:13 +01:00
Dan Brown
1de72d09ca Mail: updated peer verify option name and added test 2023-04-23 15:04:35 +01:00
Dan Brown
fa6fcc1c1c Added clojure code language option
For #4112
2023-04-23 14:16:31 +01:00
Dan Brown
a46b438a4c Merge branch 'wkhtmltopdf-env-example' into development 2023-04-21 11:56:31 +01:00
Dan Brown
7505443a0c Updated complete env wkhtml text and added advisory
Added advisory to start to refer to docs for full details.
Updated added WKHTMLTOPDF option text.
2023-04-21 11:54:23 +01:00
Dan Brown
f837083c12 Updated php deps 2023-04-21 11:37:41 +01:00
Dan Brown
e1bd13f481 Edits from reviewing public events page 2023-04-20 16:54:11 +01:00
Dan Brown
c74f7cc628 Documented public JS events used
Related to #4179
2023-04-20 16:25:48 +01:00
Dan Brown
9f467f4052 Merge pull request #4181 from BookStackApp/js_formatting
Added standard JS formatting via ESLint
2023-04-19 23:01:10 +01:00
Dan Brown
974390688d ESLINT: Added GH action and details to dev docs 2023-04-19 22:56:55 +01:00
Dan Brown
da3ae3ba8b ESLINT: Addressed remaining detected issues 2023-04-19 15:20:04 +01:00
Dan Brown
0519e58fbf ESLINT: Started inital pass at addressing issues 2023-04-19 10:46:13 +01:00
Dan Brown
e711290d8b Ran eslint fix on existing codebase
Had to do some manual fixing of the app.js file due to misplaced
comments
2023-04-18 22:20:02 +01:00
Dan Brown
752ee664c2 Added code formatting standard via eslint 2023-04-18 22:19:27 +01:00
Dan Brown
69d03042c6 Merge pull request #3617 from BookStackApp/codemirror6
Upgrade to codemirror 6
2023-04-18 15:35:39 +01:00
Dan Brown
baf5edd73a CM6: Further fixes/improvements after testing
- Updated event naming to be "cm6" when codemirror-specific.
- Removed cm block border in md editor to prevent double bordering.
- Updated copy handling to fallback to execCommand.
2023-04-18 15:08:17 +01:00
Dan Brown
3e738b1471 CM6: Fixed a range of issues during browser testing
- Fixed some keybindings not running as expected, due to some editor
  defaults overriding or further actions taking place since the action
  would not indicate it's been dealt with (by returning boolean).
- Fixed spacing/border-radius being used on codeblocks on non-intended
  areas like the MD editor.
- Fixed lack of BG on default light theme, visible on full screen md
  editor.
- Fixed error thrown when the user does not have access to change the
  current editor (Likely non-cm related existing issue)
2023-04-18 14:21:22 +01:00
Dan Brown
94f464cd14 CM6: Added tabbing, fixed dark mode border in WYSIWYG 2023-04-18 13:43:59 +01:00
Dan Brown
900571ac9c CM6: Updated for popup editor, added new interface
New simple interface added for abstraction of CM editor in simple
use-cases, just to provide common actions like get/set content, focus
and set mode.
2023-04-17 13:24:29 +01:00
Dan Brown
09fd0bc5b7 CM6: Got WYSIWYG code blocks working
Required monkey-patch to work around potential codemirror issue with
shadowdom+iframe usage.
Also updated JS packages to latest versions.
2023-04-16 23:50:11 +01:00
Dan Brown
74b4751a1c CM6: Aligned styling with existing, improved theme handling 2023-04-16 16:05:16 +01:00
Dan Brown
74b76ecdb9 Updated cm6 theme handling to allow extension via API
Uses our custom event system, uses methods that take callables so that
internal dependancies can be passed.
2023-04-15 15:35:41 +01:00
Dan Brown
9874a53206 Added cm6 strategy for splitting and dyn. loading langs
Split out legacy modes to their own dynamically imported bundle to
reduce main code bundle size.
2023-04-14 18:08:57 +01:00
Dan Brown
257a703878 Addressed existing cm6 todos
- Updated clipboard handling
  - Removed old clipboard package for browser-native API.
- Updated codemirror editor events to use new props for new data types.
2023-04-14 14:08:40 +01:00
Dan Brown
fdda813d5f Cleaned up change handling in cm6 editor action handling 2023-04-13 17:38:11 +01:00
Dan Brown
6f45d34bf8 Finished update pass of all md editor actions to cm6 2023-04-13 17:18:32 +01:00
Dan Brown
32c765d0c3 Updated another range of actions for cm6 2023-04-13 12:51:52 +01:00
Dan Brown
9813c94720 Made a start on updating editor actions 2023-04-11 13:16:04 +01:00
Dan Brown
da3e4f5f75 Got md shortcuts working, marked actions for update 2023-04-11 11:48:58 +01:00
Dan Brown
572037ef1f Got markdown editor barely functional
Updated content sync and preview scoll sync to work.
Many features commented out until they can be updated.
2023-04-10 15:01:44 +01:00
Dan Brown
50f3c10f19 Merge branch 'v23.02-branch' into development 2023-04-07 18:12:00 +01:00
Dan Brown
6c577ac3bf Updated version and assets for release v23.02.3 2023-04-07 18:07:32 +01:00
Dan Brown
31cc2423d2 Merge branch 'v23.02-branch' into release 2023-04-07 18:07:09 +01:00
Dan Brown
3f3f221e0d Updated translator attribution before release v23.02.3 2023-04-07 18:06:44 +01:00
Dan Brown
d0f970fe4f Updated translations with latest Crowdin changes (#4131) 2023-04-07 18:00:03 +01:00
Dan Brown
95b75c067f Updated translations with latest Crowdin changes (#4131) 2023-04-07 17:59:34 +01:00
Dan Brown
81134e7071 Fixed tag numbering in last commit 2023-04-07 17:54:17 +01:00
Dan Brown
e722ee4268 Fixed click issue with tag suggestions in safari
Updated selectable elements to be divs instead of buttons since Safari
akwardly does not focus on buttons on click.
Also standardised keyboard handling to our standard nav class.
Also addressed empty tag values showing in results.
For #4139
2023-04-07 17:50:57 +01:00
Dan Brown
fd674d10e3 Fixed error upon user delete with no migration id
Fixes #4162
2023-04-07 15:57:21 +01:00
Dan Brown
4835a0dcb1 Cleaned up old token services 2023-04-04 10:44:38 +01:00
Daiki Urata
d353e87ca1 Add WKHTMLTOPDF to .env.example.complete 2023-03-30 17:58:17 +09:00
Dan Brown
8e64324d62 Merge branch 'v23.02-branch' into development 2023-03-25 12:33:59 +00:00
Dan Brown
c9ed32e518 Updated version and assets for release v23.02.2 2023-03-25 12:27:32 +00:00
Dan Brown
6b4c3a0969 Merge branch 'v23.02-branch' into release 2023-03-25 12:27:05 +00:00
Dan Brown
0a0fdd7f3e Fixed delete role failing with no migrate role provided
For #4128
2023-03-25 12:21:22 +00:00
Dan Brown
3410cf21cb Updated php deps 2023-03-25 12:21:04 +00:00
Dan Brown
6e284d7a6c Fixed issue with user delete ownership not migrating
Caused by input not being part of the submitted form.
Updated test to ensure the input is within a form.
For #4124
2023-03-25 12:20:49 +00:00
Dan Brown
ea7914422c Updated php deps 2023-03-25 12:20:13 +00:00
Dan Brown
509cab3e28 Merged latest crowdin changes 2023-03-25 12:18:45 +00:00
Dan Brown
dde38e91b5 Fixed delete role failing with no migrate role provided
For #4128
2023-03-25 12:08:45 +00:00
Dan Brown
970088a8a1 Updated php deps 2023-03-24 14:46:30 +00:00
Dan Brown
0e43618dda Fixed issue with user delete ownership not migrating
Caused by input not being part of the submitted form.
Updated test to ensure the input is within a form.
For #4124
2023-03-24 14:43:48 +00:00
Vincent Bernat
f2293a70f8 Allow a user to disable peer check when using TLS/STARTTLS
This is useful when developing and on Docker setups. Despite setting
encryption to null, if a server supports STARTTLS with a self-signed
certificate, the mailer try to upgrade the connection with STARTTLS.
2023-03-24 09:34:37 +01:00
Dan Brown
dce5123452 Added own twig/smarty packages for cm6 lang support 2023-03-21 20:53:35 +00:00
Dan Brown
c81cb6f2af Merge branch 'development' into codemirror6 2023-03-19 10:22:44 +00:00
Dan Brown
9b66e93b15 Merge pull request #4103 from BookStackApp/image_api
Image API Endpoints
2023-03-15 11:45:36 +00:00
Dan Brown
402eb845ab Added examples, updated docs for image gallery api endpoints 2023-03-15 11:37:03 +00:00
Dan Brown
3a808fd768 Added phpunit tests to cover image API endpoints 2023-03-14 19:29:08 +00:00
Dan Brown
d9eec6d82c Started Image API build 2023-03-14 12:19:19 +00:00
Dan Brown
6357056d7b Updated php deps 2023-03-13 21:03:00 +00:00
Dan Brown
a369971e04 Merge pull request #4099 from BookStackApp/permissions_api
Content-Permissions API Endpoints
2023-03-13 20:55:44 +00:00
Dan Brown
1903924829 Added content-perms API examples and docs tweaks 2023-03-13 20:41:32 +00:00
Dan Brown
0de7530059 Tweaked content permission endpoints, covered with tests 2023-03-13 20:06:52 +00:00
Dan Brown
c42956bcaf Started build of content-permissions API endpoints 2023-03-13 13:18:33 +00:00
Dan Brown
7b5111571c Removed bookstack wording instances in color setting options 2023-02-28 01:01:25 +00:00
Dan Brown
2dad92d1bd Updated version and assets for release v23.02.1 2023-02-27 19:26:13 +00:00
Dan Brown
c1fb7ab7dc Merge branch 'development' into release 2023-02-27 19:23:33 +00:00
Dan Brown
3464f5e961 Updated translations with latest Crowdin changes (#4066) 2023-02-27 19:19:03 +00:00
Dan Brown
7c27d26161 Fixed language locale setting issue
Attempted to access an array that had been filtered and therefore could
have holes within, including as position 0 which would then be
accessed.
Also added cs language to internal map

Related to #4068
2023-02-27 19:14:45 +00:00
Dan Brown
98315f3899 Updated version and assets for release v23.02 2023-02-26 11:03:49 +00:00
Dan Brown
8c82aaabd6 Merge branch 'development' into release 2023-02-26 11:02:56 +00:00
Dan Brown
c7e33d1981 Fixed caching issue when running tests 2023-02-26 10:50:14 +00:00
Dan Brown
ba21b54195 Updated translations with latest Crowdin changes (#4025) 2023-02-26 10:36:15 +00:00
Dan Brown
f35c42b0b8 Updated php deps and translaters in prep for v23.02 2023-02-25 17:35:21 +00:00
Dan Brown
b88b1bef2c Added updated_at index to pages table
This has a large impact on some areas where latest updated pages are
shown, such as the homepage for example.
2023-02-23 23:06:12 +00:00
Dan Brown
8abb41abbd Added caching to the loading of system roles
Admin system role was being loaded for each permission check performed.
This caches the fetching for the request lifetime.
2023-02-23 23:01:03 +00:00
Dan Brown
a031edec16 Fixed old deprecated encoding convert on HTML doc load 2023-02-23 22:59:26 +00:00
Dan Brown
2724b2867b Merge pull request #4062 from BookStackApp/settings_perf
Changed the way settings are loaded
2023-02-23 22:22:32 +00:00
Dan Brown
8bebea4cca Changed the way settings are loaded
This new method batch-loads them from the database, and removes the
cache-layer with the intention that a couple of batch fetches from the
DB is more efficient than hitting the cache each time.
2023-02-23 22:14:47 +00:00
Dan Brown
6545afacd6 Changed autosave handling for better editor performance
This changes how the editors interact with the parent page-editor
compontent, which handles auto-saving.
Instead of blasting the full editor content upon any change to that
parent compontent, the editors just alert of a change, without the
content. The parent compontent then requests the editor content from the
editor component when it needs that data for an autosave.

For #3981
2023-02-23 12:30:27 +00:00
Dan Brown
31495758a9 Made page-save HTML formatting much more efficient
Replaced the existing xpath-heavy system with a more manual traversal
approach. Fixes following slow areas of old system:
- Old system would repeat ID-setting action for elements (Headers could
  be processed up to three times).
- Old system had a few very open xpath queries for headers.
- Old system would update links on every ID change, which triggers it's
  own xpath query for links, leading to exponential scaling issues.

New system only does one xpath query for links when changes are needed.
Added test to cover.

For #3932
2023-02-22 14:32:40 +00:00
Dan Brown
c80396136f Increased attachment link limit from 192 to 2k
Added test to cover.
Did attempt a 64k limit, but values over 2k significantly increase
chance of other issues since this URL may be used in redirect headers.
Would rather catch issues in-app.

For #4044
2023-02-20 13:05:23 +00:00
Dan Brown
8da3e64039 Updated language files to remove literal "1" values
This is to encourge the ":count" values to be used instead of 1s in the
translated variants so that non-pluralised languages are hardcoded with
"1"s in their content, even when not used in a singular context.

For #4040
2023-02-20 12:05:52 +00:00
Dan Brown
c1167f8821 Merge pull request #4051 from BookStackApp/roles_api
User Roles API Endpoint
2023-02-19 16:11:30 +00:00
Dan Brown
4176b598ce Fixed unselectable checkbox role form options 2023-02-19 16:03:50 +00:00
Dan Brown
950c02e996 Added role API responses & requests
Also applied other slight tweaks and comment updates based upon manual
endpoint testing.
2023-02-19 15:58:29 +00:00
Dan Brown
9502f349a2 Updated test to have reliable check ordering 2023-02-18 19:01:38 +00:00
Dan Brown
3c3c2ae9b5 Set order to role permissions API response 2023-02-18 18:50:01 +00:00
Dan Brown
723f108bd9 Aded roles API controller methods
Altered & updated permissions repo, and existing connected
RoleController to suit.
Also extracts in-app success notifications to auto activity system.
Tweaked tests where required.
2023-02-18 18:36:34 +00:00
Dan Brown
55456a57d6 Added tests for not-yet-built role API endpoints 2023-02-18 13:51:18 +00:00
Dan Brown
c148e2f3d9 Added esbuild bundle inspection metafile 2023-02-17 22:37:13 +00:00
Dan Brown
f51036b203 Added newer languages where possible
Cannot find existing option for twig/smarty, need to look other methods.
2023-02-17 22:14:34 +00:00
Dan Brown
9135a85de4 Merge branch 'codemirror6' into codemirror6_take2 2023-02-17 21:28:23 +00:00
Dan Brown
fd45d280b4 Updated tinymce from 6.1.0 to 6.3.1 2023-02-17 21:16:42 +00:00
Dan Brown
524adce654 Merge pull request #4049 from BookStackApp/shelf_book_sort_updates
Shelf book sort improvements
2023-02-17 16:20:59 +00:00
Dan Brown
f799c9b260 Applied shelf book sort changes from testing
Added better labelling of sort lists for screen readers.
Fadded out sort-item action buttons until hovering for a cleaner look.
2023-02-17 16:18:24 +00:00
Dan Brown
9c26ccf43d Added shelf book item sort action functionality
Adds JS logic, and dropdown action list, for quick-sorting the book
shelf list in addition to handling the book item action buttons.
2023-02-17 15:53:24 +00:00
Dan Brown
71a09bcf6e Started accessible controls for shelf book sort
Added buttons and fit to design.
Added new icon variations to support.
Extracted book item to own view and setup for future auto sorts.
2023-02-17 15:05:28 +00:00
Dan Brown
af31a6fc1b Made sendmail command configurable
For #4001
Added simple test to cover config option.
2023-02-17 14:25:38 +00:00
Dan Brown
08b39500b3 Fixed gallery images not visible until draft publish
For #4028
2023-02-16 17:57:34 +00:00
Dan Brown
f9fcc9f3c7 Updated php deps 2023-02-16 17:27:09 +00:00
Dan Brown
0812184995 Added torutec as sponsor, updated license and version 2023-02-14 16:16:08 +00:00
Dan Brown
646f8f60c0 Merge pull request #4032 from BookStackApp/favicon
Generate favicon.ico file
2023-02-09 21:37:38 +00:00
Dan Brown
f333db8e4f Added control-upon-access of the default favicon.ico file 2023-02-09 21:16:27 +00:00
Dan Brown
da42fc7457 Added default favicon creation upon access. 2023-02-09 20:57:35 +00:00
Dan Brown
48f1934387 Updated favicon gen to use png-based ICO
From testing, worked on Firefox, Chrome, Gnome Web
2023-02-09 17:47:33 +00:00
Dan Brown
2845e0003e Got favicons better supported, can't get transparency right
Digging deeper, I don't think PHPGD supports 32bit bmp output which
complicates matters.
2023-02-09 15:14:41 +00:00
Dan Brown
1a189640f1 Integrated favicon handler with correct files & actions
Format does not look 100% correct though, won't show in Firefox/gimp.
2023-02-09 13:24:43 +00:00
Dan Brown
420f89af99 Built custom favicon.ico file creator
Followed wikipedia-defined ICO file format info, and used with
Intervention's good bmp support, to create a working proof-of-concept.
2023-02-08 23:06:42 +00:00
Dan Brown
da1a66abd3 Extracted test file handling to its own class
Closes #3995
2023-02-08 14:39:13 +00:00
Dan Brown
5d18e7df79 Removed deprecated syntax in old migration file 2023-02-08 13:20:00 +00:00
Dan Brown
ba25a3e1b7 Merge pull request #4021 from BookStackApp/laravel9
Upgrade framework to Laravel 9
2023-02-07 12:11:04 +00:00
Dan Brown
bc18dc7da6 Removed parallel testing, updated predis
Parallel testing paratest library caused issues due to a single version
not being compatibile across our php range. Removed for now as not
really worth the faff to get compatible.
2023-02-07 11:50:59 +00:00
Dan Brown
5e8ec56196 Fixed issues found from tests 2023-02-06 20:41:33 +00:00
Dan Brown
9ca088a4e2 Fixed static analysis issues 2023-02-06 20:00:44 +00:00
Dan Brown
008e7a4d25 Followed Laravel 9 update steps and file changes 2023-02-06 16:58:29 +00:00
Dan Brown
ce9b536b78 Updated version and assets for release v23.01.1 2023-02-02 12:29:26 +00:00
Dan Brown
d9c50e5bc1 Merge branch 'development' into release 2023-02-02 12:29:07 +00:00
Dan Brown
6e6f113336 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2023-02-02 12:17:06 +00:00
Dan Brown
f7441e2abc Updated translations with latest Crowdin changes (#4008) 2023-02-02 12:16:56 +00:00
Dan Brown
28c168145f Added missing app icon image
Fixes #4006
2023-02-02 11:49:06 +00:00
Dan Brown
c2115cab59 Updated php depenencies 2023-02-02 11:44:25 +00:00
Dan Brown
9fd7a6abed Added dark theme handling 2022-08-04 14:19:04 +01:00
Dan Brown
4757ed9453 Converted codemirror languges to new packages where available
Does increase bundle size massively though, Will need to think about
solutions for this.
2022-08-04 13:33:51 +01:00
Dan Brown
97146a6359 Added handling of codemirror 6 code languages 2022-08-03 19:40:16 +01:00
Dan Brown
d4f2fcdf79 Started codemirror update, In broken state 2022-08-02 20:11:02 +01:00
922 changed files with 14585 additions and 11604 deletions

View File

@@ -3,6 +3,10 @@
# Each option is shown with it's default value.
# Do not copy this whole file to use as your '.env' file.
# The details here only serve as a quick reference.
# Please refer to the BookStack documentation for full details:
# https://www.bookstackapp.com/docs/
# Application environment
# Can be 'production', 'development', 'testing' or 'demo'
APP_ENV=production
@@ -79,6 +83,10 @@ MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_VERIFY_SSL=true
# Command to use when email is sent via sendmail
MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
@@ -319,6 +327,13 @@ FILE_UPLOAD_SIZE_LIMIT=50
# Can be 'a4' or 'letter'.
EXPORT_PAGE_SIZE=a4
# Set path to wkhtmltopdf binary for PDF generation.
# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf'
# When false, BookStack will attempt to find a wkhtmltopdf in the application
# root folder then fall back to the default dompdf renderer if no binary exists.
# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
WKHTMLTOPDF=false
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
@@ -369,4 +384,4 @@ LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver
# IP address '146.191.42.4' would result in '146.191.x.x' being logged.
# For the IPv6 address '2001:db8:85a3:8d3:1319:8a2e:370:7348' this would result as:
# '2001:db8:85a3:8d3:x:x:x:x'
IP_ADDRESS_PRECISION=4
IP_ADDRESS_PRECISION=4

View File

@@ -308,3 +308,24 @@ Adrian Ocneanu (aocneanu) :: Romanian
Eduardo Castanho (EduardoCastanho) :: Portuguese
VIET NAM VPS (vietnamvps) :: Vietnamese
m4tthi4s :: French
toras9000 :: Japanese
pathab :: German
MichelSchoon85 :: Dutch
Jøran Haugli (haugli92) :: Norwegian Bokmal
Vasileios Kouvelis (VasilisKouvelis) :: Greek
Dremski :: Bulgarian
Frédéric SENE (nothingfr) :: French
bendem :: French
kostasdizas :: Greek
Ricardo Schroeder (brownstone666) :: Portuguese, Brazilian
Eitan MG (EitanMG) :: Hebrew
Robin Flikkema (RobinFlikkema) :: Dutch
Michal Gurcik (mgurcik) :: Slovak
Pooyan Arab (pooyanarab) :: Persian
Ochi Darma Putra (troke12) :: Indonesian
H.-H. Peng (Hsins) :: Chinese Traditional
Mosi Wang (mosiwang) :: Chinese Traditional
骆言 (LawssssCat) :: Chinese Simplified
Stickers Gaming Shøw (StickerSGSHOW) :: French
Le Van Chinh (Chino) (lvanchinh86) :: Vietnamese
Rubens nagios (rubenix) :: Catalan

16
.github/workflows/lint-js.yml vendored Normal file
View File

@@ -0,0 +1,16 @@
name: lint-js
on: [push, pull_request]
jobs:
build:
if: ${{ github.ref != 'refs/heads/l10n_development' }}
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v1
- name: Install NPM deps
run: npm ci
- name: Run formatting check
run: npm run lint

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2']
php: ['8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1

View File

@@ -8,7 +8,7 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
php: ['7.4', '8.0', '8.1', '8.2']
php: ['8.0', '8.1', '8.2']
steps:
- uses: actions/checkout@v1

7
.gitignore vendored
View File

@@ -1,5 +1,7 @@
/vendor
/node_modules
/.vscode
/composer
Homestead.yaml
.env
.idea
@@ -11,6 +13,7 @@ yarn-error.log
/public/js/*.map
/public/bower
/public/build/
/public/favicon.ico
/storage/images
_ide_helper.php
/storage/debugbar
@@ -20,8 +23,10 @@ yarn.lock
nbproject
.buildpath
.project
.nvmrc
.settings/
webpack-stats.json
.phpunit.result.cache
.DS_Store
phpstan.neon
phpstan.neon
esbuild-meta.json

View File

@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2015-2022, Dan Brown and the BookStack Project contributors.
Copyright (c) 2015-2023, 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

@@ -11,11 +11,9 @@ use Illuminate\Support\Facades\DB;
class TagRepo
{
protected PermissionApplicator $permissions;
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
public function __construct(
protected PermissionApplicator $permissions
) {
}
/**
@@ -90,6 +88,7 @@ class TagRepo
{
$query = Tag::query()
->select('*', DB::raw('count(*) as count'))
->where('value', '!=', '')
->groupBy('value');
if ($searchTerm) {

View File

@@ -8,8 +8,8 @@ use BookStack\Notifications\ConfirmEmail;
class EmailConfirmationService extends UserTokenService
{
protected $tokenTable = 'email_confirmations';
protected $expiryTime = 24;
protected string $tokenTable = 'email_confirmations';
protected int $expiryTime = 24;
/**
* Create new confirmation for a user,

View File

@@ -4,35 +4,16 @@ namespace BookStack\Auth\Access\Oidc;
class OidcIdToken
{
/**
* @var array
*/
protected $header;
/**
* @var array
*/
protected $payload;
/**
* @var string
*/
protected $signature;
protected array $header;
protected array $payload;
protected string $signature;
protected string $issuer;
protected array $tokenParts = [];
/**
* @var array[]|string[]
*/
protected $keys;
/**
* @var string
*/
protected $issuer;
/**
* @var array
*/
protected $tokenParts = [];
protected array $keys;
public function __construct(string $token, string $issuer, array $keys)
{
@@ -106,6 +87,14 @@ class OidcIdToken
return $this->payload;
}
/**
* Replace the existing claim data of this token with that provided.
*/
public function replaceClaims(array $claims): void
{
$this->payload = $claims;
}
/**
* Validate the structure of the given token and ensure we have the required pieces.
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.

View File

@@ -9,6 +9,8 @@ use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\StoppedAuthenticationException;
use BookStack\Exceptions\UserRegistrationException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Cache;
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
@@ -21,24 +23,12 @@ use Psr\Http\Client\ClientInterface as HttpClient;
*/
class OidcService
{
protected RegistrationService $registrationService;
protected LoginService $loginService;
protected HttpClient $httpClient;
protected GroupSyncService $groupService;
/**
* OpenIdService constructor.
*/
public function __construct(
RegistrationService $registrationService,
LoginService $loginService,
HttpClient $httpClient,
GroupSyncService $groupService
protected RegistrationService $registrationService,
protected LoginService $loginService,
protected HttpClient $httpClient,
protected GroupSyncService $groupService
) {
$this->registrationService = $registrationService;
$this->loginService = $loginService;
$this->httpClient = $httpClient;
$this->groupService = $groupService;
}
/**
@@ -226,6 +216,16 @@ class OidcService
$settings->keys,
);
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
'access_token' => $accessToken->getToken(),
'expires_in' => $accessToken->getExpires(),
'refresh_token' => $accessToken->getRefreshToken(),
]);
if (!is_null($returnClaims)) {
$idToken->replaceClaims($returnClaims);
}
if ($this->config()['dump_user_details']) {
throw new JsonDebugException($idToken->getAllClaims());
}

View File

@@ -67,7 +67,7 @@ class Saml2Service
$returnRoute,
[],
$user->email,
null,
session()->get('saml2_session_index'),
true,
Constants::NAMEID_EMAIL_ADDRESS
);
@@ -118,6 +118,7 @@ class Saml2Service
$attrs = $toolkit->getAttributes();
$id = $toolkit->getNameId();
session()->put('saml2_session_index', $toolkit->getSessionIndex());
return $this->processLoginCallback($id, $attrs);
}

View File

@@ -7,14 +7,12 @@ use BookStack\Notifications\UserInvite;
class UserInviteService extends UserTokenService
{
protected $tokenTable = 'user_invites';
protected $expiryTime = 336; // Two weeks
protected string $tokenTable = 'user_invites';
protected int $expiryTime = 336; // Two weeks
/**
* Send an invitation to a user to sign into BookStack
* Removes existing invitation tokens.
*
* @param User $user
*/
public function sendInvitation(User $user)
{

View File

@@ -14,41 +14,29 @@ class UserTokenService
{
/**
* Name of table where user tokens are stored.
*
* @var string
*/
protected $tokenTable = 'user_tokens';
protected string $tokenTable = 'user_tokens';
/**
* Token expiry time in hours.
*
* @var int
*/
protected $expiryTime = 24;
protected int $expiryTime = 24;
/**
* Delete all email confirmations that belong to a user.
*
* @param User $user
*
* @return mixed
* Delete all tokens that belong to a user.
*/
public function deleteByUser(User $user)
public function deleteByUser(User $user): void
{
return DB::table($this->tokenTable)
DB::table($this->tokenTable)
->where('user_id', '=', $user->id)
->delete();
}
/**
* Get the user id from a token, while check the token exists and has not expired.
*
* @param string $token
* Get the user id from a token, while checking the token exists and has not expired.
*
* @throws UserTokenNotFoundException
* @throws UserTokenExpiredException
*
* @return int
*/
public function checkTokenAndGetUserId(string $token): int
{
@@ -67,8 +55,6 @@ class UserTokenService
/**
* Creates a unique token within the email confirmation database.
*
* @return string
*/
protected function generateToken(): string
{
@@ -82,10 +68,6 @@ class UserTokenService
/**
* Generate and store a token for the given user.
*
* @param User $user
*
* @return string
*/
protected function createTokenForUser(User $user): string
{
@@ -102,10 +84,6 @@ class UserTokenService
/**
* Check if the given token exists.
*
* @param string $token
*
* @return bool
*/
protected function tokenExists(string $token): bool
{
@@ -115,12 +93,8 @@ class UserTokenService
/**
* Get a token entry for the given token.
*
* @param string $token
*
* @return object|null
*/
protected function getEntryByToken(string $token)
protected function getEntryByToken(string $token): ?stdClass
{
return DB::table($this->tokenTable)
->where('token', '=', $token)
@@ -129,10 +103,6 @@ class UserTokenService
/**
* Check if the given token entry has expired.
*
* @param stdClass $tokenEntry
*
* @return bool
*/
protected function entryExpired(stdClass $tokenEntry): bool
{

View File

@@ -5,7 +5,6 @@ namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
@@ -23,14 +22,14 @@ class EntityPermission extends Model
protected $fillable = ['role_id', 'view', 'create', 'update', 'delete'];
public $timestamps = false;
/**
* Get this restriction's attached entity.
*/
public function restrictable(): MorphTo
{
return $this->morphTo('restrictable');
}
protected $hidden = ['entity_id', 'entity_type', 'id'];
protected $casts = [
'view' => 'boolean',
'create' => 'boolean',
'read' => 'boolean',
'update' => 'boolean',
'delete' => 'boolean',
];
/**
* Get the role assigned to this entity permission.

View File

@@ -158,6 +158,11 @@ class PermissionApplicator
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where('pages.draft', '=', false);
})->orWhereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where('pages.draft', '=', true)
->where('pages.created_by', '=', $this->currentUser()->id);
});
});
}

View File

@@ -12,11 +12,8 @@ use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo
{
protected JointPermissionBuilder $permissionBuilder;
protected $systemRoles = ['admin', 'public'];
protected array $systemRoles = ['admin', 'public'];
/**
* PermissionsRepo constructor.
*/
public function __construct(JointPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
@@ -41,7 +38,7 @@ class PermissionsRepo
/**
* Get a role via its ID.
*/
public function getRoleById($id): Role
public function getRoleById(int $id): Role
{
return Role::query()->findOrFail($id);
}
@@ -52,10 +49,10 @@ class PermissionsRepo
public function saveNewRole(array $roleData): Role
{
$role = new Role($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$permissions = $roleData['permissions'] ?? [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
@@ -66,42 +63,45 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* Ensures Admin system role always have core permissions.
*/
public function updateRole($roleId, array $roleData)
public function updateRole($roleId, array $roleData): Role
{
$role = $this->getRoleById($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if (isset($roleData['permissions'])) {
$this->assignRolePermissions($role, $roleData['permissions']);
}
$role->fill($roleData);
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
return $role;
}
/**
* Assign a list of permission names to the given role.
*/
protected function assignRolePermissions(Role $role, array $permissionNameArray = []): void
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
// Ensure the admin system role retains vital system permissions
if ($role->system_name === 'admin') {
$permissions = array_merge($permissions, [
$permissionNameArray = array_unique(array_merge($permissionNameArray, [
'users-manage',
'user-roles-manage',
'restrictions-manage-all',
'restrictions-manage-own',
'settings-manage',
]);
]));
}
$this->assignRolePermissions($role, $permissions);
$role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
}
/**
* Assign a list of permission names to a role.
*/
protected function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray) {
if (!empty($permissionNameArray)) {
$permissions = RolePermission::query()
->whereIn('name', $permissionNameArray)
->pluck('id')
@@ -114,13 +114,13 @@ class PermissionsRepo
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* If a migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
*
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
public function deleteRole(int $roleId, int $migrateRoleId = 0): void
{
$role = $this->getRoleById($roleId);
@@ -131,7 +131,7 @@ class PermissionsRepo
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
if ($migrateRoleId) {
if ($migrateRoleId !== 0) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();

View File

@@ -8,6 +8,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsToMany;
/**
* @property int $id
* @property string $name
* @property string $display_name
*/
class RolePermission extends Model
{

View File

@@ -27,10 +27,14 @@ class Role extends Model implements Loggable
{
use HasFactory;
protected $fillable = ['display_name', 'description', 'external_auth_id'];
protected $fillable = ['display_name', 'description', 'external_auth_id', 'mfa_enforced'];
protected $hidden = ['pivot'];
protected $casts = [
'mfa_enforced' => 'boolean',
];
/**
* The roles that belong to the role.
*/
@@ -107,7 +111,13 @@ class Role extends Model implements Loggable
*/
public static function getSystemRole(string $systemName): ?self
{
return static::query()->where('system_name', '=', $systemName)->first();
static $cache = [];
if (!isset($cache[$systemName])) {
$cache[$systemName] = static::query()->where('system_name', '=', $systemName)->first();
}
return $cache[$systemName];
}
/**

View File

@@ -72,7 +72,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id',
'created_at', 'updated_at', 'image_id', 'roles', 'avatar', 'user_id', 'pivot',
];
/**

View File

@@ -8,6 +8,8 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
use Illuminate\Support\Facades\Facade;
return [
// The environment to run BookStack in.
@@ -98,7 +100,13 @@ return [
// Encryption cipher
'cipher' => 'AES-256-CBC',
// Application Services Provides
// Maintenance Mode Driver
'maintenance' => [
'driver' => 'file',
// 'store' => 'redis',
],
// Application Service Providers
'providers' => [
// Laravel Framework Service Providers...
@@ -141,58 +149,9 @@ return [
BookStack\Providers\ViewTweaksServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
// Class aliases, Registered on application start
'aliases' => [
// Laravel
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
// 'Redis' => Illuminate\Support\Facades\Redis::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
// Class Aliases
// This array of class aliases to be registered on application start.
'aliases' => Facade::defaultAliases()->merge([
// Laravel Packages
'Socialite' => Laravel\Socialite\Facades\Socialite::class,
@@ -202,7 +161,7 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Theme' => BookStack\Facades\Theme::class,
],
])->toArray(),
// Proxy configuration
'proxies' => env('APP_PROXIES', ''),

View File

@@ -14,7 +14,7 @@ return [
// 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' => env('BROADCAST_DRIVER', 'pusher'),
'default' => 'null',
// Broadcast Connections
// Here you may define all of the broadcast connections that will be used
@@ -22,21 +22,7 @@ return [
// each available type of connection are provided inside this array.
'connections' => [
'pusher' => [
'driver' => 'pusher',
'key' => env('PUSHER_APP_KEY'),
'secret' => env('PUSHER_APP_SECRET'),
'app_id' => env('PUSHER_APP_ID'),
'options' => [
'cluster' => env('PUSHER_APP_CLUSTER'),
'useTLS' => true,
],
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
],
// Default options removed since we don't use broadcasting.
'log' => [
'driver' => 'log',

View File

@@ -87,6 +87,6 @@ return [
|
*/
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache'),
'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_') . '_cache_'),
];

View File

@@ -33,17 +33,20 @@ return [
'driver' => 'local',
'root' => public_path(),
'visibility' => 'public',
'throw' => true,
],
'local_secure_attachments' => [
'driver' => 'local',
'root' => storage_path('uploads/files/'),
'throw' => true,
],
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
'visibility' => 'public',
'throw' => true,
],
's3' => [
@@ -54,6 +57,7 @@ return [
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
'throw' => true,
],
],

View File

@@ -21,6 +21,15 @@ return [
// one of the channels defined in the "channels" configuration array.
'default' => env('LOG_CHANNEL', 'single'),
// Deprecations Log Channel
// This option controls the log channel that should be used to log warnings
// regarding deprecated PHP and library features. This allows you to get
// your application ready for upcoming major versions of dependencies.
'deprecations' => [
'channel' => 'null',
'trace' => false,
],
// Log Channels
// Here you may configure the log channels for your application. Out of
// the box, Laravel uses the Monolog PHP logging library. This gives

View File

@@ -14,13 +14,7 @@ return [
// From Laravel 7+ this is MAIL_MAILER in laravel.
// Kept as MAIL_DRIVER in BookStack to prevent breaking change.
// Options: smtp, sendmail, log, array
'driver' => env('MAIL_DRIVER', 'smtp'),
// SMTP host address
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
// SMTP host port
'port' => env('MAIL_PORT', 587),
'default' => env('MAIL_DRIVER', 'smtp'),
// Global "From" address & name
'from' => [
@@ -28,17 +22,43 @@ return [
'name' => env('MAIL_FROM_NAME', 'BookStack'),
],
// Email encryption protocol
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
// Mailer Configurations
// Available mailing methods and their settings.
'mailers' => [
'smtp' => [
'transport' => 'smtp',
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
'port' => env('MAIL_PORT', 587),
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
'username' => env('MAIL_USERNAME'),
'password' => env('MAIL_PASSWORD'),
'verify_peer' => env('MAIL_VERIFY_SSL', true),
'timeout' => null,
'local_domain' => env('MAIL_EHLO_DOMAIN'),
],
// SMTP server username
'username' => env('MAIL_USERNAME'),
'sendmail' => [
'transport' => 'sendmail',
'path' => env('MAIL_SENDMAIL_COMMAND', '/usr/sbin/sendmail -bs'),
],
// SMTP server password
'password' => env('MAIL_PASSWORD'),
'log' => [
'transport' => 'log',
'channel' => env('MAIL_LOG_CHANNEL'),
],
// Sendmail application path
'sendmail' => '/usr/sbin/sendmail -bs',
'array' => [
'transport' => 'array',
],
'failover' => [
'transport' => 'failover',
'mailers' => [
'smtp',
'log',
],
],
],
// Email markdown configuration
'markdown' => [
@@ -47,11 +67,4 @@ return [
resource_path('views/vendor/mail'),
],
],
// Log Channel
// If you are using the "log" driver, you may specify the logging channel
// if you prefer to keep mail messages separate from other log entries
// for simpler reading. Otherwise, the default channel will be used.
'log_channel' => env('MAIL_LOG_CHANNEL'),
];

View File

@@ -14,7 +14,8 @@ class UpdateUrl extends Command
*/
protected $signature = 'bookstack:update-url
{oldUrl : URL to replace}
{newUrl : URL to use as the replacement}';
{newUrl : URL to use as the replacement}
{--force : Force the operation to run, ignoring confirmations}';
/**
* The console command description.
@@ -23,25 +24,12 @@ class UpdateUrl extends Command
*/
protected $description = 'Find and replace the given URLs in your BookStack database';
protected $db;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Connection $db)
{
$this->db = $db;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
public function handle(Connection $db)
{
$oldUrl = str_replace("'", '', $this->argument('oldUrl'));
$newUrl = str_replace("'", '', $this->argument('newUrl'));
@@ -67,7 +55,7 @@ class UpdateUrl extends Command
foreach ($columnsToUpdateByTable as $table => $columns) {
foreach ($columns as $column) {
$changeCount = $this->replaceValueInTable($table, $column, $oldUrl, $newUrl);
$changeCount = $this->replaceValueInTable($db, $table, $column, $oldUrl, $newUrl);
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
}
}
@@ -80,7 +68,7 @@ class UpdateUrl extends Command
foreach ($columns as $column) {
$oldJson = trim(json_encode($oldUrl), '"');
$newJson = trim(json_encode($newUrl), '"');
$changeCount = $this->replaceValueInTable($table, $column, $oldJson, $newJson);
$changeCount = $this->replaceValueInTable($db, $table, $column, $oldJson, $newJson);
$this->info("Updated {$changeCount} JSON encoded rows in {$table}->{$column}");
}
}
@@ -97,13 +85,18 @@ class UpdateUrl extends Command
* Perform a find+replace operations in the provided table and column.
* Returns the count of rows changed.
*/
protected function replaceValueInTable(string $table, string $column, string $oldUrl, string $newUrl): int
{
$oldQuoted = $this->db->getPdo()->quote($oldUrl);
$newQuoted = $this->db->getPdo()->quote($newUrl);
protected function replaceValueInTable(
Connection $db,
string $table,
string $column,
string $oldUrl,
string $newUrl
): int {
$oldQuoted = $db->getPdo()->quote($oldUrl);
$newQuoted = $db->getPdo()->quote($newUrl);
return $this->db->table($table)->update([
$column => $this->db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"),
return $db->table($table)->update([
$column => $db->raw("REPLACE({$column}, {$oldQuoted}, {$newQuoted})"),
]);
}
@@ -113,6 +106,10 @@ class UpdateUrl extends Command
*/
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
{
if ($this->option('force')) {
return true;
}
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
$dangerWarning .= 'Are you sure you want to proceed?';
$backupConfirmation = 'This operation could cause issues if used incorrectly. Have you made a backup of your existing database?';

View File

@@ -18,30 +18,11 @@ use BookStack\Entities\Models\PageRevision;
*/
class EntityProvider
{
/**
* @var Bookshelf
*/
public $bookshelf;
/**
* @var Book
*/
public $book;
/**
* @var Chapter
*/
public $chapter;
/**
* @var Page
*/
public $page;
/**
* @var PageRevision
*/
public $pageRevision;
public Bookshelf $bookshelf;
public Book $book;
public Chapter $chapter;
public Page $page;
public PageRevision $pageRevision;
public function __construct()
{
@@ -69,13 +50,18 @@ class EntityProvider
}
/**
* Get an entity instance by it's basic name.
* Get an entity instance by its basic name.
*/
public function get(string $type): Entity
{
$type = strtolower($type);
$instance = $this->all()[$type] ?? null;
return $this->all()[$type];
if (is_null($instance)) {
throw new \InvalidArgumentException("Provided type \"{$type}\" is not a valid entity type");
}
return $instance;
}
/**

View File

@@ -2,18 +2,18 @@
namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Block\Element\Paragraph;
use League\CommonMark\Block\Renderer\BlockRendererInterface;
use League\CommonMark\Block\Renderer\ListItemRenderer;
use League\CommonMark\ElementRendererInterface;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\CommonMark\Renderer\Block\ListItemRenderer;
use League\CommonMark\Extension\TaskList\TaskListItemMarker;
use League\CommonMark\HtmlElement;
use League\CommonMark\Node\Block\Paragraph;
use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
class CustomListItemRenderer implements BlockRendererInterface
class CustomListItemRenderer implements NodeRendererInterface
{
protected $baseRenderer;
protected ListItemRenderer $baseRenderer;
public function __construct()
{
@@ -23,11 +23,11 @@ class CustomListItemRenderer implements BlockRendererInterface
/**
* @return HtmlElement|string|null
*/
public function render(AbstractBlock $block, ElementRendererInterface $htmlRenderer, bool $inTightList = false)
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
$listItem = $this->baseRenderer->render($block, $htmlRenderer, $inTightList);
$listItem = $this->baseRenderer->render($node, $childRenderer);
if ($this->startsTaskListItem($block)) {
if ($node instanceof ListItem && $this->startsTaskListItem($node) && $listItem instanceof HtmlElement) {
$listItem->setAttribute('class', 'task-list-item');
}

View File

@@ -2,16 +2,16 @@
namespace BookStack\Entities\Tools\Markdown;
use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\Strikethrough\Strikethrough;
use League\CommonMark\Extension\Strikethrough\StrikethroughDelimiterProcessor;
class CustomStrikeThroughExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addDelimiterProcessor(new StrikethroughDelimiterProcessor());
$environment->addInlineRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
$environment->addRenderer(Strikethrough::class, new CustomStrikethroughRenderer());
}
}

View File

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

View File

@@ -4,8 +4,9 @@ namespace BookStack\Entities\Tools\Markdown;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Environment;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
@@ -21,15 +22,16 @@ class MarkdownToHtml
public function convert(): string
{
$environment = Environment::createCommonMarkEnvironment();
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new MarkdownConverter($environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);
$environment->addRenderer(ListItem::class, new CustomListItemRenderer(), 10);
return $converter->convertToHtml($this->markdown);
return $converter->convert($this->markdown)->getContent();
}
}

View File

@@ -19,20 +19,15 @@ use Illuminate\Support\Str;
class PageContent
{
protected Page $page;
/**
* PageContent constructor.
*/
public function __construct(Page $page)
{
$this->page = $page;
public function __construct(
protected Page $page
) {
}
/**
* Update the content of the page with new provided HTML.
*/
public function setNewHTML(string $html)
public function setNewHTML(string $html): void
{
$html = $this->extractBase64ImagesFromHtml($html);
$this->page->html = $this->formatHtml($html);
@@ -43,7 +38,7 @@ class PageContent
/**
* Update the content of the page with new provided Markdown content.
*/
public function setNewMarkdown(string $markdown)
public function setNewMarkdown(string $markdown): void
{
$markdown = $this->extractBase64ImagesFromMarkdown($markdown);
$this->page->markdown = $markdown;
@@ -57,7 +52,7 @@ class PageContent
*/
protected function extractBase64ImagesFromHtml(string $htmlText): string
{
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
if (empty($htmlText) || !str_contains($htmlText, 'data:image')) {
return $htmlText;
}
@@ -91,7 +86,7 @@ class PageContent
* Attempting to capture the whole data uri using regex can cause PHP
* PCRE limits to be hit with larger, multi-MB, files.
*/
protected function extractBase64ImagesFromMarkdown(string $markdown)
protected function extractBase64ImagesFromMarkdown(string $markdown): string
{
$matches = [];
$contentLength = strlen($markdown);
@@ -183,32 +178,13 @@ class PageContent
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
// Set ids on top-level nodes
// Map to hold used ID references
$idMap = [];
foreach ($childNodes as $index => $childNode) {
[$oldId, $newId] = $this->setUniqueId($childNode, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Map to hold changing ID references
$changeMap = [];
// Set ids on nested header nodes
$nestedHeaders = $xPath->query('//body//*//h1|//body//*//h2|//body//*//h3|//body//*//h4|//body//*//h5|//body//*//h6');
foreach ($nestedHeaders as $nestedHeader) {
[$oldId, $newId] = $this->setUniqueId($nestedHeader, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
// Ensure no duplicate ids within child items
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
[$oldId, $newId] = $this->setUniqueId($domElem, $idMap);
if ($newId && $newId !== $oldId) {
$this->updateLinks($xPath, '#' . $oldId, '#' . $newId);
}
}
$this->updateIdsRecursively($body, 0, $idMap, $changeMap);
$this->updateLinks($xPath, $changeMap);
// Generate inner html as a string
$html = '';
@@ -223,20 +199,53 @@ class PageContent
}
/**
* Update the all links to the $old location to instead point to $new.
* For the given DOMNode, traverse its children recursively and update IDs
* where required (Top-level, headers & elements with IDs).
* Will update the provided $changeMap array with changes made, where keys are the old
* ids and the corresponding values are the new ids.
*/
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
protected function updateIdsRecursively(DOMNode $element, int $depth, array &$idMap, array &$changeMap): void
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
/* @var DOMNode $child */
foreach ($element->childNodes as $child) {
if ($child instanceof DOMElement && ($depth === 0 || in_array($child->nodeName, ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']) || $child->getAttribute('id'))) {
[$oldId, $newId] = $this->setUniqueId($child, $idMap);
if ($newId && $newId !== $oldId && !isset($idMap[$oldId])) {
$changeMap[$oldId] = $newId;
}
}
if ($child->hasChildNodes()) {
$this->updateIdsRecursively($child, $depth + 1, $idMap, $changeMap);
}
}
}
/**
* Update the all links in the given xpath to apply requires changes within the
* given $changeMap array.
*/
protected function updateLinks(DOMXPath $xpath, array $changeMap): void
{
if (empty($changeMap)) {
return;
}
$links = $xpath->query('//body//*//*[@href]');
/** @var DOMElement $domElem */
foreach ($links as $domElem) {
$href = ltrim($domElem->getAttribute('href'), '#');
$newHref = $changeMap[$href] ?? null;
if ($newHref) {
$domElem->setAttribute('href', '#' . $newHref);
}
}
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* A map for existing ID's should be passed in to check for current existence,
* and this will be updated with any new IDs set upon elements.
* Returns a pair of strings in the format [old_id, new_id].
*/
protected function setUniqueId(DOMNode $element, array &$idMap): array
@@ -247,7 +256,7 @@ class PageContent
// Stop if there's an existing valid id that has not already been used.
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
if (str_starts_with($existingId, 'bkmrk') && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return [$existingId, $existingId];
@@ -258,7 +267,7 @@ class PageContent
// the same content is passed through.
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
$loopIndex = 1;
while (isset($idMap[$newId])) {
$newId = urlencode($contentId . '-' . $loopIndex);
@@ -295,7 +304,9 @@ class PageContent
if ($blankIncludes) {
$content = $this->blankPageIncludes($content);
} else {
$content = $this->parsePageIncludes($content);
for ($includeDepth = 0; $includeDepth < 3; $includeDepth++) {
$content = $this->parsePageIncludes($content);
}
}
return $content;
@@ -440,8 +451,8 @@ class PageContent
{
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$html = '<body>' . $html . '</body>';
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
$doc->loadHTML($html);
return $doc;
}

View File

@@ -4,20 +4,20 @@ namespace BookStack\Entities\Tools;
use BookStack\Actions\ActivityType;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PermissionsUpdater
{
/**
* Update an entities permissions from a permission form submit request.
*/
public function updateFromPermissionsForm(Entity $entity, Request $request)
public function updateFromPermissionsForm(Entity $entity, Request $request): void
{
$permissions = $request->get('permissions', null);
$ownerId = $request->get('owned_by', null);
@@ -39,12 +39,44 @@ class PermissionsUpdater
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}
/**
* Update permissions from API request data.
*/
public function updateFromApiRequestData(Entity $entity, array $data): void
{
if (isset($data['role_permissions'])) {
$entity->permissions()->where('role_id', '!=', 0)->delete();
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions($data['role_permissions'] ?? [], false);
$entity->permissions()->createMany($rolePermissionData);
}
if (array_key_exists('fallback_permissions', $data)) {
$entity->permissions()->where('role_id', '=', 0)->delete();
}
if (isset($data['fallback_permissions']['inheriting']) && $data['fallback_permissions']['inheriting'] !== true) {
$data = $data['fallback_permissions'];
$data['role_id'] = 0;
$rolePermissionData = $this->formatPermissionsFromApiRequestToEntityPermissions([$data], true);
$entity->permissions()->createMany($rolePermissionData);
}
if (isset($data['owner_id'])) {
$this->updateOwnerFromId($entity, intval($data['owner_id']));
}
$entity->save();
$entity->rebuildPermissions();
Activity::add(ActivityType::PERMISSIONS_UPDATE, $entity);
}
/**
* Update the owner of the given entity.
* Checks the user exists in the system first.
* Does not save the model, just updates it.
*/
protected function updateOwnerFromId(Entity $entity, int $newOwnerId)
protected function updateOwnerFromId(Entity $entity, int $newOwnerId): void
{
$newOwner = User::query()->find($newOwnerId);
if (!is_null($newOwner)) {
@@ -67,7 +99,41 @@ class PermissionsUpdater
$formatted[] = $entityPermissionData;
}
return $formatted;
return $this->filterEntityPermissionDataUponRole($formatted, true);
}
protected function formatPermissionsFromApiRequestToEntityPermissions(array $permissions, bool $allowFallback): array
{
$formatted = [];
foreach ($permissions as $requestPermissionData) {
$entityPermissionData = ['role_id' => $requestPermissionData['role_id']];
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = boolval($requestPermissionData[$permission] ?? false);
}
$formatted[] = $entityPermissionData;
}
return $this->filterEntityPermissionDataUponRole($formatted, $allowFallback);
}
protected function filterEntityPermissionDataUponRole(array $entityPermissionData, bool $allowFallback): array
{
$roleIds = [];
foreach ($entityPermissionData as $permissionEntry) {
$roleIds[] = intval($permissionEntry['role_id']);
}
$actualRoleIds = array_unique(array_values(array_filter($roleIds)));
$rolesById = Role::query()->whereIn('id', $actualRoleIds)->get('id')->keyBy('id');
return array_values(array_filter($entityPermissionData, function ($data) use ($rolesById, $allowFallback) {
if (intval($data['role_id']) === 0) {
return $allowFallback;
}
return $rolesById->has($data['role_id']);
}));
}
/**

View File

@@ -2,25 +2,18 @@
namespace BookStack\Exceptions;
use Whoops\Handler\Handler;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
class WhoopsBookStackPrettyHandler extends Handler
class BookStackExceptionHandlerPage implements ExceptionRenderer
{
/**
* @return int|null A handler may return nothing, or a Handler::HANDLE_* constant
*/
public function handle()
public function render($throwable)
{
$exception = $this->getException();
echo view('errors.debug', [
'error' => $exception->getMessage(),
'errorClass' => get_class($exception),
'trace' => $exception->getTraceAsString(),
return view('errors.debug', [
'error' => $throwable->getMessage(),
'errorClass' => get_class($throwable),
'trace' => $throwable->getTraceAsString(),
'environment' => $this->getEnvironment(),
])->render();
return Handler::QUIT;
}
protected function safeReturn(callable $callback, $default = null)

View File

@@ -17,7 +17,7 @@ class Handler extends ExceptionHandler
/**
* A list of the exception types that are not reported.
*
* @var array
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
NotFoundException::class,
@@ -25,9 +25,9 @@ class Handler extends ExceptionHandler
];
/**
* A list of the inputs that are never flashed for validation exceptions.
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
@@ -98,6 +98,7 @@ class Handler extends ExceptionHandler
];
if ($e instanceof ValidationException) {
$responseData['error']['message'] = 'The given data was invalid.';
$responseData['error']['validation'] = $e->errors();
$code = $e->status;
}

View File

@@ -32,10 +32,15 @@ abstract class ApiController extends Controller
*/
public function getValidationRules(): array
{
if (method_exists($this, 'rules')) {
return $this->rules();
}
return $this->rules();
}
/**
* Get the validation rules for the actions in this controller.
* Defaults to a $rules property but can be a rules() method.
*/
protected function rules(): array
{
return $this->rules;
}
}

View File

@@ -13,11 +13,9 @@ use Illuminate\Validation\ValidationException;
class AttachmentApiController extends ApiController
{
protected $attachmentService;
public function __construct(AttachmentService $attachmentService)
{
$this->attachmentService = $attachmentService;
public function __construct(
protected AttachmentService $attachmentService
) {
}
/**
@@ -174,13 +172,13 @@ class AttachmentApiController extends ApiController
'name' => ['required', 'min:1', 'max:255', 'string'],
'uploaded_to' => ['required', 'integer', 'exists:pages,id'],
'file' => array_merge(['required_without:link'], $this->attachmentService->getFileValidationRules()),
'link' => ['required_without:file', 'min:1', 'max:255', 'safe_url'],
'link' => ['required_without:file', 'min:1', 'max:2000', 'safe_url'],
],
'update' => [
'name' => ['min:1', 'max:255', 'string'],
'uploaded_to' => ['integer', 'exists:pages,id'],
'file' => $this->attachmentService->getFileValidationRules(),
'link' => ['min:1', 'max:255', 'safe_url'],
'link' => ['min:1', 'max:2000', 'safe_url'],
],
];
}

View File

@@ -0,0 +1,100 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\EntityProvider;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Tools\PermissionsUpdater;
use Illuminate\Http\Request;
class ContentPermissionApiController extends ApiController
{
public function __construct(
protected PermissionsUpdater $permissionsUpdater,
protected EntityProvider $entities
) {
}
protected $rules = [
'update' => [
'owner_id' => ['int'],
'role_permissions' => ['array'],
'role_permissions.*.role_id' => ['required', 'int', 'exists:roles,id'],
'role_permissions.*.view' => ['required', 'boolean'],
'role_permissions.*.create' => ['required', 'boolean'],
'role_permissions.*.update' => ['required', 'boolean'],
'role_permissions.*.delete' => ['required', 'boolean'],
'fallback_permissions' => ['nullable'],
'fallback_permissions.inheriting' => ['required_with:fallback_permissions', 'boolean'],
'fallback_permissions.view' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.create' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.update' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
'fallback_permissions.delete' => ['required_if:fallback_permissions.inheriting,false', 'boolean'],
]
];
/**
* Read the configured content-level permissions for the item of the given type and ID.
* 'contentType' should be one of: page, book, chapter, bookshelf.
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
* The permissions shown are those that override the default for just the specified item, they do not show the
* full evaluated permission for a role, nor do they reflect permissions inherited from other items in the hierarchy.
* Fallback permission values may be `null` when inheriting is active.
*/
public function read(string $contentType, string $contentId)
{
$entity = $this->entities->get($contentType)
->newQuery()->scopes(['visible'])->findOrFail($contentId);
$this->checkOwnablePermission('restrictions-manage', $entity);
return response()->json($this->formattedPermissionDataForEntity($entity));
}
/**
* Update the configured content-level permission overrides for the item of the given type and ID.
* 'contentType' should be one of: page, book, chapter, bookshelf.
* 'contentId' should be the relevant ID of that item type you'd like to handle permissions for.
* Providing an empty `role_permissions` array will remove any existing configured role permissions,
* so you may want to fetch existing permissions beforehand if just adding/removing a single item.
* You should completely omit the `owner_id`, `role_permissions` and/or the `fallback_permissions` properties
* from your request data if you don't wish to update details within those categories.
*/
public function update(Request $request, string $contentType, string $contentId)
{
$entity = $this->entities->get($contentType)
->newQuery()->scopes(['visible'])->findOrFail($contentId);
$this->checkOwnablePermission('restrictions-manage', $entity);
$data = $this->validate($request, $this->rules()['update']);
$this->permissionsUpdater->updateFromApiRequestData($entity, $data);
return response()->json($this->formattedPermissionDataForEntity($entity));
}
protected function formattedPermissionDataForEntity(Entity $entity): array
{
$rolePermissions = $entity->permissions()
->where('role_id', '!=', 0)
->with(['role:id,display_name'])
->get();
$fallback = $entity->permissions()->where('role_id', '=', 0)->first();
$fallbackData = [
'inheriting' => is_null($fallback),
'view' => $fallback->view ?? null,
'create' => $fallback->create ?? null,
'update' => $fallback->update ?? null,
'delete' => $fallback->delete ?? null,
];
return [
'owner' => $entity->ownedBy()->first(),
'role_permissions' => $rolePermissions,
'fallback_permissions' => $fallbackData,
];
}
}

View File

@@ -0,0 +1,146 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Models\Page;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
class ImageGalleryApiController extends ApiController
{
protected array $fieldsToExpose = [
'id', 'name', 'url', 'path', 'type', 'uploaded_to', 'created_by', 'updated_by', 'created_at', 'updated_at',
];
public function __construct(
protected ImageRepo $imageRepo
) {
}
protected function rules(): array
{
return [
'create' => [
'type' => ['required', 'string', 'in:gallery,drawio'],
'uploaded_to' => ['required', 'integer'],
'image' => ['required', 'file', ...$this->getImageValidationRules()],
'name' => ['string', 'max:180'],
],
'update' => [
'name' => ['string', 'max:180'],
]
];
}
/**
* Get a listing of images in the system. Includes gallery (page content) images and drawings.
* Requires visibility of the page they're originally uploaded to.
*/
public function list()
{
$images = Image::query()->scopes(['visible'])
->select($this->fieldsToExpose)
->whereIn('type', ['gallery', 'drawio']);
return $this->apiListingResponse($images, [
...$this->fieldsToExpose
]);
}
/**
* Create a new image in the system.
* Since "image" is expected to be a file, this needs to be a 'multipart/form-data' type request.
* The provided "uploaded_to" should be an existing page ID in the system.
* If the "name" parameter is omitted, the filename of the provided image file will be used instead.
* The "type" parameter should be 'gallery' for page content images, and 'drawio' should only be used
* when the file is a PNG file with diagrams.net image data embedded within.
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$data = $this->validate($request, $this->rules()['create']);
Page::visible()->findOrFail($data['uploaded_to']);
$image = $this->imageRepo->saveNew($data['image'], $data['type'], $data['uploaded_to']);
if (isset($data['name'])) {
$image->refresh();
$image->update(['name' => $data['name']]);
}
return response()->json($this->formatForSingleResponse($image));
}
/**
* View the details of a single image.
* The "thumbs" response property contains links to scaled variants that BookStack may use in its UI.
* The "content" response property provides HTML and Markdown content, in the format that BookStack
* would typically use by default to add the image in page content, as a convenience.
* Actual image file data is not provided but can be fetched via the "url" response property.
*/
public function read(string $id)
{
$image = Image::query()->scopes(['visible'])->findOrFail($id);
return response()->json($this->formatForSingleResponse($image));
}
/**
* Update the details of an existing image in the system.
* Only allows updating of the image name at this time.
*/
public function update(Request $request, string $id)
{
$data = $this->validate($request, $this->rules()['update']);
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('page-view', $image->getPage());
$this->checkOwnablePermission('image-update', $image);
$this->imageRepo->updateImageDetails($image, $data);
return response()->json($this->formatForSingleResponse($image));
}
/**
* Delete an image from the system.
* Will also delete thumbnails for the image.
* Does not check or handle image usage so this could leave pages with broken image references.
*/
public function delete(string $id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('page-view', $image->getPage());
$this->checkOwnablePermission('image-delete', $image);
$this->imageRepo->destroyImage($image);
return response('', 204);
}
/**
* Format the given image model for single-result display.
*/
protected function formatForSingleResponse(Image $image): array
{
$this->imageRepo->loadThumbs($image);
$data = $image->getAttributes();
$data['created_by'] = $image->createdBy;
$data['updated_by'] = $image->updatedBy;
$data['content'] = [];
$escapedUrl = htmlentities($image->url);
$escapedName = htmlentities($image->name);
if ($image->type === 'drawio') {
$data['content']['html'] = "<div drawio-diagram=\"{$image->id}\"><img src=\"{$escapedUrl}\"></div>";
$data['content']['markdown'] = $data['content']['html'];
} else {
$escapedDisplayThumb = htmlentities($image->thumbs['display']);
$data['content']['html'] = "<a href=\"{$escapedUrl}\" target=\"_blank\"><img src=\"{$escapedDisplayThumb}\" alt=\"{$escapedName}\"></a>";
$mdEscapedName = str_replace(']', '', str_replace('[', '', $image->name));
$mdEscapedThumb = str_replace(']', '', str_replace('[', '', $image->thumbs['display']));
$data['content']['markdown'] = "![{$mdEscapedName}]({$mdEscapedThumb})";
}
return $data;
}
}

View File

@@ -0,0 +1,136 @@
<?php
namespace BookStack\Http\Controllers\Api;
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Auth\Role;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class RoleApiController extends ApiController
{
protected PermissionsRepo $permissionsRepo;
protected array $fieldsToExpose = [
'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
];
protected $rules = [
'create' => [
'display_name' => ['required', 'string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
],
'update' => [
'display_name' => ['string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],
'mfa_enforced' => ['boolean'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'permissions.*' => ['string'],
]
];
public function __construct(PermissionsRepo $permissionsRepo)
{
$this->permissionsRepo = $permissionsRepo;
// Checks for all endpoints in this controller
$this->middleware(function ($request, $next) {
$this->checkPermission('user-roles-manage');
return $next($request);
});
}
/**
* Get a listing of roles in the system.
* Requires permission to manage roles.
*/
public function list()
{
$roles = Role::query()->select(['*'])
->withCount(['users', 'permissions']);
return $this->apiListingResponse($roles, [
...$this->fieldsToExpose,
'permissions_count',
'users_count',
]);
}
/**
* Create a new role in the system.
* Permissions should be provided as an array of permission name strings.
* Requires permission to manage roles.
*/
public function create(Request $request)
{
$data = $this->validate($request, $this->rules()['create']);
$role = null;
DB::transaction(function () use ($data, &$role) {
$role = $this->permissionsRepo->saveNewRole($data);
});
$this->singleFormatter($role);
return response()->json($role);
}
/**
* View the details of a single role.
* Provides the permissions and a high-level list of the users assigned.
* Requires permission to manage roles.
*/
public function read(string $id)
{
$role = $this->permissionsRepo->getRoleById($id);
$this->singleFormatter($role);
return response()->json($role);
}
/**
* Update an existing role in the system.
* Permissions should be provided as an array of permission name strings.
* An empty "permissions" array would clear granted permissions.
* In many cases, where permissions are changed, you'll want to fetch the existing
* permissions and then modify before providing in your update request.
* Requires permission to manage roles.
*/
public function update(Request $request, string $id)
{
$data = $this->validate($request, $this->rules()['update']);
$role = $this->permissionsRepo->updateRole($id, $data);
$this->singleFormatter($role);
return response()->json($role);
}
/**
* Delete a role from the system.
* Requires permission to manage roles.
*/
public function delete(string $id)
{
$this->permissionsRepo->deleteRole(intval($id));
return response('', 204);
}
/**
* Format the given role model for single-result display.
*/
protected function singleFormatter(Role $role)
{
$role->load('users:id,name,slug');
$role->unsetRelation('permissions');
$role->setAttribute('permissions', $role->permissions()->orderBy('name', 'asc')->pluck('name'));
$role->makeVisible(['users', 'permissions']);
}
}

View File

@@ -13,9 +13,9 @@ use Illuminate\Validation\Rules\Unique;
class UserApiController extends ApiController
{
protected $userRepo;
protected UserRepo $userRepo;
protected $fieldsToExpose = [
protected array $fieldsToExpose = [
'email', 'created_at', 'updated_at', 'last_activity_at', 'external_auth_id',
];

View File

@@ -15,16 +15,10 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected AttachmentService $attachmentService;
protected PageRepo $pageRepo;
/**
* AttachmentController constructor.
*/
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
{
$this->attachmentService = $attachmentService;
$this->pageRepo = $pageRepo;
public function __construct(
protected AttachmentService $attachmentService,
protected PageRepo $pageRepo
) {
}
/**
@@ -112,7 +106,7 @@ class AttachmentController extends Controller
try {
$this->validate($request, [
'attachment_edit_name' => ['required', 'string', 'min:1', 'max:255'],
'attachment_edit_url' => ['string', 'min:1', 'max:255', 'safe_url'],
'attachment_edit_url' => ['string', 'min:1', 'max:2000', 'safe_url'],
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
@@ -148,7 +142,7 @@ class AttachmentController extends Controller
$this->validate($request, [
'attachment_link_uploaded_to' => ['required', 'integer', 'exists:pages,id'],
'attachment_link_name' => ['required', 'string', 'min:1', 'max:255'],
'attachment_link_url' => ['required', 'string', 'min:1', 'max:255', 'safe_url'],
'attachment_link_url' => ['required', 'string', 'min:1', 'max:2000', 'safe_url'],
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [

View File

@@ -14,21 +14,11 @@ use Illuminate\Http\Request;
class ConfirmEmailController extends Controller
{
protected EmailConfirmationService $emailConfirmationService;
protected LoginService $loginService;
protected UserRepo $userRepo;
/**
* Create a new controller instance.
*/
public function __construct(
EmailConfirmationService $emailConfirmationService,
LoginService $loginService,
UserRepo $userRepo
protected EmailConfirmationService $emailConfirmationService,
protected LoginService $loginService,
protected UserRepo $userRepo
) {
$this->emailConfirmationService = $emailConfirmationService;
$this->loginService = $loginService;
$this->userRepo = $userRepo;
}
/**

View File

@@ -64,7 +64,7 @@ class BookshelfController extends Controller
public function create()
{
$this->checkPermission('bookshelf-create-all');
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug']);
$books = Book::visible()->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves.create', ['books' => $books]);
@@ -140,7 +140,7 @@ class BookshelfController extends Controller
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBookIds = $shelf->books()->get(['id'])->pluck('id');
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug']);
$books = Book::visible()->whereNotIn('id', $shelfBookIds)->orderBy('name')->get(['name', 'id', 'slug', 'created_at', 'updated_at']);
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Tools\PageContent;
use BookStack\Uploads\FaviconHandler;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
@@ -127,4 +128,15 @@ class HomeController extends Controller
{
return response()->view('errors.404', [], 404);
}
/**
* Serve the application favicon.
* Ensures a 'favicon.ico' file exists at the web root location (if writable) to be served
* directly by the webserver in the future.
*/
public function favicon(FaviconHandler $favicons)
{
$exists = $favicons->restoreOriginalIfNotExists();
return response()->file($exists ? $favicons->getPath() : $favicons->getOriginalPath());
}
}

View File

@@ -10,14 +10,9 @@ use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller
{
protected $imageRepo;
/**
* GalleryImageController constructor.
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
public function __construct(
protected ImageRepo $imageRepo
) {
}
/**
@@ -47,9 +42,14 @@ class GalleryImageController extends Controller
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => $this->getImageValidationRules(),
]);
try {
$this->validate($request, [
'file' => $this->getImageValidationRules(),
]);
} catch (ValidationException $exception) {
return $this->jsonError(implode("\n", $exception->errors()['file']));
}
try {
$imageUpload = $request->file('file');

View File

@@ -74,13 +74,17 @@ class RoleController extends Controller
public function store(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);
$this->permissionsRepo->saveNewRole($request->all());
$this->showSuccessNotification(trans('settings.role_create_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->saveNewRole($data);
return redirect('/settings/roles');
}
@@ -100,19 +104,21 @@ class RoleController extends Controller
/**
* Updates a user role.
*
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
$data = $this->validate($request, [
'display_name' => ['required', 'min:3', 'max:180'],
'description' => ['max:180'],
'external_auth_id' => ['string'],
'permissions' => ['array'],
'mfa_enforced' => ['string'],
]);
$this->permissionsRepo->updateRole($id, $request->all());
$this->showSuccessNotification(trans('settings.role_update_success'));
$data['permissions'] = array_keys($data['permissions'] ?? []);
$data['mfa_enforced'] = ($data['mfa_enforced'] ?? 'false') === 'true';
$this->permissionsRepo->updateRole($id, $data);
return redirect('/settings/roles');
}
@@ -145,15 +151,14 @@ class RoleController extends Controller
$this->checkPermission('user-roles-manage');
try {
$this->permissionsRepo->deleteRole($id, $request->get('migrate_role_id'));
$migrateRoleId = intval($request->get('migrate_role_id') ?: "0");
$this->permissionsRepo->deleteRole($id, $migrateRoleId);
} catch (PermissionsException $e) {
$this->showErrorNotification($e->getMessage());
return redirect()->back();
}
$this->showSuccessNotification(trans('settings.role_delete_success'));
return redirect('/settings/roles');
}
}

View File

@@ -8,11 +8,9 @@ use Illuminate\Http\Request;
class TagController extends Controller
{
protected TagRepo $tagRepo;
public function __construct(TagRepo $tagRepo)
{
$this->tagRepo = $tagRepo;
public function __construct(
protected TagRepo $tagRepo
) {
}
/**

View File

@@ -197,7 +197,7 @@ class UserController extends Controller
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
$newOwnerId = $request->get('new_owner_id', null);
$newOwnerId = intval($request->get('new_owner_id')) ?: null;
$this->userRepo->destroy($user, $newOwnerId);

View File

@@ -9,10 +9,8 @@ class Request extends LaravelRequest
/**
* Override the default request methods to get the scheme and host
* to directly use the custom APP_URL, if set.
*
* @return string
*/
public function getSchemeAndHttpHost()
public function getSchemeAndHttpHost(): string
{
$appUrl = config('app.url', null);
@@ -27,10 +25,8 @@ class Request extends LaravelRequest
* Override the default request methods to get the base URL
* to directly use the custom APP_URL, if set.
* The base URL never ends with a / but should start with one if not empty.
*
* @return string
*/
public function getBaseUrl()
public function getBaseUrl(): string
{
$appUrl = config('app.url', null);

View File

@@ -8,16 +8,16 @@ use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\WhoopsBookStackPrettyHandler;
use BookStack\Exceptions\BookStackExceptionHandlerPage;
use BookStack\Settings\SettingService;
use BookStack\Util\CspService;
use GuzzleHttp\Client;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Psr\Http\Client\ClientInterface as HttpClientInterface;
use Whoops\Handler\HandlerInterface;
class AppServiceProvider extends ServiceProvider
{
@@ -26,7 +26,7 @@ class AppServiceProvider extends ServiceProvider
* @var string[]
*/
public $bindings = [
HandlerInterface::class => WhoopsBookStackPrettyHandler::class,
ExceptionRenderer::class => BookStackExceptionHandlerPage::class,
];
/**

View File

@@ -24,11 +24,22 @@ class EventServiceProvider extends ServiceProvider
];
/**
* Register any other events for your application.
* Register any events for your application.
*
* @return void
*/
public function boot()
{
//
}
/**
* Determine if events and listeners should be automatically discovered.
*
* @return bool
*/
public function shouldDiscoverEvents()
{
return false;
}
}

View File

@@ -77,7 +77,7 @@ class RouteServiceProvider extends ServiceProvider
protected function configureRateLimiting()
{
RateLimiter::for('api', function (Request $request) {
return Limit::perMinute(60)->by(optional($request->user())->id ?: $request->ip());
return Limit::perMinute(60)->by($request->user()?->id ?: $request->ip());
});
}
}

View File

@@ -21,8 +21,8 @@ class ValidationRuleServiceProvider extends ServiceProvider
Validator::extend('safe_url', function ($attribute, $value, $parameters, $validator) {
$cleanLinkName = strtolower(trim($value));
$isJs = strpos($cleanLinkName, 'javascript:') === 0;
$isData = strpos($cleanLinkName, 'data:') === 0;
$isJs = str_starts_with($cleanLinkName, 'javascript:');
$isData = str_starts_with($cleanLinkName, 'data:');
return !$isJs && !$isData;
});

View File

@@ -54,10 +54,10 @@ class CrossLinkParser
{
$links = [];
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML($html);
$xPath = new DOMXPath($doc);
$anchors = $xPath->query('//a[@href]');

View File

@@ -15,25 +15,18 @@ class SearchIndex
{
/**
* A list of delimiter characters used to break-up parsed content into terms for indexing.
*
* @var string
*/
public static $delimiters = " \n\t.,!?:;()[]{}<>`'\"";
public static string $delimiters = " \n\t.,!?:;()[]{}<>`'\"";
/**
* @var EntityProvider
*/
protected $entityProvider;
public function __construct(EntityProvider $entityProvider)
{
$this->entityProvider = $entityProvider;
public function __construct(
protected EntityProvider $entityProvider
) {
}
/**
* Index the given entity.
*/
public function indexEntity(Entity $entity)
public function indexEntity(Entity $entity): void
{
$this->deleteEntityTerms($entity);
$terms = $this->entityToTermDataArray($entity);
@@ -45,7 +38,7 @@ class SearchIndex
*
* @param Entity[] $entities
*/
public function indexEntities(array $entities)
public function indexEntities(array $entities): void
{
$terms = [];
foreach ($entities as $entity) {
@@ -69,7 +62,7 @@ class SearchIndex
*
* @param callable(Entity, int, int):void|null $progressCallback
*/
public function indexAllEntities(?callable $progressCallback = null)
public function indexAllEntities(?callable $progressCallback = null): void
{
SearchTerm::query()->truncate();
@@ -101,7 +94,7 @@ class SearchIndex
/**
* Delete related Entity search terms.
*/
public function deleteEntityTerms(Entity $entity)
public function deleteEntityTerms(Entity $entity): void
{
$entity->searchTerms()->delete();
}
@@ -145,12 +138,12 @@ class SearchIndex
'h6' => 1.5,
];
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
$html = str_ireplace(['<br>', '<br />', '<br/>'], "\n", $html);
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML($html);
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
/** @var DOMNode $child */

View File

@@ -173,6 +173,7 @@ class SearchRunner
// Handle exact term matching
foreach ($searchOpts->exacts as $inputTerm) {
$entityQuery->where(function (EloquentBuilder $query) use ($inputTerm, $entityModelInstance) {
$inputTerm = str_replace('\\', '\\\\', $inputTerm);
$query->where('name', 'like', '%' . $inputTerm . '%')
->orWhere($entityModelInstance->textField, 'like', '%' . $inputTerm . '%');
});
@@ -218,6 +219,7 @@ class SearchRunner
$subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms as $inputTerm) {
$inputTerm = str_replace('\\', '\\\\', $inputTerm);
$query->orWhere('term', 'like', $inputTerm . '%');
}
});
@@ -354,6 +356,9 @@ class SearchRunner
$tagValue = (float) trim($connection->getPdo()->quote($tagValue), "'");
$query->whereRaw("value {$tagOperator} {$tagValue}");
} else {
if ($tagOperator === 'like') {
$tagValue = str_replace('\\', '\\\\', $tagValue);
}
$query->where('value', $tagOperator, $tagValue);
}
} else {

View File

@@ -2,16 +2,16 @@
namespace BookStack\Settings;
use BookStack\Uploads\FaviconHandler;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
class AppSettingsStore
{
protected ImageRepo $imageRepo;
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
public function __construct(
protected ImageRepo $imageRepo,
protected FaviconHandler $faviconHandler,
) {
}
public function storeFromUpdateRequest(Request $request, string $category)
@@ -39,6 +39,8 @@ class AppSettingsStore
$icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size);
setting()->put('app-icon-' . $size, $icon->url);
}
$this->faviconHandler->saveForUploadedImage($iconFile);
}
// Clear icon image if requested
@@ -49,6 +51,8 @@ class AppSettingsStore
$this->destroyExistingSettingImage('app-icon-' . $size);
setting()->remove('app-icon-' . $size);
}
$this->faviconHandler->restoreOriginal();
}
}

View File

@@ -3,45 +3,29 @@
namespace BookStack\Settings;
use BookStack\Auth\User;
use Illuminate\Contracts\Cache\Repository as Cache;
/**
* Class SettingService
* The settings are a simple key-value database store.
* For non-authenticated users, user settings are stored via the session instead.
* A local array-based cache is used to for setting accesses across a request.
*/
class SettingService
{
protected Setting $setting;
protected Cache $cache;
protected array $localCache = [];
protected string $cachePrefix = 'setting-';
public function __construct(Setting $setting, Cache $cache)
{
$this->setting = $setting;
$this->cache = $cache;
}
/**
* Gets a setting from the database,
* If not found, Returns default, Which is false by default.
*/
public function get(string $key, $default = null)
public function get(string $key, $default = null): mixed
{
if (is_null($default)) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
$value = $this->getValueFromStore($key) ?? $default;
$formatted = $this->formatValue($value, $default);
$this->localCache[$key] = $formatted;
return $formatted;
return $this->formatValue($value, $default);
}
/**
@@ -79,52 +63,78 @@ class SettingService
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
* Returns null if nothing is found.
* Gets a setting value from the local cache.
* Will load the local cache if not previously loaded.
*/
protected function getValueFromStore(string $key)
protected function getValueFromStore(string $key): mixed
{
// Check the cache
$cacheKey = $this->cachePrefix . $key;
$cacheVal = $this->cache->get($cacheKey, null);
if ($cacheVal !== null) {
return $cacheVal;
$cacheCategory = $this->localCacheCategory($key);
if (!isset($this->localCache[$cacheCategory])) {
$this->loadToLocalCache($cacheCategory);
}
// Check the database
$settingObject = $this->getSettingObjectByKey($key);
if ($settingObject !== null) {
$value = $settingObject->value;
if ($settingObject->type === 'array') {
$value = json_decode($value, true) ?? [];
}
$this->cache->forever($cacheKey, $value);
return $value;
}
return null;
return $this->localCache[$cacheCategory][$key] ?? null;
}
/**
* Clear an item from the cache completely.
* Put the given value into the local cached under the given key.
*/
protected function clearFromCache(string $key)
protected function putValueIntoLocalCache(string $key, mixed $value): void
{
$cacheKey = $this->cachePrefix . $key;
$this->cache->forget($cacheKey);
if (isset($this->localCache[$key])) {
unset($this->localCache[$key]);
$cacheCategory = $this->localCacheCategory($key);
if (!isset($this->localCache[$cacheCategory])) {
$this->loadToLocalCache($cacheCategory);
}
$this->localCache[$cacheCategory][$key] = $value;
}
/**
* Get the category for the given setting key.
* Will return 'app' for a general app setting otherwise 'user:<user_id>' for a user setting.
*/
protected function localCacheCategory(string $key): string
{
if (str_starts_with($key, 'user:')) {
return implode(':', array_slice(explode(':', $key), 0, 2));
}
return 'app';
}
/**
* For the given category, load the relevant settings from the database into the local cache.
*/
protected function loadToLocalCache(string $cacheCategory): void
{
$query = Setting::query();
if ($cacheCategory === 'app') {
$query->where('setting_key', 'not like', 'user:%');
} else {
$query->where('setting_key', 'like', $cacheCategory . ':%');
}
$settings = $query->toBase()->get();
if (!isset($this->localCache[$cacheCategory])) {
$this->localCache[$cacheCategory] = [];
}
foreach ($settings as $setting) {
$value = $setting->value;
if ($setting->type === 'array') {
$value = json_decode($value, true) ?? [];
}
$this->localCache[$cacheCategory][$setting->setting_key] = $value;
}
}
/**
* Format a settings value.
*/
protected function formatValue($value, $default)
protected function formatValue(mixed $value, mixed $default): mixed
{
// Change string booleans to actual booleans
if ($value === 'true') {
@@ -155,21 +165,22 @@ class SettingService
* Add a setting to the database.
* Values can be an array or a string.
*/
public function put(string $key, $value): bool
public function put(string $key, mixed $value): bool
{
$setting = $this->setting->newQuery()->firstOrNew([
$setting = Setting::query()->firstOrNew([
'setting_key' => $key,
]);
$setting->type = 'string';
$setting->value = $value;
if (is_array($value)) {
$setting->type = 'array';
$value = $this->formatArrayValue($value);
$setting->value = $this->formatArrayValue($value);
}
$setting->value = $value;
$setting->save();
$this->clearFromCache($key);
$this->putValueIntoLocalCache($key, $value);
return true;
}
@@ -209,7 +220,7 @@ class SettingService
* Can only take string value types since this may use
* the session which is less flexible to data types.
*/
public function putForCurrentUser(string $key, string $value)
public function putForCurrentUser(string $key, string $value): bool
{
return $this->putUser(user(), $key, $value);
}
@@ -231,15 +242,19 @@ class SettingService
if ($setting) {
$setting->delete();
}
$this->clearFromCache($key);
$cacheCategory = $this->localCacheCategory($key);
if (isset($this->localCache[$cacheCategory])) {
unset($this->localCache[$cacheCategory][$key]);
}
}
/**
* Delete settings for a given user id.
*/
public function deleteUserSettings(string $userId)
public function deleteUserSettings(string $userId): void
{
return $this->setting->newQuery()
Setting::query()
->where('setting_key', 'like', $this->userKey($userId) . '%')
->delete();
}
@@ -249,7 +264,16 @@ class SettingService
*/
protected function getSettingObjectByKey(string $key): ?Setting
{
return $this->setting->newQuery()
->where('setting_key', '=', $key)->first();
return Setting::query()
->where('setting_key', '=', $key)
->first();
}
/**
* Empty the local setting value cache used by this service.
*/
public function flushCache(): void
{
$this->localCache = [];
}
}

View File

@@ -65,11 +65,24 @@ class ThemeEvents
* Provides the commonmark library environment for customization before it's used to render markdown content.
* If the listener returns a non-null value, that will be used as an environment instead.
*
* @param \League\CommonMark\ConfigurableEnvironmentInterface $environment
* @returns \League\CommonMark\ConfigurableEnvironmentInterface|null
* @param \League\CommonMark\Environment\Environment $environment
* @returns \League\CommonMark\Environment\Environment|null
*/
const COMMONMARK_ENVIRONMENT_CONFIGURE = 'commonmark_environment_configure';
/**
* OIDC ID token pre-validate event.
* Runs just before BookStack validates the user ID token data upon login.
* Provides the existing found set of claims for the user as a key-value array,
* along with an array of the proceeding access token data provided by the identity platform.
* If the listener returns a non-null value, that will replace the existing ID token claim data.
*
* @param array $idTokenData
* @param array $accessTokenData
* @returns array|null
*/
const OIDC_ID_TOKEN_PRE_VALIDATE = 'oidc_id_token_pre_validate';
/**
* Page include parse event.
* Runs when a page include tag is being parsed, typically when page content is being processed for viewing.

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -29,6 +30,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
class Attachment extends Model
{
use HasCreatorAndUpdater;
use HasFactory;
protected $fillable = ['name', 'order'];
protected $hidden = ['path', 'page'];
@@ -38,12 +40,10 @@ class Attachment extends Model
/**
* Get the downloadable file name for this upload.
*
* @return mixed|string
*/
public function getFileName()
public function getFileName(): string
{
if (strpos($this->name, '.') !== false) {
if (str_contains($this->name, '.')) {
return $this->name;
}
@@ -69,7 +69,7 @@ class Attachment extends Model
*/
public function getUrl($openInline = false): string
{
if ($this->external && strpos($this->path, 'http') !== 0) {
if ($this->external && !str_starts_with($this->path, 'http')) {
return $this->path;
}

View File

@@ -9,7 +9,7 @@ use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
use League\Flysystem\Util;
use League\Flysystem\WhitespacePathNormalizer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
@@ -54,7 +54,7 @@ class AttachmentService
*/
protected function adjustPathForStorageDisk(string $path): string
{
$path = Util::normalizePath(str_replace('uploads/files/', '', $path));
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/files/', '', $path));
if ($this->getStorageDiskName() === 'local_secure_attachments') {
return $path;

View File

@@ -0,0 +1,110 @@
<?php
namespace BookStack\Uploads;
use Illuminate\Http\UploadedFile;
use Intervention\Image\ImageManager;
class FaviconHandler
{
protected string $path;
public function __construct(
protected ImageManager $imageTool
) {
$this->path = public_path('favicon.ico');
}
/**
* Save the given UploadedFile instance as the application favicon.
*/
public function saveForUploadedImage(UploadedFile $file): void
{
if (!is_writeable($this->path)) {
return;
}
$imageData = file_get_contents($file->getRealPath());
$image = $this->imageTool->make($imageData);
$image->resize(32, 32);
$bmpData = $image->encode('png');
$icoData = $this->pngToIco($bmpData, 32, 32);
file_put_contents($this->path, $icoData);
}
/**
* Restore the original favicon image.
* Returned boolean indicates if the copy occurred.
*/
public function restoreOriginal(): bool
{
$permissionItem = file_exists($this->path) ? $this->path : dirname($this->path);
if (!is_writeable($permissionItem)) {
return false;
}
return copy($this->getOriginalPath(), $this->path);
}
/**
* Restore the original favicon image if no favicon image is already in use.
* Returns a boolean to indicate if the file exists.
*/
public function restoreOriginalIfNotExists(): bool
{
if (file_exists($this->path)) {
return true;
}
return $this->restoreOriginal();
}
/**
* Get the path to the favicon file.
*/
public function getPath(): string
{
return $this->path;
}
/**
* Get the path of the original favicon copy.
*/
public function getOriginalPath(): string
{
return public_path('icon.ico');
}
/**
* Convert PNG image data to ICO file format.
* Built following the file format info from Wikipedia:
* https://en.wikipedia.org/wiki/ICO_(file_format)
*/
protected function pngToIco(string $bmpData, int $width, int $height): string
{
// ICO header
$header = pack('v', 0x00); // Reserved. Must always be 0
$header .= pack('v', 0x01); // Specifies ico image
$header .= pack('v', 0x01); // Specifies number of images
// ICO Image Directory
$entry = hex2bin(dechex($width)); // Image width
$entry .= hex2bin(dechex($height)); // Image height
$entry .= "\0"; // Color palette, typically 0
$entry .= "\0"; // Reserved
// Color planes, Appears to remain 1 for bmp image data
$entry .= pack('v', 0x01);
// Bits per pixel, can range from 1 to 32. From testing conversion
// via intervention from png typically provides this as 24.
$entry .= pack('v', 0x00);
// Size of the image data in bytes
$entry .= pack('V', strlen($bmpData));
// Offset of the bmp data from file start
$entry .= pack('V', strlen($header) + strlen($entry) + 4);
// Join & return the combined parts of the ICO image data
return $header . $entry . $bmpData;
}
}

View File

@@ -3,9 +3,11 @@
namespace BookStack\Uploads;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
@@ -33,12 +35,21 @@ class Image extends Model
->where('joint_permissions.entity_type', '=', 'page');
}
/**
* Scope the query to just the images visible to the user based upon the
* user visibility of the uploaded_to page.
*/
public function scopeVisible(Builder $query): Builder
{
return app()->make(PermissionApplicator::class)->restrictPageRelationQuery($query, 'images', 'uploaded_to');
}
/**
* Get a thumbnail for this image.
*
* @throws \Exception
*/
public function getThumb(int $width, int $height, bool $keepRatio = false): string
public function getThumb(?int $width, ?int $height, bool $keepRatio = false): string
{
return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
}

View File

@@ -20,7 +20,7 @@ use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\Image as InterventionImage;
use Intervention\Image\ImageManager;
use League\Flysystem\Util;
use League\Flysystem\WhitespacePathNormalizer;
use Psr\SimpleCache\InvalidArgumentException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\StreamedResponse;
@@ -29,10 +29,9 @@ class ImageService
{
protected ImageManager $imageTool;
protected Cache $cache;
protected $storageUrl;
protected FilesystemManager $fileSystem;
protected static $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
public function __construct(ImageManager $imageTool, FilesystemManager $fileSystem, Cache $cache)
{
@@ -73,7 +72,7 @@ class ImageService
*/
protected function adjustPathForStorageDisk(string $path, string $imageType = ''): string
{
$path = Util::normalizePath(str_replace('uploads/images/', '', $path));
$path = (new WhitespacePathNormalizer())->normalizePath(str_replace('uploads/images/', '', $path));
if ($this->usingSecureImages($imageType)) {
return $path;
@@ -548,7 +547,7 @@ class ImageService
// Check the image file exists
&& $disk->exists($imagePath)
// Check the file is likely an image file
&& strpos($disk->getMimetype($imagePath), 'image/') === 0;
&& strpos($disk->mimeType($imagePath), 'image/') === 0;
}
/**
@@ -661,25 +660,21 @@ class ImageService
*/
private function getPublicUrl(string $filePath): string
{
if (is_null($this->storageUrl)) {
$storageUrl = config('filesystems.url');
$storageUrl = config('filesystems.url');
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if ($storageUrl == false && config('filesystems.images') === 's3') {
$storageDetails = config('filesystems.disks.s3');
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
// Get the standard public s3 url if s3 is set as storage type
// Uses the nice, short URL if bucket name has no periods in otherwise the longer
// region-based url will be used to prevent http issues.
if (!$storageUrl && config('filesystems.images') === 's3') {
$storageDetails = config('filesystems.disks.s3');
if (strpos($storageDetails['bucket'], '.') === false) {
$storageUrl = 'https://' . $storageDetails['bucket'] . '.s3.amazonaws.com';
} else {
$storageUrl = 'https://s3-' . $storageDetails['region'] . '.amazonaws.com/' . $storageDetails['bucket'];
}
$this->storageUrl = $storageUrl;
}
$basePath = ($this->storageUrl == false) ? url('/') : $this->storageUrl;
$basePath = $storageUrl ?: url('/');
return rtrim($basePath, '/') . $filePath;
}

View File

@@ -19,10 +19,10 @@ class HtmlContentFilter
return $html;
}
$html = '<body>' . $html . '</body>';
$html = '<?xml encoding="utf-8" ?><body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$doc->loadHTML($html);
$xPath = new DOMXPath($doc);
// Remove standard script tags

View File

@@ -24,6 +24,7 @@ class LanguageManager
'bg' => ['iso' => 'bg_BG', 'windows' => 'Bulgarian'],
'bs' => ['iso' => 'bs_BA', 'windows' => 'Bosnian (Latin)'],
'ca' => ['iso' => 'ca', 'windows' => 'Catalan'],
'cs' => ['iso' => 'cs_CZ', 'windows' => 'Czech'],
'da' => ['iso' => 'da_DK', 'windows' => 'Danish'],
'de' => ['iso' => 'de_DE', 'windows' => 'German'],
'de_informal' => ['iso' => 'de_DE', 'windows' => 'German'],
@@ -120,17 +121,17 @@ class LanguageManager
$isoLang = $this->localeMap[$language]['iso'] ?? '';
$isoLangPrefix = explode('_', $isoLang)[0];
$locales = array_filter([
$locales = array_values(array_filter([
$isoLang ? $isoLang . '.utf8' : false,
$isoLang ?: false,
$isoLang ? str_replace('_', '-', $isoLang) : false,
$isoLang ? $isoLangPrefix . '.UTF-8' : false,
$this->localeMap[$language]['windows'] ?? false,
$language,
]);
]));
if (!empty($locales)) {
setlocale(LC_TIME, ...$locales);
setlocale(LC_TIME, $locales[0], ...array_slice($locales, 1));
}
}
}

View File

@@ -147,7 +147,7 @@ function icon(string $name, array $attrs = []): string
}
/**
* Generate a url with multiple parameters for sorting purposes.
* 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.
*/
@@ -172,7 +172,7 @@ function sortUrl(string $path, array $data, array $overrideData = []): string
}
if (count($queryStringSections) === 0) {
return $path;
return url($path);
}
return url($path . '?' . implode('&', $queryStringSections));

BIN
bookstack-system-cli Executable file

Binary file not shown.

View File

@@ -8,7 +8,7 @@
"license": "MIT",
"type": "project",
"require": {
"php": "^7.4|^8.0",
"php": "^8.0.2",
"ext-curl": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
@@ -19,39 +19,37 @@
"bacon/bacon-qr-code": "^2.0",
"barryvdh/laravel-dompdf": "^2.0",
"barryvdh/laravel-snappy": "^1.0",
"doctrine/dbal": "^3.1",
"filp/whoops": "^2.14",
"doctrine/dbal": "^3.5",
"guzzlehttp/guzzle": "^7.4",
"intervention/image": "^2.7",
"laravel/framework": "^8.68",
"laravel/framework": "^9.0",
"laravel/socialite": "^5.2",
"laravel/tinker": "^2.6",
"league/commonmark": "^1.6",
"league/flysystem-aws-s3-v3": "^1.0.29",
"league/commonmark": "^2.3",
"league/flysystem-aws-s3-v3": "^3.0",
"league/html-to-markdown": "^5.0.0",
"league/oauth2-client": "^2.6",
"onelogin/php-saml": "^4.0",
"phpseclib/phpseclib": "~3.0",
"phpseclib/phpseclib": "^3.0",
"pragmarx/google2fa": "^8.0",
"predis/predis": "^1.1",
"predis/predis": "^2.1",
"socialiteproviders/discord": "^4.1",
"socialiteproviders/gitlab": "^4.1",
"socialiteproviders/microsoft-azure": "^5.0.1",
"socialiteproviders/okta": "^4.1",
"socialiteproviders/microsoft-azure": "^5.1",
"socialiteproviders/okta": "^4.2",
"socialiteproviders/slack": "^4.1",
"socialiteproviders/twitch": "^5.3",
"ssddanbrown/htmldiff": "^1.0.2"
},
"require-dev": {
"brianium/paratest": "^6.6",
"fakerphp/faker": "^1.16",
"fakerphp/faker": "^1.21",
"itsgoingd/clockwork": "^5.1",
"mockery/mockery": "^1.4",
"nunomaduro/collision": "^5.10",
"nunomaduro/larastan": "^1.0",
"mockery/mockery": "^1.5",
"nunomaduro/collision": "^6.4",
"nunomaduro/larastan": "^2.4",
"phpunit/phpunit": "^9.5",
"squizlabs/php_codesniffer": "^3.7",
"ssddanbrown/asserthtml": "^1.0"
"ssddanbrown/asserthtml": "^2.0"
},
"autoload": {
"psr-4": {
@@ -73,7 +71,6 @@
"format": "phpcbf",
"lint": "phpcs",
"test": "phpunit",
"t": "@php artisan test --parallel",
"t-reset": "@php artisan test --recreate-databases",
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
@@ -102,7 +99,7 @@
"preferred-install": "dist",
"sort-packages": true,
"platform": {
"php": "7.4.0"
"php": "8.0.2"
}
},
"extra": {
@@ -110,6 +107,6 @@
"dont-discover": []
}
},
"minimum-stability": "dev",
"minimum-stability": "stable",
"prefer-stable": true
}

4190
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,6 +5,6 @@ pull_request_title: Updated translations with latest Crowdin changes
pull_request_labels:
- ":earth_africa: Translations"
files:
- source: /resources/lang/en/*.php
translation: /resources/lang/%two_letters_code%/%original_file_name%
- source: /lang/en/*.php
translation: /lang/%two_letters_code%/%original_file_name%
type: php

View File

@@ -0,0 +1,39 @@
<?php
namespace Database\Factories\Uploads;
use BookStack\Auth\User;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\BookStack\Uploads\Attachment>
*/
class AttachmentFactory extends Factory
{
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = \BookStack\Uploads\Attachment::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'name' => $this->faker->words(2, true),
'path' => $this->faker->url(),
'extension' => '',
'external' => true,
'uploaded_to' => Page::factory(),
'created_by' => User::factory(),
'updated_by' => User::factory(),
'order' => 0,
];
}
}

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateUsersTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -40,4 +40,4 @@ class CreateUsersTable extends Migration
{
Schema::drop('users');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePasswordResetsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -28,4 +28,4 @@ class CreatePasswordResetsTable extends Migration
{
Schema::drop('password_resets');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateBooksTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -30,4 +30,4 @@ class CreateBooksTable extends Migration
{
Schema::drop('books');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePagesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -34,4 +34,4 @@ class CreatePagesTable extends Migration
{
Schema::drop('pages');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateImagesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -29,4 +29,4 @@ class CreateImagesTable extends Migration
{
Schema::drop('images');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateChaptersTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -32,4 +32,4 @@ class CreateChaptersTable extends Migration
{
Schema::drop('chapters');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddUsersToEntities extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -54,4 +54,4 @@ class AddUsersToEntities extends Migration
$table->dropColumn('updated_by');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreatePageRevisionsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -32,4 +32,4 @@ class CreatePageRevisionsTable extends Migration
{
Schema::drop('page_revisions');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateActivitiesTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -33,4 +33,4 @@ class CreateActivitiesTable extends Migration
{
Schema::drop('activities');
}
}
};

View File

@@ -1,8 +1,5 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
* Much of this code has been taken from entrust,
* a role & permission management solution for Laravel.
@@ -12,7 +9,11 @@ use Illuminate\Database\Schema\Blueprint;
* @license MIT
* @url https://github.com/Zizaco/entrust
*/
class AddRolesAndPermissions extends Migration
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
return new class extends Migration
{
/**
* Run the migrations.
@@ -147,4 +148,4 @@ class AddRolesAndPermissions extends Migration
Schema::drop('role_user');
Schema::drop('roles');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateSettingsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -28,4 +28,4 @@ class CreateSettingsTable extends Migration
{
Schema::drop('settings');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddSearchIndexes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -51,4 +51,4 @@ class AddSearchIndexes extends Migration
});
}
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateSocialAccountsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -31,4 +31,4 @@ class CreateSocialAccountsTable extends Migration
{
Schema::drop('social_accounts');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEmailConfirmationTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -36,4 +36,4 @@ class AddEmailConfirmationTable extends Migration
});
Schema::drop('email_confirmations');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class CreateViewsTable extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -31,4 +31,4 @@ class CreateViewsTable extends Migration
{
Schema::drop('views');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEntityIndexes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -86,4 +86,4 @@ class AddEntityIndexes extends Migration
$table->dropIndex('views_viewable_id_index');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class FulltextWeighting extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -51,4 +51,4 @@ class FulltextWeighting extends Migration
});
}
}
}
};

View File

@@ -4,7 +4,7 @@ use BookStack\Uploads\Image;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddImageUploadTypes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -37,4 +37,4 @@ class AddImageUploadTypes extends Migration
$table->dropColumn('path');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddUserAvatars extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -28,4 +28,4 @@ class AddUserAvatars extends Migration
$table->dropColumn('image_id');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddExternalAuthToUsers extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -28,4 +28,4 @@ class AddExternalAuthToUsers extends Migration
$table->dropColumn('external_auth_id');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddSlugToRevisions extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -32,4 +32,4 @@ class AddSlugToRevisions extends Migration
$table->dropColumn('book_slug');
});
}
}
};

View File

@@ -2,7 +2,7 @@
use Illuminate\Database\Migrations\Migration;
class UpdatePermissionsAndRoles extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -113,4 +113,4 @@ class UpdatePermissionsAndRoles extends Migration
}
}
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddEntityAccessControls extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -69,4 +69,4 @@ class AddEntityAccessControls extends Migration
Schema::drop('restrictions');
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddPageRevisionTypes extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -29,4 +29,4 @@ class AddPageRevisionTypes extends Migration
$table->dropColumn('type');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddPageDrafts extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -29,4 +29,4 @@ class AddPageDrafts extends Migration
$table->dropColumn('draft');
});
}
}
};

View File

@@ -3,7 +3,7 @@
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
class AddMarkdownSupport extends Migration
return new class extends Migration
{
/**
* Run the migrations.
@@ -36,4 +36,4 @@ class AddMarkdownSupport extends Migration
$table->dropColumn('markdown');
});
}
}
};

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