Compare commits

..

390 Commits

Author SHA1 Message Date
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
bf075f7dd8 Updated version and assets for release v23.01 2023-01-31 11:59:51 +00:00
Dan Brown
a4fd673285 Merge branch 'development' into release 2023-01-31 11:59:28 +00:00
Dan Brown
813d140213 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2023-01-31 11:39:21 +00:00
Dan Brown
3dc5942a85 Updated translation attribution before v23.01 release 2023-01-31 11:38:56 +00:00
Dan Brown
03e2a9b200 Updated translations with latest Crowdin changes (#3925) 2023-01-31 11:29:36 +00:00
Dan Brown
8367a94e90 Merge pull request #4002 from BookStackApp/color_upgrades
Better application color scheme control
2023-01-28 17:59:54 +00:00
Dan Brown
631546a68a Adjusted/improved some color setting wording 2023-01-28 17:57:43 +00:00
Dan Brown
7751022c66 Updated migration to carry across more colors, updated export
Updated export to use link color for link.
Export will now copy primary color to link color options for stable
upgrades.
2023-01-28 17:49:48 +00:00
Dan Brown
f42ff59b43 Added migration of color settings to dark mode 2023-01-28 17:31:43 +00:00
Dan Brown
104621841b Update JS to show live changes and set light color values 2023-01-28 17:11:15 +00:00
Dan Brown
c337439370 Rolled out use of seperate link color style 2023-01-28 16:06:11 +00:00
Dan Brown
65ebdb7234 Added usage and defaults for dark colors 2023-01-28 15:20:08 +00:00
Dan Brown
e708ce93ba Updated generic tab styles and js to force accessible usage
Added use of more accessible tags to create tabbed-interfaces then
updated css and JS to require use of those attributes rather than custom
techniques.

Updated relevant parts of app.
Some custom parts using their own tabs though, something to improve in
future.
2023-01-28 12:50:51 +00:00
Dan Brown
1f69965c1e Updated settings view to have dark-mode color options
Also added link color option, not yet used.
Cleaned up tabbed interface control design as part of this.
2023-01-28 11:50:46 +00:00
Dan Brown
d7723b33f3 Merge pull request #3999 from BookStackApp/sort_ui_improvements
Improve Book Sorting User Experience
2023-01-27 18:02:14 +00:00
Dan Brown
87e371ffde Added prevention of nested chapters on sort 2023-01-27 17:39:51 +00:00
Dan Brown
b649738718 Made book-sort changes based on screen reader testing
- Removed having sort items in tabbing order since they have no action.
- Updated "show other books" list to add upon single selection since it
  was not clear how these were added (double press) without then seeing
the add button, and even then the add button would be after the scroll
list.
2023-01-27 17:06:39 +00:00
Dan Brown
022cbb9c00 Finished off design and fixing of sort buttons 2023-01-27 16:25:06 +00:00
Dan Brown
40e112fc5b Extracted text & added dropdown for book sort move actions
Primarily styling and testing left to do.
2023-01-27 13:26:58 +00:00
Dan Brown
7cacbaadf0 Added functionality/logic for button-based sorting 2023-01-27 13:08:35 +00:00
Dan Brown
a3e7e754b9 Improves sortable ux
- Fixes multi-select functionality.
- Updated other books to be sticky.
- Added some general intro/desc text.
- Updated sort boxes to be collapsible.
- Cleaned up other books styling.
2023-01-27 11:16:17 +00:00
Dan Brown
03ad288aaa Updated user avatar reset to clear relation id in database
Added test to cover.
For #3977
2023-01-26 17:15:09 +00:00
Dan Brown
811be3a36a Added option to change the OIDC claim regarded as the ID
Defined via a OIDC_EXTERNAL_ID_CLAIM env option.
For #3914
2023-01-26 16:43:15 +00:00
Dan Brown
3202f96181 Tweak tag list to add new row on input instead of change
Prevented interferance with the user's action if they interacted with
something below the tags, since a new row would be added on blur and
hence shift down positions.

For #3931
2023-01-26 16:10:47 +00:00
Dan Brown
f6a6b11ec5 Added and addressed multi-role/own-role-perm/inheretance scenario
Found during manual testing.
Have checked against relation queries manually too.
2023-01-26 12:53:25 +00:00
Dan Brown
48df8725d8 Added better drawing load failure handling
Failure of loading drawings will now close the drawing view and show an
error message, hinting at file or permission issues, instead of leaving
the user facing a continuosly loading interface.

Adds test to cover.

This also updates errors from our HTTP service to be wrapped in a custom
error type for better identification and so the error is an actual
javascript error. Should be object compatible.

Related to #3955.
2023-01-26 12:18:33 +00:00
Dan Brown
25bdd71477 Add scheme and sql-variant code language options
For #3954 and #3942
2023-01-26 11:26:20 +00:00
Dan Brown
deda331745 Fixed global search preview click on safari
Safari needs an element to be focusable to be able to use :focus-within.
For #3926
2023-01-25 21:46:26 +00:00
Dan Brown
f6d3944b20 Merge pull request #3994 from BookStackApp/app_icon_setting
Added ability to control app icon (favicon) via settings
2023-01-25 16:50:48 +00:00
Dan Brown
a50b0ea1e5 Covered app icon setting with testing 2023-01-25 16:41:41 +00:00
Dan Brown
3c658e39ab Extracted app icon text, fixed issues
Tweaked sizes and meta tags based unpon ipad testing.
Fixed reduced sizes not being cleaned up.
2023-01-25 16:11:34 +00:00
Dan Brown
d8354255e7 Added practicali to sponsor list 2023-01-25 12:06:11 +00:00
Dan Brown
55b6a7842e Added ability to control app icon (favicon) via settings 2023-01-25 11:03:19 +00:00
Dan Brown
0f113ec41f Merge pull request #3986 from BookStackApp/permission_testing
Permission Testing & Alignment
2023-01-24 21:37:28 +00:00
Dan Brown
1fa5a31960 Fixed role entity permissions ignoring inheritance
Added additional scnenario tests to cover
2023-01-24 21:26:41 +00:00
Dan Brown
8be36455ab Addressed fallback override cases found during testing
Had misalignment between query and usercan, The nuance between fallback
and entity-role permissions was not taken into account by the query
system. Now added with new test cases to cover.
2023-01-24 20:42:20 +00:00
Dan Brown
d1bd6d0e39 Fixed incorrect field in down migration 2023-01-24 19:21:23 +00:00
Dan Brown
1660e72cc5 Migrated remaining relation permission usages
Now all tests are passing.
Some level of manual checks to do.
2023-01-24 19:04:32 +00:00
Dan Brown
2d1f1abce4 Implemented alternate approach to current joint_permissions
Is a tweak upon the existing approach, mainly to store and query role
permission access in a way that allows muli-level states that may
override eachother. These states are represented in the new PermissionStatus
class.

This also simplifies how own permissions are stored and queried, to be
part of a single column.
2023-01-24 14:55:34 +00:00
Dan Brown
7d74575eb8 Found a sql having-style approach to permissions
As a way to check aggregate queries for required changes to need to
analyse across combined permission values.
2023-01-24 13:44:38 +00:00
Dan Brown
91e613fe60 Shared entity permission logic across both query methods
The runtime userCan() and the JointPermissionBuilder now share much of
the same logic for handling entity permission resolution.
2023-01-23 15:09:03 +00:00
Dan Brown
f3f2a0c1d5 Updated userCan logic to meet expectations in tests
Updated with similar logic to that used in the user_permissions branch,
but all extracted to a seperate class for doing all fetch and collapse
work.
2023-01-23 12:40:11 +00:00
Dan Brown
1c2ae7bff6 Added gmp extension to test workflow
If was not already enabled by default, should enable faster testing
handling as it helps the phpseclib usage for OIDC tokens in test rocket
through.
2023-01-21 21:34:39 +00:00
Dan Brown
78ebcb6f38 Addressed a range of deprecation warnings
Closes #3969
2023-01-21 20:50:04 +00:00
Dan Brown
28dda39260 Updated PHP and JS depenencies 2023-01-21 19:09:19 +00:00
Dan Brown
e2a72d16aa Made adjustments to fit copied work into dev branch
Ported non-compatible elements, Now all tests passing apart from some
specific permission scenario tests which are probably correctly failing.
Updates some tests to better avoid messing environment state.
2023-01-21 13:03:47 +00:00
Dan Brown
c724bfe4d3 Copied over work from user_permissions branch
Only that relevant to the additional testing work.
2023-01-21 11:08:34 +00:00
Dan Brown
6070d804f8 Fixed incorrect pluralisation for de_informal
Updated language system to only use initial part of locale for
translation pluralisation to better match the hard-coded logic of the
built-in MessageSelector. Extends and overrides Laravel's default for
this system.

Added test to cover.
Related to #3976.
2023-01-16 16:56:41 +00:00
Dan Brown
e794c977bc Updated version and assets for release v22.11.1 2022-12-16 23:49:14 +00:00
Dan Brown
0b088ef1d3 Merge branch 'development' into release 2022-12-16 23:48:35 +00:00
Dan Brown
5393465ea7 Updated translator attribution before release v22.11.1 2022-12-16 23:48:04 +00:00
Dan Brown
f5df811b15 Removed old unused style definition 2022-12-16 23:21:24 +00:00
Dan Brown
a521f41838 Fixed lack of scroll in editor toolbox contents
For #2887
2022-12-16 23:16:51 +00:00
Dan Brown
0123d83fb2 Fixed not being able to remove all user roles
User roles would only be actioned if they existed in the form request,
hence removal of all roles would have no data to action upon.
This adds a placeholder 0-id role to ensure there is always role data to
send, even when no roles are selected. This field value is latter
filtered out.

Added test to cover.

Likely related to #3922.
2022-12-16 17:44:13 +00:00
Dan Brown
559e392f1b Merge branch 'development' of https://github.com/jhit/BookStack into jhit-development 2022-12-16 17:12:57 +00:00
Dan Brown
8468b632a1 Updated crowdin config with PR title and labels
Aligns to the title and labelling I already do manually.
2022-12-16 17:11:01 +00:00
Dan Brown
7053a8669f New Crowdin updates (#3881) 2022-12-16 17:06:52 +00:00
Dan Brown
2c0a7346b1 Prevent search focus change on left/right arrow press
For #3920
2022-12-16 17:03:48 +00:00
Dan Brown
bf6a6af683 Updated version and assets for release v22.11 2022-11-30 12:30:21 +00:00
Dan Brown
914790fd99 Merge branch 'development' into release 2022-11-30 12:29:52 +00:00
Jürgen Hörmann
d505642336 Add popular PHP templating languages to code editor
Smarty and Twig are two very popular PHP templating engines and might be
useful to some Bookstack users too.
2022-11-29 14:53:41 +01:00
Dan Brown
edb0c6a9e8 Updated version and assets for release v22.10.2 2022-11-02 15:22:13 +00:00
Dan Brown
84049de696 Merge branch 'v22-10' into release 2022-11-02 15:19:33 +00:00
Dan Brown
da0531e63b Updated version and assets for release v22.10.1 2022-10-21 21:52:32 +01:00
Dan Brown
421dc75f4e Merge branch 'development' into release 2022-10-21 21:52:16 +01:00
Dan Brown
8ae91df038 Updated version and assets for release v22.10 2022-10-21 11:16:45 +01:00
Dan Brown
64b41dd626 Merge branch 'development' into release 2022-10-21 11:16:25 +01:00
Dan Brown
ebd6e4d3a2 Updated version and assets for release v22.09.1 2022-09-20 13:19:34 +01:00
Dan Brown
80374aea5c Merge branch 'development' into release 2022-09-20 13:19:03 +01:00
Dan Brown
2ac9efae7d Updated version and assets for release v22.09 2022-09-08 12:41:09 +01:00
Dan Brown
a11d565ba4 Merge branch 'development' into release 2022-09-08 12:40:57 +01:00
Dan Brown
1fdf854ea7 Updated version and assets for release v22.07.3 2022-08-11 15:17:06 +01:00
Dan Brown
e9c9792cb9 Merge branch 'development' into release 2022-08-11 15:16:34 +01:00
Dan Brown
5ae524c25a Updated version and assets for release v22.07.2 2022-08-09 13:55:52 +01:00
Dan Brown
0d7287fc8b Merge branch 'development' into release 2022-08-09 13:55:40 +01:00
Dan Brown
e77c96f6b7 Updated version and assets for release v22.07.1 2022-08-02 11:47:25 +01:00
Dan Brown
9b8a10dd3a Merge branch 'development' into release 2022-08-02 11:47:08 +01:00
Dan Brown
49200ca5ce Updated version and assets for release v22.07 2022-07-28 14:53:15 +01:00
Dan Brown
34aa4dbf10 Merge branch 'development' into release 2022-07-28 14:53:01 +01:00
Dan Brown
5ee79d16c9 Updated version and assets for release v22.06.2 2022-06-28 11:57:37 +01:00
Dan Brown
a1ea4006e0 Merge branch 'development' into release 2022-06-28 11:57:24 +01:00
Dan Brown
9078188939 Updated version and assets for release v22.06.1 2022-06-25 14:33:07 +01:00
Dan Brown
ed0aad1a7a Merge branch 'development' into release 2022-06-25 14:32:49 +01:00
Dan Brown
5c59cfb020 Updated version and assets for release v22.06 2022-06-24 11:50:56 +01:00
Dan Brown
3ca15ad68a Merge branch 'development' into release 2022-06-24 11:45:29 +01:00
Dan Brown
60014989f5 Updated version and assets for release v22.04.2 2022-05-09 16:10:16 +01:00
Dan Brown
57b10f195e Merge branch 'development' into release 2022-05-09 16:09:54 +01:00
Dan Brown
b1e95eb39f Updated version and assets for release v22.04.1 2022-05-04 21:26:58 +01:00
Dan Brown
b3da77b8f9 Merge branch 'development' into release 2022-05-04 21:26:31 +01:00
Dan Brown
1a345b74bb Updated version and assets for release v22.04 2022-04-29 15:55:32 +01:00
Dan Brown
8ffc3a4abf Merge branch 'development' into release 2022-04-29 15:55:05 +01:00
Dan Brown
7233c1c7b2 Updated version and assets for release v22.03.1 2022-03-30 19:37:07 +01:00
Dan Brown
1309a01131 Merge branch 'development' into release 2022-03-30 19:36:45 +01:00
Dan Brown
0333185b6d Updated version and assets for release v22.03 2022-03-30 13:49:17 +01:00
Dan Brown
83f89f64e8 Merge branch 'development' into release 2022-03-30 13:49:05 +01:00
Dan Brown
11a1a6fb16 Updated version and assets for release v22.02.3 2022-03-07 15:12:22 +00:00
Dan Brown
882c609296 Merge branch 'development' into release 2022-03-07 15:12:09 +00:00
Dan Brown
176a0dcd59 Updated version and assets for release v22.02.2 2022-03-01 22:45:41 +00:00
Dan Brown
94b0f70bfa Merge branch 'development' into release 2022-03-01 22:45:12 +00:00
Dan Brown
08b2a77d41 Updated version and assets for release v22.02.1 2022-02-27 17:46:06 +00:00
Dan Brown
3e8e9a23cf Merge branch 'development' into release 2022-02-27 17:45:49 +00:00
Dan Brown
58b83b64c8 Updated version and assets for release v22.02 2022-02-26 12:01:44 +00:00
Dan Brown
dfe4cde6ee Merge branch 'development' into release 2022-02-26 12:00:46 +00:00
Dan Brown
d11144d9e2 Updated version and assets for release v21.12.5 2022-02-06 15:49:23 +00:00
Dan Brown
f96b0ea5f3 Merge branch 'development' into release 2022-02-06 15:48:55 +00:00
Dan Brown
815f8d79ed Updated version and assets for release v21.12.4 2022-02-01 11:52:24 +00:00
Dan Brown
b62dab32e0 Merge branch 'development' into release 2022-02-01 11:51:48 +00:00
Dan Brown
262f863981 Updated version and assets for release v21.12.3 2022-01-24 22:49:42 +00:00
Dan Brown
a4c94390a1 Merge branch 'master' into release 2022-01-24 22:49:31 +00:00
Dan Brown
53f3cca85d Updated version and assets for release v21.12.2 2022-01-10 18:23:44 +00:00
Dan Brown
ed08bbcecc Merge branch 'master' into release 2022-01-10 18:23:19 +00:00
Dan Brown
de97ebf9b7 Updated version and assets for release v21.12.1 2022-01-06 12:20:37 +00:00
Dan Brown
f492a660a8 Merge branch 'master' into release 2022-01-06 12:20:26 +00:00
Dan Brown
09436836a5 Updated version and assets for release v21.12 2021-12-22 17:04:18 +00:00
Dan Brown
bb455d7788 Merge branch 'master' into release 2021-12-22 17:03:50 +00:00
Dan Brown
009212ab80 Updated version and assets for release v21.11.3 2021-12-15 14:08:37 +00:00
Dan Brown
ba9cb591c8 Merge branch 'master' into release 2021-12-15 14:08:17 +00:00
Dan Brown
d00ac2f34e Updated version and assets for release v21.11.2 2021-11-30 14:30:19 +00:00
Dan Brown
bd4dc6d463 Merge branch 'master' into release 2021-11-30 14:29:53 +00:00
Dan Brown
d91180a909 Updated version and assets for release v21.11.1 2021-11-23 20:44:36 +00:00
Dan Brown
bc2913a5cb Merge branch 'master' into release 2021-11-23 20:44:12 +00:00
Dan Brown
4802394562 Updated version and assets for release v21.11 2021-11-16 13:22:24 +00:00
Dan Brown
1755556468 Merge branch 'master' into release 2021-11-16 13:21:44 +00:00
Dan Brown
01cdbdb7ae Updated version and assets for release v21.10.3 2021-11-01 13:31:10 +00:00
Dan Brown
fc8bbf3eab Merge branch 'master' into release 2021-11-01 13:30:36 +00:00
Dan Brown
3cdab19319 Updated version and assets for release v21.10.2 2021-10-28 15:57:04 +01:00
Dan Brown
5661d20e87 Merge branch 'master' into release 2021-10-28 15:56:49 +01:00
Dan Brown
91f80123e8 Merge branch 'master' into release 2021-10-27 12:35:00 +01:00
Dan Brown
7a0636d0f8 Updated version and assets for release v21.10.1 2021-10-27 12:31:40 +01:00
Dan Brown
0fe5bdfbac Updated version and assets for release v21.10 2021-10-25 15:59:23 +01:00
Dan Brown
f88687e977 Merge branch 'master' into release 2021-10-25 15:58:59 +01:00
Dan Brown
68d437d05b Updated version and assets for release v21.08.6 2021-10-15 14:34:44 +01:00
Dan Brown
1e56aaea04 Merge branch 'master' into release 2021-10-15 14:34:23 +01:00
Dan Brown
dab170a6fe Updated version and assets for release v21.08.5 2021-10-08 22:25:36 +01:00
Dan Brown
a8de717d9b Merge branch 'master' into release 2021-10-08 22:25:05 +01:00
Dan Brown
78fe95b6fc Updated version and assets for release v21.08.4 2021-10-04 16:25:24 +01:00
Dan Brown
e0c24e41aa Merge branch 'master' into release 2021-10-04 16:24:54 +01:00
Dan Brown
fa8553839b Updated version and assets for release v21.08.3 2021-09-12 16:31:02 +01:00
Dan Brown
b8fcefc794 Merge branch 'master' into release 2021-09-12 16:30:35 +01:00
Dan Brown
88bcb68fcb Updated version and assets for release v21.08.2 2021-09-04 15:07:20 +01:00
Dan Brown
7c000553ae Merge branch 'master' into release 2021-09-04 15:06:33 +01:00
Dan Brown
391fa35c80 Updated version and assets for release v21.08.1 2021-09-02 21:13:09 +01:00
Dan Brown
c6773a8c9f Merge branch 'master' into release 2021-09-02 21:12:06 +01:00
Dan Brown
9b226e7d39 Updated version and assets for release v21.08 2021-08-31 22:07:53 +01:00
Dan Brown
9865446267 Merge branch 'master' into release 2021-08-31 22:07:23 +01:00
Dan Brown
926abbe776 Updated version and assets for release v21.05.4 2021-08-04 21:29:10 +01:00
Dan Brown
4fabef3a57 Merge branch 'v21.05.x' into release 2021-08-04 21:28:45 +01:00
Dan Brown
5ef4cd80c3 Updated version and assets for release v21.05.3 2021-07-03 11:59:52 +01:00
Dan Brown
e01f23583f Merge branch 'v21.05.x' into release 2021-07-03 11:59:21 +01:00
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
78886b1e67 Updated version and assets for release v21.04.1 2021-04-19 22:26:19 +01:00
Dan Brown
d9debaf032 Merge branch 'master' into release 2021-04-19 22:25:29 +01:00
Dan Brown
d4360d6347 Updated version and assets for release v21.04 2021-04-09 21:18:32 +01:00
Dan Brown
175b1785c0 Merge branch 'master' into release 2021-04-09 21:18:09 +01:00
Dan Brown
c8740c0171 Updated version for release v0.31.8 2021-03-13 15:32:54 +00:00
Dan Brown
91ee895a74 Merge branch 'v0.31.x' into release 2021-03-13 15:32:06 +00:00
Dan Brown
a045e46571 Updated version for release v0.31.7 2021-03-02 21:19:17 +00:00
Dan Brown
44eaa65c3b Merge branch 'v0.31.x' into release 2021-03-02 21:18:31 +00:00
Dan Brown
0a22af7b14 Updated version for release v0.31.6 2021-02-06 14:41:19 +00:00
Dan Brown
b54702ab08 Merge branch 'v0.31.x' into release 2021-02-06 14:40:47 +00:00
Dan Brown
c4fdcfc5d1 Updated version for release v0.31.5 2021-02-02 20:58:06 +00:00
Dan Brown
cb8117e8df Merge branch 'v0.31.x' into release 2021-02-02 20:57:41 +00:00
Dan Brown
5a218d5056 Updated version and assets for release v0.31.4 2021-01-16 17:50:45 +00:00
Dan Brown
8dbc5cf9c6 Merge branch 'master' into release 2021-01-16 17:50:11 +00:00
Dan Brown
71e81615a3 Updated version for release v0.31.3 2021-01-10 23:29:58 +00:00
Dan Brown
611d37da04 Merge branch 'master' into release 2021-01-10 23:29:11 +00:00
Dan Brown
0e799a3857 Updated version and assets for release v0.31.2 2021-01-10 14:05:16 +00:00
Dan Brown
b91d6e2bfa Merge branch 'master' into release 2021-01-10 14:04:59 +00:00
Dan Brown
ea16ad7e94 Updated version and assets for release v0.31.1 2021-01-04 18:41:55 +00:00
Dan Brown
ba6eb54552 Merge branch 'master' into release 2021-01-04 18:41:26 +00:00
Dan Brown
f705e7683b Updated assets for release v0.31.0 again 2021-01-03 22:33:36 +00:00
Dan Brown
dc996adb20 Merge branch 'master' into release 2021-01-03 22:32:40 +00:00
Dan Brown
a64c638ccc Updated version and assets for release v0.31.0 2021-01-03 21:52:37 +00:00
Dan Brown
359c067279 Merge branch 'master' into release 2021-01-03 21:52:00 +00:00
Dan Brown
66a746e297 Updated version for release v0.30.7 2020-12-18 14:13:40 +00:00
Dan Brown
a4d43ee24b Merge branch 'v0.30.x' into release 2020-12-18 14:13:19 +00:00
Dan Brown
f7793a70a9 Updated version for release v0.30.6 2020-12-17 21:07:06 +00:00
Dan Brown
ceba3d31fb Merge branch 'v0.30.x' into release 2020-12-17 21:03:20 +00:00
Dan Brown
eecc08edde Updated version for release v0.30.5 2020-12-06 21:05:43 +00:00
Dan Brown
eb19aadc75 Merge branch 'v0.30.x' into release 2020-12-06 21:05:11 +00:00
Dan Brown
06c81e69b9 Updated version and assets for release v0.30.4 2020-10-31 16:52:33 +00:00
Dan Brown
3dc3d4a639 Merge branch 'master' into release 2020-10-31 16:51:54 +00:00
Dan Brown
94c59c1e3d Updated version and assets for release v0.30.3 2020-10-13 22:50:52 +01:00
Dan Brown
4d2205853a Merge branch 'master' into release 2020-10-13 22:50:30 +01:00
Dan Brown
751772b87a Updated version and assets for release v0.30.2 2020-09-30 22:44:58 +01:00
Dan Brown
76e30869e1 Merge branch 'master' into release 2020-09-30 22:44:17 +01:00
Dan Brown
3edc9fe9eb Updated version and assets for release v0.30.1 2020-09-26 17:51:37 +01:00
Dan Brown
616c62703e Merge branch 'master' into release 2020-09-26 17:50:25 +01:00
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
4818192a2a Updated version and assets for release v0.29.1 2020-04-28 12:30:31 +01:00
Dan Brown
965dd97f54 Merge branch 'master' into release 2020-04-28 12:30:09 +01:00
Dan Brown
195b74926c Updated version and assets for release v0.29.0 2020-04-13 16:10:23 +01:00
Dan Brown
2120db12b2 Merge branch 'master' into release 2020-04-13 16:10:11 +01:00
Dan Brown
ed563fef28 Updated version and assets for release v0.28.3 2020-03-14 22:31:42 +00:00
Dan Brown
0d31a8e3f1 Merge branch 'master' into release 2020-03-14 22:31:11 +00:00
Dan Brown
b8354b974b Updated version and assets for release v0.28.2 2020-02-15 22:36:08 +00:00
Dan Brown
034c1e289d Merge branch 'master' into release 2020-02-15 22:35:46 +00:00
Dan Brown
f31605a3de Updated version and assets for release v0.28.1 2020-02-15 22:08:06 +00:00
Dan Brown
e7cc75c74d Merge branch 'master' into release 2020-02-15 22:07:17 +00:00
Dan Brown
4b79d5e4e8 Updated version and assets for release v0.28.0 2020-02-03 22:44:45 +00:00
Dan Brown
34854915b3 Merge branch 'master' into release 2020-02-03 22:43:58 +00:00
Dan Brown
af6f34b529 Updated version and assets for release v0.27.5 2019-10-16 16:35:50 +01:00
Dan Brown
fb82a2b896 Merge branch 'patching-v0.27' into release 2019-10-16 16:35:10 +01:00
Dan Brown
5b464938b6 Updated version and assets for release v0.27.4 2019-09-07 13:30:08 +01:00
Dan Brown
81f954890d Merge branch 'patching-v0.27' into release 2019-09-07 13:29:53 +01:00
Dan Brown
0e2bbcec62 Updated version and assets for release v0.27.3 2019-09-03 21:50:12 +01:00
Dan Brown
fdd339f525 Merge branch 'master' into release 2019-09-03 21:49:46 +01:00
Dan Brown
8cf7d6a83d Updated version and assets for release v0.27.2 2019-09-01 12:12:23 +01:00
Dan Brown
58a5008718 Merge branch 'master' into release 2019-09-01 12:12:10 +01:00
Dan Brown
c44a8df55d Updated version and assets for release v0.27.1 2019-09-01 11:13:50 +01:00
Dan Brown
ff1494c519 Merge branch 'master' into release 2019-09-01 11:13:18 +01:00
Dan Brown
b8ce8fd852 Updated assets for release v0.27 2019-08-31 14:16:14 +01:00
Dan Brown
75e7454a5f Merge branch 'master' into release and set version 2019-08-31 14:15:18 +01:00
Dan Brown
2558ea8931 Updated version for release v0.26.4 2019-08-06 21:42:09 +01:00
Dan Brown
ac0f47a4b2 Merge branch 'v0.26' into release 2019-08-06 21:41:06 +01:00
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
b70a5c0cdb Updated version and assets for release v0.26.1 2019-05-07 23:05:47 +01:00
Dan Brown
9443ae9f40 Merge branch 'master' into release 2019-05-07 23:05:10 +01:00
Dan Brown
220c2a4102 Updated version and assets for release v0.26.0 2019-05-06 18:58:56 +01:00
Dan Brown
e9914eb301 Merge branch 'master' into release 2019-05-06 18:57:58 +01:00
Dan Brown
934512d09c Updated version and assets for release v0.25.5 2019-03-24 19:45:17 +00:00
Dan Brown
9102c90986 Merge branch 'master' into release 2019-03-24 19:45:00 +00:00
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
95b3e78573 Updated version and assets for release v0.25.0 2019-01-12 22:48:53 +00:00
Dan Brown
63a345bc93 Merge branch 'master' into release 2019-01-12 22:47:07 +00:00
Dan Brown
e093a172cb Updated assets and version for release v0.24.3 2018-11-27 21:52:20 +00:00
Dan Brown
4b01f8934b Merge branch 'master' into release 2018-11-27 21:51:32 +00:00
Dan Brown
bc116b45b5 Re-updated assets for release v0.24.2 2018-11-10 16:10:22 +00:00
Dan Brown
a059960b9e Merge branch 'master' into release 2018-11-10 16:09:14 +00:00
Dan Brown
7770966fed Updated assets for release v0.24.2 2018-11-10 16:01:55 +00:00
Dan Brown
d7adcf6c69 Merge branch 'master' into release 2018-11-10 16:01:01 +00:00
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
6ad21ce885 Added built assets for release 2015-11-30 21:59:34 +00:00
348 changed files with 4938 additions and 3363 deletions

View File

@@ -268,6 +268,7 @@ OIDC_DUMP_USER_DETAILS=false
OIDC_USER_TO_GROUPS=false
OIDC_GROUPS_CLAIM=groups
OIDC_REMOVE_FROM_GROUPS=false
OIDC_EXTERNAL_ID_CLAIM=sub
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option

View File

@@ -176,7 +176,7 @@ Alexander Predl (Harveyhase68) :: German
Rem (Rem9000) :: Dutch
Michał Stelmach (stelmach-web) :: Polish
arniom :: French
REMOVED_USER :: ; Dutch; Turkish
REMOVED_USER :: ; French; Dutch; Turkish
林祖年 (contagion) :: Chinese Traditional
Siamak Guodarzi (siamakgoudarzi88) :: Persian
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
@@ -283,13 +283,13 @@ Kuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified
digilady :: Greek
Linus (LinusOP) :: Swedish
Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian
RandomUser0815 :: German
RandomUser0815 :: German Informal; German
Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
구인회 (laskdjlaskdj12) :: Korean
LiZerui (CNLiZerui) :: Chinese Traditional
Fabrice Boyer (FabriceBoyer) :: French
mikael (bitcanon) :: Swedish
Matthias Mai (schnapsidee) :: German
Matthias Mai (schnapsidee) :: German; German Informal
Ufuk Ayyıldız (ufukayyildiz) :: Turkish
Jan Mitrof (jan.kachlik) :: Czech
edwardsmirnov :: Russian
@@ -298,3 +298,13 @@ shotu :: French
Cesar_Lopez_Aguillon :: Spanish
bdewoop :: German
dina davoudi (dina.davoudi) :: Persian
Angelos Chouvardas (achouvardas) :: Greek
rndrss :: Portuguese, Brazilian
rirac294 :: Russian
David Furman (thefourCraft) :: Hebrew
Pafzedog :: French
Yllelder :: Spanish
Adrian Ocneanu (aocneanu) :: Romanian
Eduardo Castanho (EduardoCastanho) :: Portuguese
VIET NAM VPS (vietnamvps) :: Vietnamese
m4tthi4s :: French

View File

@@ -16,7 +16,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
extensions: gd, mbstring, json, curl, xml, mysql, ldap
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
- name: Get Composer Cache Directory
id: composer-cache

6
.gitignore vendored
View File

@@ -5,10 +5,10 @@ Homestead.yaml
.idea
npm-debug.log
yarn-error.log
/public/dist
/public/dist/*.map
/public/plugins
/public/css
/public/js
/public/css/*.map
/public/js/*.map
/public/bower
/public/build/
/storage/images

View File

@@ -2,10 +2,12 @@
namespace BookStack\Actions;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
use Illuminate\Support\Str;
@@ -40,6 +42,12 @@ class Activity extends Model
return $this->belongsTo(User::class);
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type');
}
/**
* Returns text from the language files, Looks up by using the activity key.
*/

View File

@@ -15,8 +15,6 @@ class ActivityQueries
{
protected PermissionApplicator $permissions;
protected array $fieldsForLists = ['id', 'type', 'detail', 'activities.entity_type', 'activities.entity_id', 'user_id', 'created_at'];
public function __construct(PermissionApplicator $permissions)
{
$this->permissions = $permissions;
@@ -27,11 +25,9 @@ class ActivityQueries
*/
public function latest(int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->whereNotNull('activities.entity_id')
->with(['user', 'entity'])
->skip($count * $page)
->take($count)
@@ -82,12 +78,10 @@ class ActivityQueries
*/
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$query = Activity::query()->select($this->fieldsForLists);
$activityList = $this->permissions
->restrictEntityRelationQuery($query, 'activities', 'entity_id', 'entity_type')
->restrictEntityRelationQuery(Activity::query(), 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->whereNotNull('activities.entity_id')
->skip($count * $page)
->take($count)
->get();

View File

@@ -2,7 +2,9 @@
namespace BookStack\Actions;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
@@ -16,4 +18,10 @@ class Favourite extends Model
{
return $this->morphTo();
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')
->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');
}
}

View File

@@ -2,8 +2,10 @@
namespace BookStack\Actions;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
@@ -27,6 +29,12 @@ class Tag extends Model
return $this->morphTo('entity');
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');
}
/**
* Get a full URL to start a tag name search for this tag name.
*/

View File

@@ -29,16 +29,15 @@ class TagRepo
$sort = 'value';
}
$entityTypeCol = DB::getTablePrefix() . 'tags.entity_type';
$query = Tag::query()
->select([
'name',
($searchTerm || $nameFilter) ? 'value' : DB::raw('COUNT(distinct value) as `values`'),
DB::raw('COUNT(id) as usages'),
DB::raw("SUM(IF({$entityTypeCol} = 'page', 1, 0)) as page_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'chapter', 1, 0)) as chapter_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'book', 1, 0)) as book_count"),
DB::raw("SUM(IF({$entityTypeCol} = 'bookshelf', 1, 0)) as shelf_count"),
DB::raw('SUM(IF(entity_type = \'page\', 1, 0)) as page_count'),
DB::raw('SUM(IF(entity_type = \'chapter\', 1, 0)) as chapter_count'),
DB::raw('SUM(IF(entity_type = \'book\', 1, 0)) as book_count'),
DB::raw('SUM(IF(entity_type = \'bookshelf\', 1, 0)) as shelf_count'),
])
->orderBy($sort, $listOptions->getOrder());

View File

@@ -2,8 +2,10 @@
namespace BookStack\Actions;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
@@ -28,6 +30,12 @@ class View extends Model
return $this->morphTo();
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')
->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');
}
/**
* Increment the current user's view count for the given viewable model.
*/

View File

@@ -198,7 +198,8 @@ class OidcService
*/
protected function getUserDetails(OidcIdToken $token): array
{
$id = $token->getClaim('sub');
$idClaim = $this->config()['external_id_claim'];
$id = $token->getClaim($idClaim);
return [
'external_id' => $id,

View File

@@ -1,18 +0,0 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Model;
/**
* @property int $id
* @property ?int $role_id
* @property ?int $user_id
* @property string $entity_type
* @property int $entity_id
* @property bool $view
*/
class CollapsedPermission extends Model
{
protected $table = 'entity_permissions_collapsed';
}

View File

@@ -1,278 +0,0 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
/**
* Collapsed permissions act as a "flattened" view of entity-level permissions in the system
* so inheritance does not have to managed as part of permission querying.
*/
class CollapsedPermissionBuilder
{
/**
* Re-generate all collapsed permissions from scratch.
*/
public function rebuildForAll()
{
DB::table('entity_permissions_collapsed')->truncate();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) {
$this->buildForBooks($books, false);
});
// Chunk through all bookshelves
Bookshelf::query()->withTrashed()
->select(['id'])
->chunk(50, function (EloquentCollection $shelves) {
$this->generateCollapsedPermissions($shelves->all());
});
}
/**
* Rebuild the collapsed permissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildForBooks($books, true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildForEntities($entities);
}
/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'book_id', 'chapter_id']);
},
]);
}
/**
* Build collapsed permissions for the given books.
*/
protected function buildForBooks(EloquentCollection $books, bool $deleteOld)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteForEntities($entities->all());
}
$this->generateCollapsedPermissions($entities->all());
}
/**
* Rebuild the collapsed permissions for a collection of entities.
*/
protected function buildForEntities(array $entities)
{
$this->deleteForEntities($entities);
$this->generateCollapsedPermissions($entities);
}
/**
* Delete the stored collapsed permissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('entity_permissions_collapsed')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}
/**
* Convert the given list of entities into "SimpleEntityData" representations
* for faster usage and property access.
*
* @param Entity[] $entities
*
* @return SimpleEntityData[]
*/
protected function entitiesToSimpleEntities(array $entities): array
{
$simpleEntities = [];
foreach ($entities as $entity) {
$attrs = $entity->getAttributes();
$simple = new SimpleEntityData();
$simple->id = $attrs['id'];
$simple->type = $entity->getMorphClass();
$simple->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
$simpleEntities[] = $simple;
}
return $simpleEntities;
}
/**
* Create & Save collapsed entity permissions.
*
* @param Entity[] $originalEntities
*/
protected function generateCollapsedPermissions(array $originalEntities)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$collapsedPermData = [];
// Fetch related entity permissions
$permissions = $this->getEntityPermissionsForEntities($entities);
// Create a mapping of explicit entity permissions
$permissionMap = new EntityPermissionMap($permissions);
// Create Joint Permission Data
foreach ($entities as $entity) {
array_push($collapsedPermData, ...$this->createCollapsedPermissionData($entity, $permissionMap));
}
DB::transaction(function () use ($collapsedPermData) {
foreach (array_chunk($collapsedPermData, 1000) as $dataChunk) {
DB::table('entity_permissions_collapsed')->insert($dataChunk);
}
});
}
/**
* Create collapsed permission data for the given entity using the given permission map.
*/
protected function createCollapsedPermissionData(SimpleEntityData $entity, EntityPermissionMap $permissionMap): array
{
$chain = [
$entity->type . ':' . $entity->id,
$entity->chapter_id ? ('chapter:' . $entity->chapter_id) : null,
$entity->book_id ? ('book:' . $entity->book_id) : null,
];
$permissionData = [];
$overridesApplied = [];
foreach ($chain as $entityTypeId) {
if ($entityTypeId === null) {
continue;
}
$permissions = $permissionMap->getForEntity($entityTypeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType() . ':' . $permission->getAssignedTypeId();
if (!isset($overridesApplied[$related])) {
$permissionData[] = [
'role_id' => $permission->role_id,
'user_id' => $permission->user_id,
'view' => $permission->view,
'entity_type' => $entity->type,
'entity_id' => $entity->id,
];
$overridesApplied[$related] = true;
}
}
}
return $permissionData;
}
/**
* From the given entity list, provide back a mapping of entity types to
* the ids of that given type. The type used is the DB morph class.
*
* @param SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];
foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}
$idsByType[$entity->type][] = $entity->id;
}
return $idsByType;
}
/**
* Get the entity permissions for all the given entities.
*
* @param SimpleEntityData[] $entities
*
* @return EntityPermission[]
*/
protected function getEntityPermissionsForEntities(array $entities): array
{
$idsByType = $this->entitiesToTypeIdMap($entities);
$permissionFetch = EntityPermission::query()
->where(function (Builder $query) use ($idsByType) {
foreach ($idsByType as $type => $ids) {
$query->orWhere(function (Builder $query) use ($type, $ids) {
$query->where('entity_type', '=', $type)->whereIn('entity_id', $ids);
});
}
});
return $permissionFetch->get()->all();
}
}

View File

@@ -3,14 +3,13 @@
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* @property int $id
* @property int $role_id
* @property int $user_id
* @property int $entity_id
* @property string $entity_type
* @property boolean $view
@@ -22,9 +21,17 @@ class EntityPermission extends Model
{
public const PERMISSIONS = ['view', 'create', 'update', 'delete'];
protected $fillable = ['role_id', 'user_id', 'view', 'create', 'update', 'delete'];
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');
}
/**
* Get the role assigned to this entity permission.
*/
@@ -32,38 +39,4 @@ class EntityPermission extends Model
{
return $this->belongsTo(Role::class);
}
/**
* Get the user assigned to this entity permission.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
/**
* Get the type of entity permission this is.
* Will be one of: user, role, fallback
*/
public function getAssignedType(): string
{
if ($this->user_id) {
return 'user';
}
if ($this->role_id) {
return 'role';
}
return 'fallback';
}
/**
* Get the ID for the assigned type of permission.
* (Role/User ID). Defaults to 0 for fallback.
*/
public function getAssignedTypeId(): int
{
return $this->user_id ?? $this->role_id ?? 0;
}
}

View File

@@ -0,0 +1,141 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Entity;
use Illuminate\Database\Eloquent\Builder;
class EntityPermissionEvaluator
{
protected string $action;
public function __construct(string $action)
{
$this->action = $action;
}
public function evaluateEntityForUser(Entity $entity, array $userRoleIds): ?bool
{
if ($this->isUserSystemAdmin($userRoleIds)) {
return true;
}
$typeIdChain = $this->gatherEntityChainTypeIds(SimpleEntityData::fromEntity($entity));
$relevantPermissions = $this->getPermissionsMapByTypeId($typeIdChain, [...$userRoleIds, 0]);
$permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
$status = $this->evaluatePermitsByType($permitsByType);
return is_null($status) ? null : $status === PermissionStatus::IMPLICIT_ALLOW || $status === PermissionStatus::EXPLICIT_ALLOW;
}
/**
* @param array<string, array<string, int>> $permitsByType
*/
protected function evaluatePermitsByType(array $permitsByType): ?int
{
// Return grant or reject from role-level if exists
if (count($permitsByType['role']) > 0) {
return max($permitsByType['role']) ? PermissionStatus::EXPLICIT_ALLOW : PermissionStatus::EXPLICIT_DENY;
}
// Return fallback permission if exists
if (count($permitsByType['fallback']) > 0) {
return $permitsByType['fallback'][0] ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
}
return null;
}
/**
* @param string[] $typeIdChain
* @param array<string, EntityPermission[]> $permissionMapByTypeId
* @return array<string, array<string, int>>
*/
protected function collapseAndCategorisePermissions(array $typeIdChain, array $permissionMapByTypeId): array
{
$permitsByType = ['fallback' => [], 'role' => []];
foreach ($typeIdChain as $typeId) {
$permissions = $permissionMapByTypeId[$typeId] ?? [];
foreach ($permissions as $permission) {
$roleId = $permission->role_id;
$type = $roleId === 0 ? 'fallback' : 'role';
if (!isset($permitsByType[$type][$roleId])) {
$permitsByType[$type][$roleId] = $permission->{$this->action};
}
}
if (isset($permitsByType['fallback'][0])) {
break;
}
}
return $permitsByType;
}
/**
* @param string[] $typeIdChain
* @return array<string, EntityPermission[]>
*/
protected function getPermissionsMapByTypeId(array $typeIdChain, array $filterRoleIds): array
{
$query = EntityPermission::query()->where(function (Builder $query) use ($typeIdChain) {
foreach ($typeIdChain as $typeId) {
$query->orWhere(function (Builder $query) use ($typeId) {
[$type, $id] = explode(':', $typeId);
$query->where('entity_type', '=', $type)
->where('entity_id', '=', $id);
});
}
});
if (!empty($filterRoleIds)) {
$query->where(function (Builder $query) use ($filterRoleIds) {
$query->whereIn('role_id', [...$filterRoleIds, 0]);
});
}
$relevantPermissions = $query->get(['entity_id', 'entity_type', 'role_id', $this->action])->all();
$map = [];
foreach ($relevantPermissions as $permission) {
$key = $permission->entity_type . ':' . $permission->entity_id;
if (!isset($map[$key])) {
$map[$key] = [];
}
$map[$key][] = $permission;
}
return $map;
}
/**
* @return string[]
*/
protected function gatherEntityChainTypeIds(SimpleEntityData $entity): array
{
// The array order here is very important due to the fact we walk up the chain
// elsewhere in the class. Earlier items in the chain have higher priority.
$chain = [$entity->type . ':' . $entity->id];
if ($entity->type === 'page' && $entity->chapter_id) {
$chain[] = 'chapter:' . $entity->chapter_id;
}
if ($entity->type === 'page' || $entity->type === 'chapter') {
$chain[] = 'book:' . $entity->book_id;
}
return $chain;
}
protected function isUserSystemAdmin($userRoleIds): bool
{
$adminRoleId = Role::getSystemRole('admin')->id;
return in_array($adminRoleId, $userRoleIds);
}
}

View File

@@ -1,37 +0,0 @@
<?php
namespace BookStack\Auth\Permissions;
class EntityPermissionMap
{
protected array $map = [];
/**
* @param EntityPermission[] $permissions
*/
public function __construct(array $permissions = [])
{
foreach ($permissions as $entityPermission) {
$this->addPermission($entityPermission);
}
}
protected function addPermission(EntityPermission $permission)
{
$entityCombinedId = $permission->entity_type . ':' . $permission->entity_id;
if (!isset($this->map[$entityCombinedId])) {
$this->map[$entityCombinedId] = [];
}
$this->map[$entityCombinedId][] = $permission;
}
/**
* @return EntityPermission[]
*/
public function getForEntity(string $typeIdString): array
{
return $this->map[$typeIdString] ?? [];
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Entity;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphOne;
class JointPermission extends Model
{
protected $primaryKey = null;
public $timestamps = false;
/**
* Get the role that this points to.
*/
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
/**
* Get the entity this points to.
*/
public function entity(): MorphOne
{
return $this->morphOne(Entity::class, 'entity');
}
}

View File

@@ -0,0 +1,292 @@
<?php
namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Facades\DB;
/**
* Joint permissions provide a pre-query "cached" table of view permissions for all core entity
* types for all roles in the system. This class generates out that table for different scenarios.
*/
class JointPermissionBuilder
{
/**
* Re-generate all entity permission from scratch.
*/
public function rebuildForAll()
{
JointPermission::query()->truncate();
// Get all roles (Should be the most limited dimension)
$roles = Role::query()->with('permissions')->get()->all();
// Chunk through all books
$this->bookFetchQuery()->chunk(5, function (EloquentCollection $books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
Bookshelf::query()->withTrashed()->select(['id', 'owned_by'])
->chunk(50, function (EloquentCollection $shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
}
/**
* Rebuild the entity jointPermissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
{
$entities = [$entity];
if ($entity instanceof Book) {
$books = $this->bookFetchQuery()->where('id', '=', $entity->id)->get();
$this->buildJointPermissionsForBooks($books, Role::query()->with('permissions')->get()->all(), true);
return;
}
/** @var BookChild $entity */
if ($entity->book) {
$entities[] = $entity->book;
}
if ($entity instanceof Page && $entity->chapter_id) {
$entities[] = $entity->chapter;
}
if ($entity instanceof Chapter) {
foreach ($entity->pages as $page) {
$entities[] = $page;
}
}
$this->buildJointPermissionsForEntities($entities);
}
/**
* Build the entity jointPermissions for a particular role.
*/
public function rebuildForRole(Role $role)
{
$roles = [$role];
$role->jointPermissions()->delete();
$role->load('permissions');
// Chunk through all books
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
$this->buildJointPermissionsForBooks($books, $roles);
});
// Chunk through all bookshelves
Bookshelf::query()->select(['id', 'owned_by'])
->chunk(50, function ($shelves) use ($roles) {
$this->createManyJointPermissions($shelves->all(), $roles);
});
}
/**
* Get a query for fetching a book with its children.
*/
protected function bookFetchQuery(): Builder
{
return Book::query()->withTrashed()
->select(['id', 'owned_by'])->with([
'chapters' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id']);
},
'pages' => function ($query) {
$query->withTrashed()->select(['id', 'owned_by', 'book_id', 'chapter_id']);
},
]);
}
/**
* Build joint permissions for the given book and role combinations.
*/
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
{
$entities = clone $books;
/** @var Book $book */
foreach ($books->all() as $book) {
foreach ($book->getRelation('chapters') as $chapter) {
$entities->push($chapter);
}
foreach ($book->getRelation('pages') as $page) {
$entities->push($page);
}
}
if ($deleteOld) {
$this->deleteManyJointPermissionsForEntities($entities->all());
}
$this->createManyJointPermissions($entities->all(), $roles);
}
/**
* Rebuild the entity jointPermissions for a collection of entities.
*/
protected function buildJointPermissionsForEntities(array $entities)
{
$roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities);
$this->createManyJointPermissions($entities, $roles);
}
/**
* Delete all the entity jointPermissions for a list of entities.
*
* @param Entity[] $entities
*/
protected function deleteManyJointPermissionsForEntities(array $entities)
{
$simpleEntities = $this->entitiesToSimpleEntities($entities);
$idsByType = $this->entitiesToTypeIdMap($simpleEntities);
DB::transaction(function () use ($idsByType) {
foreach ($idsByType as $type => $ids) {
foreach (array_chunk($ids, 1000) as $idChunk) {
DB::table('joint_permissions')
->where('entity_type', '=', $type)
->whereIn('entity_id', $idChunk)
->delete();
}
}
});
}
/**
* @param Entity[] $entities
*
* @return SimpleEntityData[]
*/
protected function entitiesToSimpleEntities(array $entities): array
{
$simpleEntities = [];
foreach ($entities as $entity) {
$simple = SimpleEntityData::fromEntity($entity);
$simpleEntities[] = $simple;
}
return $simpleEntities;
}
/**
* Create & Save entity jointPermissions for many entities and roles.
*
* @param Entity[] $originalEntities
* @param Role[] $roles
*/
protected function createManyJointPermissions(array $originalEntities, array $roles)
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$jointPermissions = [];
// Fetch related entity permissions
$permissions = new MassEntityPermissionEvaluator($entities, 'view');
// Create a mapping of role permissions
$rolePermissionMap = [];
foreach ($roles as $role) {
foreach ($role->permissions as $permission) {
$rolePermissionMap[$role->getRawAttribute('id') . ':' . $permission->getRawAttribute('name')] = true;
}
}
// Create Joint Permission Data
foreach ($entities as $entity) {
foreach ($roles as $role) {
$jp = $this->createJointPermissionData(
$entity,
$role->getRawAttribute('id'),
$permissions,
$rolePermissionMap,
$role->system_name === 'admin'
);
$jointPermissions[] = $jp;
}
}
DB::transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
DB::table('joint_permissions')->insert($jointPermissionChunk);
}
});
}
/**
* From the given entity list, provide back a mapping of entity types to
* the ids of that given type. The type used is the DB morph class.
*
* @param SimpleEntityData[] $entities
*
* @return array<string, int[]>
*/
protected function entitiesToTypeIdMap(array $entities): array
{
$idsByType = [];
foreach ($entities as $entity) {
if (!isset($idsByType[$entity->type])) {
$idsByType[$entity->type] = [];
}
$idsByType[$entity->type][] = $entity->id;
}
return $idsByType;
}
/**
* Create entity permission data for an entity and role
* for a particular action.
*/
protected function createJointPermissionData(SimpleEntityData $entity, int $roleId, MassEntityPermissionEvaluator $permissionMap, array $rolePermissionMap, bool $isAdminRole): array
{
// Ensure system admin role retains permissions
if ($isAdminRole) {
return $this->createJointPermissionDataArray($entity, $roleId, PermissionStatus::EXPLICIT_ALLOW, true);
}
// Return evaluated entity permission status if it has an affect.
$entityPermissionStatus = $permissionMap->evaluateEntityForRole($entity, $roleId);
if ($entityPermissionStatus !== null) {
return $this->createJointPermissionDataArray($entity, $roleId, $entityPermissionStatus, false);
}
// Otherwise default to the role-level permissions
$permissionPrefix = $entity->type . '-view';
$roleHasPermission = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-all']);
$roleHasPermissionOwn = isset($rolePermissionMap[$roleId . ':' . $permissionPrefix . '-own']);
$status = $roleHasPermission ? PermissionStatus::IMPLICIT_ALLOW : PermissionStatus::IMPLICIT_DENY;
return $this->createJointPermissionDataArray($entity, $roleId, $status, $roleHasPermissionOwn);
}
/**
* Create an array of data with the information of an entity jointPermissions.
* Used to build data for bulk insertion.
*/
protected function createJointPermissionDataArray(SimpleEntityData $entity, int $roleId, int $permissionStatus, bool $hasPermissionOwn): array
{
$ownPermissionActive = ($hasPermissionOwn && $permissionStatus !== PermissionStatus::EXPLICIT_DENY && $entity->owned_by);
return [
'entity_id' => $entity->id,
'entity_type' => $entity->type,
'role_id' => $roleId,
'status' => $permissionStatus,
'owner_id' => $ownPermissionActive ? $entity->owned_by : null,
];
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace BookStack\Auth\Permissions;
class MassEntityPermissionEvaluator extends EntityPermissionEvaluator
{
/**
* @var SimpleEntityData[]
*/
protected array $entitiesInvolved;
protected array $permissionMapCache;
public function __construct(array $entitiesInvolved, string $action)
{
$this->entitiesInvolved = $entitiesInvolved;
parent::__construct($action);
}
public function evaluateEntityForRole(SimpleEntityData $entity, int $roleId): ?int
{
$typeIdChain = $this->gatherEntityChainTypeIds($entity);
$relevantPermissions = $this->getPermissionMapByTypeIdForChainAndRole($typeIdChain, $roleId);
$permitsByType = $this->collapseAndCategorisePermissions($typeIdChain, $relevantPermissions);
return $this->evaluatePermitsByType($permitsByType);
}
/**
* @param string[] $typeIdChain
* @return array<string, EntityPermission[]>
*/
protected function getPermissionMapByTypeIdForChainAndRole(array $typeIdChain, int $roleId): array
{
$allPermissions = $this->getPermissionMapByTypeIdAndRoleForAllInvolved();
$relevantPermissions = [];
// Filter down permissions to just those for current typeId
// and current roleID or fallback permissions.
foreach ($typeIdChain as $typeId) {
$relevantPermissions[$typeId] = [
...($allPermissions[$typeId][$roleId] ?? []),
...($allPermissions[$typeId][0] ?? [])
];
}
return $relevantPermissions;
}
/**
* @return array<string, array<int, EntityPermission[]>>
*/
protected function getPermissionMapByTypeIdAndRoleForAllInvolved(): array
{
if (isset($this->permissionMapCache)) {
return $this->permissionMapCache;
}
$entityTypeIdChain = [];
foreach ($this->entitiesInvolved as $entity) {
$entityTypeIdChain[] = $entity->type . ':' . $entity->id;
}
$permissionMap = $this->getPermissionsMapByTypeId($entityTypeIdChain, []);
// Manipulate permission map to also be keyed by roleId.
foreach ($permissionMap as $typeId => $permissions) {
$permissionMap[$typeId] = [];
foreach ($permissions as $permission) {
$roleId = $permission->getRawAttribute('role_id');
if (!isset($permissionMap[$typeId][$roleId])) {
$permissionMap[$typeId][$roleId] = [];
}
$permissionMap[$typeId][$roleId][] = $permission;
}
}
$this->permissionMapCache = $permissionMap;
return $this->permissionMapCache;
}
}

View File

@@ -4,7 +4,6 @@ namespace BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use BookStack\Model;
@@ -12,8 +11,6 @@ use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Facades\DB;
use InvalidArgumentException;
class PermissionApplicator
@@ -50,7 +47,7 @@ class PermissionApplicator
return $hasRolePermission;
}
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $user->id, $action);
$hasApplicableEntityPermissions = $this->hasEntityPermission($ownable, $userRoleIds, $action);
return is_null($hasApplicableEntityPermissions) ? $hasRolePermission : $hasApplicableEntityPermissions;
}
@@ -59,75 +56,11 @@ class PermissionApplicator
* Check if there are permissions that are applicable for the given entity item, action and roles.
* Returns null when no entity permissions are in force.
*/
protected function hasEntityPermission(Entity $entity, array $userRoleIds, int $userId, string $action): ?bool
protected function hasEntityPermission(Entity $entity, array $userRoleIds, string $action): ?bool
{
$this->ensureValidEntityAction($action);
$adminRoleId = Role::getSystemRole('admin')->id;
if (in_array($adminRoleId, $userRoleIds)) {
return true;
}
// The array order here is very important due to the fact we walk up the chain
// in the flattening loop below. Earlier items in the chain have higher priority.
$typeIdList = [$entity->getMorphClass() . ':' . $entity->id];
if ($entity instanceof Page && $entity->chapter_id) {
$typeIdList[] = 'chapter:' . $entity->chapter_id;
}
if ($entity instanceof Page || $entity instanceof Chapter) {
$typeIdList[] = 'book:' . $entity->book_id;
}
$relevantPermissions = EntityPermission::query()
->where(function (Builder $query) use ($typeIdList) {
foreach ($typeIdList as $typeId) {
$query->orWhere(function (Builder $query) use ($typeId) {
[$type, $id] = explode(':', $typeId);
$query->where('entity_type', '=', $type)
->where('entity_id', '=', $id);
});
}
})->where(function (Builder $query) use ($userRoleIds, $userId) {
$query->whereIn('role_id', $userRoleIds)
->orWhere('user_id', '=', $userId)
->orWhere(function (Builder $query) {
$query->whereNull(['role_id', 'user_id']);
});
})->get(['entity_id', 'entity_type', 'role_id', 'user_id', $action])
->all();
$permissionMap = new EntityPermissionMap($relevantPermissions);
$permitsByType = ['user' => [], 'fallback' => [], 'role' => []];
// Collapse and simplify permission structure
foreach ($typeIdList as $typeId) {
$permissions = $permissionMap->getForEntity($typeId);
foreach ($permissions as $permission) {
$related = $permission->getAssignedType();
$relatedId = $permission->getAssignedTypeId();
if (!isset($permitsByType[$related][$relatedId])) {
$permitsByType[$related][$relatedId] = $permission->$action;
}
}
}
// Return user-level permission if exists
if (count($permitsByType['user']) > 0) {
return boolval(array_values($permitsByType['user'])[0]);
}
// Return grant or reject from role-level if exists
if (count($permitsByType['role']) > 0) {
return boolval(max($permitsByType['role']));
}
// Return fallback permission if exists
if (count($permitsByType['fallback']) > 0) {
return boolval($permitsByType['fallback'][0]);
}
return null;
return (new EntityPermissionEvaluator($action))->evaluateEntityForUser($entity, $userRoleIds);
}
/**
@@ -140,10 +73,7 @@ class PermissionApplicator
$permissionQuery = EntityPermission::query()
->where($action, '=', true)
->where(function (Builder $query) {
$query->whereIn('role_id', $this->getCurrentUserRoleIds())
->orWhere('user_id', '=', $this->currentUser()->id);
});
->whereIn('role_id', $this->getCurrentUserRoleIds());
if (!empty($entityClass)) {
/** @var Entity $entityInstance */
@@ -160,140 +90,20 @@ class PermissionApplicator
* Limit the given entity query so that the query will only
* return items that the user has view permission for.
*/
public function restrictEntityQuery(Builder $query, string $morphClass): Builder
public function restrictEntityQuery(Builder $query): Builder
{
$this->applyPermissionsToQuery($query, $query->getModel()->getTable(), $morphClass, 'id', '');
return $query;
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionsToQuery($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn): void
{
if ($this->currentUser()->hasSystemRole('admin')) {
return;
}
$this->applyFallbackJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyRoleJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyUserJoin($query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
$this->applyPermissionWhereFilter($query, $queryTable, $entityTypeLimiter, $entityTypeColumn);
}
/**
* Apply the where condition to a permission restricting query, to limit based upon the values of the joined
* permission data. Query must have joins pre-applied.
* Either entityTypeLimiter or entityTypeColumn should be supplied, with the other empty.
* Both should not be applied since that would conflict upon intent.
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionWhereFilter($query, string $queryTable, string $entityTypeLimiter, string $entityTypeColumn)
{
$abilities = ['all' => [], 'own' => []];
$types = $entityTypeLimiter ? [$entityTypeLimiter] : ['page', 'chapter', 'bookshelf', 'book'];
$fullEntityTypeColumn = $queryTable . '.' . $entityTypeColumn;
foreach ($types as $type) {
$abilities['all'][$type] = userCan($type . '-view-all');
$abilities['own'][$type] = userCan($type . '-view-own');
}
$abilities['all'] = array_filter($abilities['all']);
$abilities['own'] = array_filter($abilities['own']);
$query->where(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->where('perms_user', '=', 1)
->orWhere(function (Builder $query) {
$query->whereNull('perms_user')->where('perms_role', '=', 1);
})->orWhere(function (Builder $query) {
$query->whereNull(['perms_user', 'perms_role'])
->where('perms_fallback', '=', 1);
});
if (count($abilities['all']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback']);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
if (count($abilities['own']) > 0) {
$query->orWhere(function (Builder $query) use ($abilities, $fullEntityTypeColumn, $entityTypeColumn) {
$query->whereNull(['perms_user', 'perms_role', 'perms_fallback'])
->where('owned_by', '=', $this->currentUser()->id);
if ($entityTypeColumn) {
$query->whereIn($fullEntityTypeColumn, array_keys($abilities['all']));
}
});
}
return $query->where(function (Builder $parentQuery) {
$parentQuery->whereHas('jointPermissions', function (Builder $permissionQuery) {
$permissionQuery->select(['entity_id', 'entity_type'])
->selectRaw('max(owner_id) as owner_id')
->selectRaw('max(status) as status')
->whereIn('role_id', $this->getCurrentUserRoleIds())
->groupBy(['entity_type', 'entity_id'])
->havingRaw('(status IN (1, 3) or (owner_id = ? and status != 2))', [$this->currentUser()->id]);
});
});
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyPermissionJoin(callable $joinCallable, string $subAlias, $query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$joinCondition = $this->getJoinCondition($queryTable, $subAlias, $entityIdColumn, $entityTypeColumn);
$query->joinSub(function (QueryBuilder $joinQuery) use ($joinCallable, $entityTypeLimiter) {
$joinQuery->select(['entity_id', 'entity_type'])->from('entity_permissions_collapsed')
->groupBy('entity_id', 'entity_type');
$joinCallable($joinQuery);
if ($entityTypeLimiter) {
$joinQuery->where('entity_type', '=', $entityTypeLimiter);
}
}, $subAlias, $joinCondition, null, null, 'left');
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyUserJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_user')
->where('user_id', '=', $this->currentUser()->id);
}, 'p_u', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyRoleJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_role')
->whereIn('role_id', $this->getCurrentUserRoleIds());
}, 'p_r', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
/**
* @param Builder|QueryBuilder $query
*/
protected function applyFallbackJoin($query, string $queryTable, string $entityTypeLimiter, string $entityIdColumn, string $entityTypeColumn)
{
$this->applyPermissionJoin(function (QueryBuilder $joinQuery) {
$joinQuery->selectRaw('max(view) as perms_fallback')
->whereNull(['role_id', 'user_id']);
}, 'p_f', $query, $queryTable, $entityTypeLimiter, $entityIdColumn, $entityTypeColumn);
}
protected function getJoinCondition(string $queryTable, string $joinTableName, string $entityIdColumn, string $entityTypeColumn): callable
{
return function (JoinClause $join) use ($queryTable, $joinTableName, $entityIdColumn, $entityTypeColumn) {
$join->on($queryTable . '.' . $entityIdColumn, '=', $joinTableName . '.entity_id');
if ($entityTypeColumn) {
$join->on($queryTable . '.' . $entityTypeColumn, '=', $joinTableName . '.entity_type');
}
};
}
/**
* Extend the given page query to ensure draft items are not visible
* unless created by the given user.
@@ -313,28 +123,23 @@ class PermissionApplicator
* Filter items that have entities set as a polymorphic relation.
* For simplicity, this will not return results attached to draft pages.
* Draft pages should never really have related items though.
*
* @param Builder|QueryBuilder $query
*/
public function restrictEntityRelationQuery($query, string $tableName, string $entityIdColumn, string $entityTypeColumn)
public function restrictEntityRelationQuery(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn): Builder
{
$query->leftJoinSub(function (QueryBuilder $query) {
$query->select(['id as entity_id', DB::raw("'page' as entity_type"), 'owned_by', 'deleted_at', 'draft'])->from('pages');
$tablesByType = ['page' => 'pages', 'book' => 'books', 'chapter' => 'chapters', 'bookshelf' => 'bookshelves'];
foreach ($tablesByType as $type => $table) {
$query->unionAll(function (QueryBuilder $query) use ($type, $table) {
$query->select(['id as entity_id', DB::raw("'{$type}' as entity_type"), 'owned_by', 'deleted_at', DB::raw('0 as draft')])->from($table);
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$pageMorphClass = (new Page())->getMorphClass();
return $this->restrictEntityQuery($query)
->where(function ($query) use ($tableDetails, $pageMorphClass) {
/** @var Builder $query */
$query->where($tableDetails['entityTypeColumn'], '!=', $pageMorphClass)
->orWhereExists(function (QueryBuilder $query) use ($tableDetails, $pageMorphClass) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->where($tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'], '=', $pageMorphClass)
->where('pages.draft', '=', false);
});
}
}, 'entities', function (JoinClause $join) use ($tableName, $entityIdColumn, $entityTypeColumn) {
$join->on($tableName . '.' . $entityIdColumn, '=', 'entities.entity_id')
->on($tableName . '.' . $entityTypeColumn, '=', 'entities.entity_type');
});
$this->applyPermissionsToQuery($query, $tableName, '', $entityIdColumn, $entityTypeColumn);
// TODO - Test page draft access (Might allow drafts which should not be seen)
return $query;
});
}
/**
@@ -345,12 +150,16 @@ class PermissionApplicator
*/
public function restrictPageRelationQuery(Builder $query, string $tableName, string $pageIdColumn): Builder
{
$morphClass = (new Page())->getMorphClass();
$this->applyPermissionsToQuery($query, $tableName, $morphClass, $pageIdColumn, '');
// TODO - Draft display
// TODO - Likely need owned_by entity join workaround as used above
return $query;
$fullPageIdColumn = $tableName . '.' . $pageIdColumn;
return $this->restrictEntityQuery($query)
->where(function ($query) use ($fullPageIdColumn) {
/** @var Builder $query */
$query->whereExists(function (QueryBuilder $query) use ($fullPageIdColumn) {
$query->select('id')->from('pages')
->whereColumn('pages.id', '=', $fullPageIdColumn)
->where('pages.draft', '=', false);
});
});
}
/**

View File

@@ -21,32 +21,19 @@ class PermissionFormData
{
return $this->entity->permissions()
->with('role')
->whereNotNull('role_id')
->where('role_id', '!=', 0)
->get()
->sortBy('role.display_name')
->all();
}
/**
* Get the permissions with assigned users.
*/
public function permissionsWithUsers(): array
{
return $this->entity->permissions()
->with('user')
->whereNotNull('user_id')
->get()
->sortBy('user.name')
->all();
}
/**
* Get the roles that don't yet have specific permissions for the
* entity we're managing permissions for.
*/
public function rolesNotAssigned(): array
{
$assigned = $this->entity->permissions()->whereNotNull('role_id')->pluck('role_id');
$assigned = $this->entity->permissions()->pluck('role_id');
return Role::query()
->where('system_name', '!=', 'admin')
->whereNotIn('id', $assigned)
@@ -62,19 +49,20 @@ class PermissionFormData
{
/** @var ?EntityPermission $permission */
$permission = $this->entity->permissions()
->whereNull(['role_id', 'user_id'])
->where('role_id', '=', 0)
->first();
return $permission ?? (new EntityPermission());
}
/**
* Check if the "Everyone else" option is inheriting default role system permissions.
* Is determined by any system entity_permission existing for the current entity.
* Get the "Everyone Else" role entry.
*/
public function everyoneElseInheriting(): bool
public function everyoneElseRole(): Role
{
return !$this->entity->permissions()
->whereNull(['role_id', 'user_id'])
->exists();
return (new Role())->forceFill([
'id' => 0,
'display_name' => trans('entities.permissions_role_everyone_else'),
'description' => trans('entities.permissions_role_everyone_else_desc'),
]);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace BookStack\Auth\Permissions;
class PermissionStatus
{
const IMPLICIT_DENY = 0;
const IMPLICIT_ALLOW = 1;
const EXPLICIT_DENY = 2;
const EXPLICIT_ALLOW = 3;
}

View File

@@ -11,13 +11,13 @@ use Illuminate\Database\Eloquent\Collection;
class PermissionsRepo
{
protected CollapsedPermissionBuilder $permissionBuilder;
protected array $systemRoles = ['admin', 'public'];
protected JointPermissionBuilder $permissionBuilder;
protected $systemRoles = ['admin', 'public'];
/**
* PermissionsRepo constructor.
*/
public function __construct(CollapsedPermissionBuilder $permissionBuilder)
public function __construct(JointPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
}
@@ -57,6 +57,7 @@ class PermissionsRepo
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
@@ -87,6 +88,7 @@ class PermissionsRepo
$role->fill($roleData);
$role->mfa_enforced = ($roleData['mfa_enforced'] ?? 'false') === 'true';
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
}
@@ -138,7 +140,7 @@ class PermissionsRepo
}
$role->entityPermissions()->delete();
$role->collapsedPermissions()->delete();
$role->jointPermissions()->delete();
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}

View File

@@ -2,10 +2,27 @@
namespace BookStack\Auth\Permissions;
use BookStack\Entities\Models\Entity;
class SimpleEntityData
{
public int $id;
public string $type;
public int $owned_by;
public ?int $book_id;
public ?int $chapter_id;
public static function fromEntity(Entity $entity): self
{
$attrs = $entity->getAttributes();
$simple = new self();
$simple->id = $attrs['id'];
$simple->type = $entity->getMorphClass();
$simple->owned_by = $attrs['owned_by'] ?? 0;
$simple->book_id = $attrs['book_id'] ?? null;
$simple->chapter_id = $attrs['chapter_id'] ?? null;
return $simple;
}
}

View File

@@ -2,8 +2,8 @@
namespace BookStack\Auth;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Interfaces\Loggable;
use BookStack\Model;
@@ -39,6 +39,14 @@ class Role extends Model implements Loggable
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
* Get all related JointPermissions.
*/
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
/**
* The RolePermissions that belong to the role.
*/
@@ -55,14 +63,6 @@ class Role extends Model implements Loggable
return $this->hasMany(EntityPermission::class);
}
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/**
* Check if this role has a permission.
*/

View File

@@ -5,8 +5,6 @@ namespace BookStack\Auth;
use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken;
use BookStack\Auth\Access\Mfa\MfaValue;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable;
use BookStack\Interfaces\Sluggable;
@@ -202,6 +200,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
public function attachRole(Role $role)
{
$this->roles()->attach($role->id);
$this->unsetRelation('roles');
}
/**
@@ -300,22 +299,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
}, 'activities', 'users.id', '=', 'activities.user_id');
}
/**
* Get the entity permissions assigned to this specific user.
*/
public function entityPermissions(): HasMany
{
return $this->hasMany(EntityPermission::class);
}
/**
* Get all related entity collapsed permissions.
*/
public function collapsedPermissions(): HasMany
{
return $this->hasMany(CollapsedPermission::class);
}
/**
* Get the url for editing this user.
*/

View File

@@ -153,8 +153,6 @@ class UserRepo
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->mfaValues()->delete();
$user->collapsedPermissions()->delete();
$user->entityPermissions()->delete();
$user->delete();
// Delete user profile images
@@ -236,6 +234,8 @@ class UserRepo
*/
protected function setUserRoles(User $user, array $roles)
{
$roles = array_filter(array_values($roles));
if ($this->demotingLastAdmin($user, $roles)) {
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
}

View File

@@ -8,9 +8,12 @@ return [
// Dump user details after a login request for debugging purposes
'dump_user_details' => env('OIDC_DUMP_USER_DETAILS', false),
// Attribute, within a OpenId token, to find the user's display name
// Claim, within an OpenId token, to find the user's display name
'display_name_claims' => explode('|', env('OIDC_DISPLAY_NAME_CLAIMS', 'name')),
// Claim, within an OpenID token, to use to connect a BookStack user to the OIDC user.
'external_id_claim' => env('OIDC_EXTERNAL_ID_CLAIM', 'sub'),
// OAuth2/OpenId client id, as configured in your Authorization server.
'client_id' => env('OIDC_CLIENT_ID', null),

View File

@@ -16,11 +16,20 @@ return [
'app-editor' => 'wysiwyg',
'app-color' => '#206ea7',
'app-color-light' => 'rgba(32,110,167,0.15)',
'link-color' => '#206ea7',
'bookshelf-color' => '#a94747',
'book-color' => '#077b70',
'chapter-color' => '#af4d0d',
'page-color' => '#206ea7',
'page-draft-color' => '#7e50b1',
'app-color-dark' => '#195785',
'app-color-light-dark' => 'rgba(32,110,167,0.15)',
'link-color-dark' => '#429fe3',
'bookshelf-color-dark' => '#ff5454',
'book-color-dark' => '#389f60',
'chapter-color-dark' => '#ee7a2d',
'page-color-dark' => '#429fe3',
'page-draft-color-dark' => '#a66ce8',
'app-custom-head' => false,
'registration-enabled' => false,

View File

@@ -2,7 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
@@ -22,12 +22,12 @@ class RegeneratePermissions extends Command
*/
protected $description = 'Regenerate all system permissions';
protected CollapsedPermissionBuilder $permissionBuilder;
protected JointPermissionBuilder $permissionBuilder;
/**
* Create a new command instance.
*/
public function __construct(CollapsedPermissionBuilder $permissionBuilder)
public function __construct(JointPermissionBuilder $permissionBuilder)
{
$this->permissionBuilder = $permissionBuilder;
parent::__construct();

View File

@@ -7,9 +7,9 @@ use BookStack\Actions\Comment;
use BookStack\Actions\Favourite;
use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\CollapsedPermission;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Deletable;
@@ -69,7 +69,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
*/
public function scopeVisible(Builder $query): Builder
{
return app()->make(PermissionApplicator::class)->restrictEntityQuery($query, $this->getMorphClass());
return app()->make(PermissionApplicator::class)->restrictEntityQuery($query);
}
/**
@@ -187,11 +187,11 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
}
/**
* Get the entity collapsed permissions this is connected to.
* Get the entity jointPermissions this is connected to.
*/
public function collapsedPermissions(): MorphMany
public function jointPermissions(): MorphMany
{
return $this->morphMany(CollapsedPermission::class, 'entity');
return $this->morphMany(JointPermission::class, 'entity');
}
/**
@@ -292,7 +292,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
*/
public function rebuildPermissions()
{
app()->make(CollapsedPermissionBuilder::class)->rebuildForEntity(clone $this);
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
}
/**

View File

@@ -5,10 +5,10 @@ namespace BookStack\Entities\Tools\Markdown;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Extension\TaskList\TaskListExtension;
use League\CommonMark\MarkdownConverter;
class MarkdownToHtml
{
@@ -26,7 +26,7 @@ class MarkdownToHtml
$environment->addExtension(new TaskListExtension());
$environment->addExtension(new CustomStrikeThroughExtension());
$environment = Theme::dispatch(ThemeEvents::COMMONMARK_ENVIRONMENT_CONFIGURE, $environment) ?? $environment;
$converter = new CommonMarkConverter([], $environment);
$converter = new MarkdownConverter($environment);
$environment->addBlockRenderer(ListItem::class, new CustomListItemRenderer(), 10);

View File

@@ -10,6 +10,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Entity;
use BookStack\Facades\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
class PermissionsUpdater
{
@@ -57,30 +58,13 @@ class PermissionsUpdater
protected function formatPermissionsFromRequestToEntityPermissions(array $permissions): array
{
$formatted = [];
$columnsByType = [
'role' => 'role_id',
'user' => 'user_id',
'fallback' => '',
];
foreach ($permissions as $type => $byId) {
$column = $columnsByType[$type] ?? null;
if (is_null($column)) {
continue;
}
foreach ($byId as $id => $info) {
$entityPermissionData = [];
if (!empty($column)) {
$entityPermissionData[$column] = $id;
}
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
}
$formatted[] = $entityPermissionData;
foreach ($permissions as $roleId => $info) {
$entityPermissionData = ['role_id' => $roleId];
foreach (EntityPermission::PERMISSIONS as $permission) {
$entityPermissionData[$permission] = (($info[$permission] ?? false) === "true");
}
$formatted[] = $entityPermissionData;
}
return $formatted;

View File

@@ -372,7 +372,7 @@ class TrashCan
$entity->permissions()->delete();
$entity->tags()->delete();
$entity->comments()->delete();
$entity->collapsedPermissions()->delete();
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
$entity->deletions()->delete();
$entity->favourites()->delete();

View File

@@ -66,14 +66,19 @@ class DrawioImageController extends Controller
*/
public function getAsBase64($id)
{
$image = $this->imageRepo->getById($id);
if (is_null($image) || $image->type !== 'drawio' || !userCan('page-view', $image->getPage())) {
return $this->jsonError('Image data could not be found');
try {
$image = $this->imageRepo->getById($id);
} catch (Exception $exception) {
return $this->jsonError(trans('errors.drawing_data_not_found'), 404);
}
if ($image->type !== 'drawio' || !userCan('page-view', $image->getPage())) {
return $this->jsonError(trans('errors.drawing_data_not_found'), 404);
}
$imageData = $this->imageRepo->getImageData($image);
if (is_null($imageData)) {
return $this->jsonError('Image data could not be found');
return $this->jsonError(trans('errors.drawing_data_not_found'), 404);
}
return response()->json([

View File

@@ -5,7 +5,6 @@ namespace BookStack\Http\Controllers;
use BookStack\Auth\Permissions\EntityPermission;
use BookStack\Auth\Permissions\PermissionFormData;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Models\Chapter;
@@ -163,35 +162,10 @@ class PermissionsController extends Controller
{
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
/** @var Role $role */
$role = Role::query()->findOrFail($roleId);
return view('form.entity-permissions-row', [
'modelType' => 'role',
'modelId' => $role->id,
'modelName' => $role->display_name,
'modelDescription' => $role->description,
'permission' => new EntityPermission(),
'entityType' => $entityType,
'inheriting' => false,
]);
}
/**
* Get an empty entity permissions form row for the given user.
*/
public function formRowForUser(string $entityType, string $userId)
{
$this->checkPermissionOr('restrictions-manage-all', fn() => userCan('restrictions-manage-own'));
/** @var User $user */
$user = User::query()->findOrFail($userId);
return view('form.entity-permissions-row', [
'modelType' => 'user',
'modelId' => $user->id,
'modelName' => $user->name,
'modelDescription' => '',
'role' => $role,
'permission' => new EntityPermission(),
'entityType' => $entityType,
'inheriting' => false,

View File

@@ -4,20 +4,14 @@ namespace BookStack\Http\Controllers;
use BookStack\Actions\ActivityType;
use BookStack\Auth\User;
use BookStack\Settings\AppSettingsStore;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
class SettingController extends Controller
{
protected ImageRepo $imageRepo;
protected array $settingCategories = ['features', 'customization', 'registration'];
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
}
/**
* Handle requests to the settings index path.
*/
@@ -48,37 +42,17 @@ class SettingController extends Controller
/**
* Update the specified settings in storage.
*/
public function update(Request $request, string $category)
public function update(Request $request, AppSettingsStore $store, string $category)
{
$this->ensureCategoryExists($category);
$this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => array_merge(['nullable'], $this->getImageValidationRules()),
'app_logo' => ['nullable', ...$this->getImageValidationRules()],
'app_icon' => ['nullable', ...$this->getImageValidationRules()],
]);
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
$key = str_replace('setting-', '', trim($name));
if (strpos($name, 'setting-') !== 0) {
continue;
}
setting()->put($key, $value);
}
// Update logo image if set
if ($category === 'customization' && $request->hasFile('app_logo')) {
$logoFile = $request->file('app_logo');
$this->imageRepo->destroyByType('system');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
setting()->put('app-logo', $image->url);
}
// Clear logo image if requested
if ($category === 'customization' && $request->get('app_logo_reset', null)) {
$this->imageRepo->destroyByType('system');
setting()->remove('app-logo');
}
$store->storeFromUpdateRequest($request, $category);
$this->logActivity(ActivityType::SETTINGS_UPDATE, $category);
$this->showSuccessNotification(trans('settings.settings_save_success'));

View File

@@ -164,6 +164,8 @@ class UserController extends Controller
// Delete the profile image if reset option is in request
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
$user->image_id = 0;
$user->save();
}
$redirectUrl = userCan('users-manage') ? '/settings/users' : "/settings/users/{$user->id}";

View File

@@ -3,10 +3,41 @@
namespace BookStack\Providers;
use BookStack\Translation\FileLoader;
use BookStack\Translation\MessageSelector;
use Illuminate\Translation\TranslationServiceProvider as BaseProvider;
use Illuminate\Translation\Translator;
class TranslationServiceProvider extends BaseProvider
{
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->registerLoader();
// This is a tweak upon Laravel's based translation service registration to allow
// usage of a custom MessageSelector class
$this->app->singleton('translator', function ($app) {
$loader = $app['translation.loader'];
// When registering the translator component, we'll need to set the default
// locale as well as the fallback locale. So, we'll grab the application
// configuration so we can easily get both of these values from there.
$locale = $app['config']['app.locale'];
$trans = new Translator($loader, $locale);
$trans->setFallback($app['config']['app.fallback_locale']);
$trans->setSelector(new MessageSelector());
return $trans;
});
}
/**
* Register the translation line loader.
* Overrides the default register action from Laravel so a custom loader can be used.

View File

@@ -2,7 +2,9 @@
namespace BookStack\References;
use BookStack\Auth\Permissions\JointPermission;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
@@ -24,4 +26,10 @@ class Reference extends Model
{
return $this->morphTo('to');
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'from_id')
->whereColumn('references.from_type', '=', 'joint_permissions.entity_type');
}
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\References;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Models\Page;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
@@ -23,8 +24,7 @@ class ReferenceFetcher
*/
public function getPageReferencesToEntity(Entity $entity): Collection
{
$baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass())
$baseQuery = $this->queryPageReferencesToEntity($entity)
->with([
'from' => fn (Relation $query) => $query->select(Page::$listAttributes),
'from.book' => fn (Relation $query) => $query->scopes('visible'),
@@ -47,11 +47,8 @@ class ReferenceFetcher
*/
public function getPageReferenceCountToEntity(Entity $entity): int
{
$baseQuery = $entity->referencesTo()
->where('from_type', '=', (new Page())->getMorphClass());
$count = $this->permissions->restrictEntityRelationQuery(
$baseQuery,
$this->queryPageReferencesToEntity($entity),
'references',
'from_id',
'from_type'
@@ -59,4 +56,12 @@ class ReferenceFetcher
return $count;
}
protected function queryPageReferencesToEntity(Entity $entity): Builder
{
return Reference::query()
->where('to_type', '=', $entity->getMorphClass())
->where('to_id', '=', $entity->id)
->where('from_type', '=', (new Page())->getMorphClass());
}
}

View File

@@ -112,12 +112,12 @@ class SearchIndex
*
* @returns array<string, int>
*/
protected function generateTermScoreMapFromText(string $text, int $scoreAdjustment = 1): array
protected function generateTermScoreMapFromText(string $text, float $scoreAdjustment = 1): array
{
$termMap = $this->textToTermCountMap($text);
foreach ($termMap as $term => $count) {
$termMap[$term] = $count * $scoreAdjustment;
$termMap[$term] = floor($count * $scoreAdjustment);
}
return $termMap;

View File

@@ -223,7 +223,7 @@ class SearchRunner
});
$subQuery->groupBy('entity_type', 'entity_id');
$entityQuery->joinSub($subQuery, 's', 'id', '=', 's.entity_id');
$entityQuery->joinSub($subQuery, 's', 'id', '=', 'entity_id');
$entityQuery->addSelect('s.score');
$entityQuery->orderBy('score', 'desc');
}

View File

@@ -0,0 +1,91 @@
<?php
namespace BookStack\Settings;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
class AppSettingsStore
{
protected ImageRepo $imageRepo;
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
}
public function storeFromUpdateRequest(Request $request, string $category)
{
$this->storeSimpleSettings($request);
if ($category === 'customization') {
$this->updateAppLogo($request);
$this->updateAppIcon($request);
}
}
protected function updateAppIcon(Request $request): void
{
$sizes = [180, 128, 64, 32];
// Update icon image if set
if ($request->hasFile('app_icon')) {
$iconFile = $request->file('app_icon');
$this->destroyExistingSettingImage('app-icon');
$image = $this->imageRepo->saveNew($iconFile, 'system', 0, 256, 256);
setting()->put('app-icon', $image->url);
foreach ($sizes as $size) {
$this->destroyExistingSettingImage('app-icon-' . $size);
$icon = $this->imageRepo->saveNew($iconFile, 'system', 0, $size, $size);
setting()->put('app-icon-' . $size, $icon->url);
}
}
// Clear icon image if requested
if ($request->get('app_icon_reset')) {
$this->destroyExistingSettingImage('app-icon');
setting()->remove('app-icon');
foreach ($sizes as $size) {
$this->destroyExistingSettingImage('app-icon-' . $size);
setting()->remove('app-icon-' . $size);
}
}
}
protected function updateAppLogo(Request $request): void
{
// Update logo image if set
if ($request->hasFile('app_logo')) {
$logoFile = $request->file('app_logo');
$this->destroyExistingSettingImage('app-logo');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
setting()->put('app-logo', $image->url);
}
// Clear logo image if requested
if ($request->get('app_logo_reset')) {
$this->destroyExistingSettingImage('app-logo');
setting()->remove('app-logo');
}
}
protected function storeSimpleSettings(Request $request): void
{
foreach ($request->all() as $name => $value) {
if (strpos($name, 'setting-') !== 0) {
continue;
}
$key = str_replace('setting-', '', trim($name));
setting()->put($key, $value);
}
}
protected function destroyExistingSettingImage(string $settingKey)
{
$existingVal = setting()->get($settingKey);
if ($existingVal) {
$this->imageRepo->destroyByUrlAndType($existingVal, 'system');
}
}
}

View File

@@ -12,15 +12,11 @@ use Illuminate\Contracts\Cache\Repository as Cache;
*/
class SettingService
{
protected $setting;
protected $cache;
protected $localCache = [];
protected Setting $setting;
protected Cache $cache;
protected array $localCache = [];
protected string $cachePrefix = 'setting-';
protected $cachePrefix = 'setting-';
/**
* SettingService constructor.
*/
public function __construct(Setting $setting, Cache $cache)
{
$this->setting = $setting;

View File

@@ -0,0 +1,19 @@
<?php
namespace BookStack\Translation;
use Illuminate\Translation\MessageSelector as BaseClass;
/**
* This is a customization of the default Laravel MessageSelector class to tweak pluralization,
* so that is uses just the first part of the locale string to provide support with
* non-standard locales such as "de_informal".
*/
class MessageSelector extends BaseClass
{
public function getPluralIndex($locale, $number)
{
$locale = explode('_', $locale)[0];
return parent::getPluralIndex($locale, $number);
}
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Uploads;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\PermissionApplicator;
use BookStack\Auth\User;
use BookStack\Entities\Models\Entity;
@@ -10,6 +11,7 @@ use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
@@ -56,6 +58,12 @@ class Attachment extends Model
return $this->belongsTo(Page::class, 'uploaded_to');
}
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')
->where('joint_permissions.entity_type', '=', 'page');
}
/**
* Get the url of this file.
*/

View File

@@ -2,10 +2,12 @@
namespace BookStack\Uploads;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* @property int $id
@@ -25,6 +27,12 @@ class Image extends Model
protected $fillable = ['name'];
protected $hidden = [];
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class, 'entity_id', 'uploaded_to')
->where('joint_permissions.entity_type', '=', 'page');
}
/**
* Get a thumbnail for this image.
*

View File

@@ -123,7 +123,10 @@ class ImageRepo
public function saveNew(UploadedFile $uploadFile, string $type, int $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true): Image
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
if ($type !== 'system') {
$this->loadThumbs($image);
}
return $image;
}
@@ -180,13 +183,17 @@ class ImageRepo
}
/**
* Destroy all images of a certain type.
* Destroy images that have a specific URL and type combination.
*
* @throws Exception
*/
public function destroyByType(string $imageType): void
public function destroyByUrlAndType(string $url, string $imageType): void
{
$images = Image::query()->where('type', '=', $imageType)->get();
$images = Image::query()
->where('url', '=', $url)
->where('type', '=', $imageType)
->get();
foreach ($images as $image) {
$this->destroyImage($image);
}

View File

@@ -126,7 +126,7 @@ class CspService
protected function getAllowedIframeHosts(): array
{
$hosts = config('app.iframe_hosts', '');
$hosts = config('app.iframe_hosts') ?? '';
return array_filter(explode(' ', $hosts));
}

View File

@@ -55,9 +55,8 @@ function hasAppAccess(): bool
}
/**
* Check if the current user has a permission.
* Checks a generic role permission or, if an ownable model is passed in, it will
* check against the given entity model, taking into account entity-level permissions.
* Check if the current user has a permission. If an ownable element
* is passed in the jointPermissions are checked against that particular item.
*/
function userCan(string $permission, Model $ownable = null): bool
{

665
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,9 @@
project_identifier: bookstack
base_path: .
preserve_hierarchy: false
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%

View File

@@ -21,7 +21,7 @@ class TagFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->city,
'name' => $this->faker->city(),
'value' => $this->faker->sentence(3),
];
}

View File

@@ -18,7 +18,7 @@ class WebhookFactory extends Factory
{
return [
'name' => 'My webhook for ' . $this->faker->country(),
'endpoint' => $this->faker->url,
'endpoint' => $this->faker->url(),
'active' => true,
'timeout' => 3,
];

View File

@@ -22,11 +22,11 @@ class UserFactory extends Factory
*/
public function definition()
{
$name = $this->faker->name;
$name = $this->faker->name();
return [
'name' => $name,
'email' => $this->faker->email,
'email' => $this->faker->email(),
'slug' => Str::slug($name . '-' . Str::random(5)),
'password' => Str::random(10),
'remember_token' => Str::random(10),

View File

@@ -22,9 +22,9 @@ class BookFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->sentence,
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
'description' => $this->faker->paragraph,
'description' => $this->faker->paragraph(),
];
}
}

View File

@@ -22,9 +22,9 @@ class ChapterFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->sentence,
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
'description' => $this->faker->paragraph,
'description' => $this->faker->paragraph(),
];
}
}

View File

@@ -24,7 +24,7 @@ class PageFactory extends Factory
$html = '<p>' . implode('</p>', $this->faker->paragraphs(5)) . '</p>';
return [
'name' => $this->faker->sentence,
'name' => $this->faker->sentence(),
'slug' => Str::random(10),
'html' => $html,
'text' => strip_tags($html),

View File

@@ -21,9 +21,9 @@ class ImageFactory extends Factory
public function definition()
{
return [
'name' => $this->faker->slug . '.jpg',
'url' => $this->faker->url,
'path' => $this->faker->url,
'name' => $this->faker->slug() . '.jpg',
'url' => $this->faker->url(),
'path' => $this->faker->url(),
'type' => 'gallery',
'uploaded_to' => 0,
];

View File

@@ -1,47 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AddUserIdToEntityPermissions extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('entity_permissions', function (Blueprint $table) {
$table->unsignedInteger('role_id')->nullable()->default(null)->change();
$table->unsignedInteger('user_id')->nullable()->default(null)->index();
});
DB::table('entity_permissions')
->where('role_id', '=', 0)
->update(['role_id' => null]);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('entity_permissions')
->whereNull('role_id')
->update(['role_id' => 0]);
DB::table('entity_permissions')
->whereNotNull('user_id')
->delete();
Schema::table('entity_permissions', function (Blueprint $table) {
$table->unsignedInteger('role_id')->nullable(false)->change();
$table->dropColumn('user_id');
});
}
}

View File

@@ -1,40 +0,0 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateCollapsedRolePermissionsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// TODO - Drop joint permissions
// TODO - Run collapsed table rebuild.
Schema::create('entity_permissions_collapsed', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('role_id')->nullable()->index();
$table->unsignedInteger('user_id')->nullable()->index();
$table->string('entity_type');
$table->unsignedInteger('entity_id');
$table->boolean('view')->index();
$table->index(['entity_type', 'entity_id']);
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('entity_permissions_collapsed');
}
}

View File

@@ -0,0 +1,52 @@
<?php
use BookStack\Auth\Permissions\JointPermissionBuilder;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class RefactorJointPermissionsStorage extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
// Truncate before schema changes to avoid performance issues
// since we'll need to rebuild anyway.
DB::table('joint_permissions')->truncate();
if (Schema::hasColumn('joint_permissions', 'owned_by')) {
Schema::table('joint_permissions', function (Blueprint $table) {
$table->dropColumn(['has_permission', 'has_permission_own', 'owned_by']);
$table->unsignedTinyInteger('status')->index();
$table->unsignedInteger('owner_id')->nullable()->index();
});
}
// Rebuild permissions
app(JointPermissionBuilder::class)->rebuildForAll();
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
DB::table('joint_permissions')->truncate();
Schema::table('joint_permissions', function (Blueprint $table) {
$table->dropColumn(['status', 'owner_id']);
$table->boolean('has_permission')->index();
$table->boolean('has_permission_own')->index();
$table->unsignedInteger('owned_by')->index();
});
}
}

View File

@@ -0,0 +1,69 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
class CopyColorSettingsForDarkMode extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$colorSettings = [
'app-color',
'app-color-light',
'bookshelf-color',
'book-color',
'chapter-color',
'page-color',
'page-draft-color',
];
$existing = DB::table('settings')
->whereIn('setting_key', $colorSettings)
->get()->toArray();
$newData = [];
foreach ($existing as $setting) {
$newSetting = (array) $setting;
$newSetting['setting_key'] .= '-dark';
$newData[] = $newSetting;
if ($newSetting['setting_key'] === 'app-color-dark') {
$newSetting['setting_key'] = 'link-color';
$newData[] = $newSetting;
$newSetting['setting_key'] = 'link-color-dark';
$newData[] = $newSetting;
}
}
DB::table('settings')->insert($newData);
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
$colorSettings = [
'app-color-dark',
'link-color',
'link-color-dark',
'app-color-light-dark',
'bookshelf-color-dark',
'book-color-dark',
'chapter-color-dark',
'page-color-dark',
'page-draft-color-dark',
];
DB::table('settings')
->whereIn('setting_key', $colorSettings)
->delete();
}
}

View File

@@ -3,7 +3,7 @@
namespace Database\Seeders;
use BookStack\Api\ApiToken;
use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Auth\Role;
use BookStack\Auth\User;
@@ -69,7 +69,7 @@ class DummyContentSeeder extends Seeder
]);
$token->save();
app(CollapsedPermissionBuilder::class)->rebuildForAll();
app(JointPermissionBuilder::class)->rebuildForAll();
app(SearchIndex::class)->indexAllEntities();
}
}

View File

@@ -2,7 +2,7 @@
namespace Database\Seeders;
use BookStack\Auth\Permissions\CollapsedPermissionBuilder;
use BookStack\Auth\Permissions\JointPermissionBuilder;
use BookStack\Auth\Role;
use BookStack\Auth\User;
use BookStack\Entities\Models\Book;
@@ -35,7 +35,7 @@ class LargeContentSeeder extends Seeder
$largeBook->chapters()->saveMany($chapters);
$all = array_merge([$largeBook], array_values($pages->all()), array_values($chapters->all()));
app()->make(CollapsedPermissionBuilder::class)->rebuildForEntity($largeBook);
app()->make(JointPermissionBuilder::class)->rebuildForEntity($largeBook);
app()->make(SearchIndex::class)->indexEntities($all);
}
}

View File

@@ -29,6 +29,8 @@ The testing database will also need migrating and seeding beforehand. This can b
Once done you can run `composer test` in the application root directory to run all tests. Tests can be ran in parallel by running them via `composer t`. This will use Laravel's built-in parallel testing functionality, and attempt to create and seed a database instance for each testing thread. If required these parallel testing instances can be reset, before testing again, by running `composer t-reset`.
If the codebase needs to be tested with deprecations, this can be done via uncommenting the relevant line within the TestCase@setUp function.
## Code Standards
PHP code standards are managed by [using PHP_CodeSniffer](https://github.com/squizlabs/PHP_CodeSniffer).

View File

@@ -6,19 +6,16 @@ Test cases are written ability abstract, since all abilities should act the same
Tests are categorised by the most specific element involved in the scenario, where the below list is most specific to least:
- User entity permissions.
- Role entity permissions.
- Fallback entity permissions.
- Role permissions.
- TODO - Test fallback in the context of the above.
## General Permission Logical Rules
The below are some general rules we follow to standardise the behaviour of permissions in the platform:
- Most specific permission application (as above) take priority and can deny less specific permissions.
- Parent user/role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role/user.
- Parent role entity permissions that may be inherited, are considered to essentially be applied on the item they are inherited to unless a lower level has its own permission rule for an already specific role.
- Where both grant and deny exist at the same specificity, we side towards grant.
## Cases
@@ -232,7 +229,17 @@ User denied page permission.
User denied page permission.
#### test_80_multi_role_inherited_deny_via_parent
#### test_71_multi_role_inheriting_deny_on_own
- Page permissions have inherit enabled.
- Role A has own page role permission.
- Role B has entity denied page permission.
- User has Role A and B.
- Use owns Page.
User denied page permission.
#### test_75_multi_role_inherited_deny_via_parent
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
@@ -242,180 +249,115 @@ User denied page permission.
User denied page permission.
---
#### test_76_multi_role_inherited_deny_via_parent_on_own
### Entity User Permissions
These are tests related to entity-level user-specific permission overrides.
#### test_01_explicit_allow
- Page permissions have inherit disabled.
- User has entity allow page permission.
User granted page permission.
#### test_02_explicit_deny
- Page permissions have inherit disabled.
- User has entity deny page permission.
- Page permissions have inherit enabled.
- Chapter permissions have inherit enabled.
- Role A has own page role permission.
- Role B has entity denied chapter permission.
- User has Role A & B.
User denied page permission.
#### test_10_allow_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
User granted page permission.
#### test_11_deny_inherit
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
User denied page permission.
#### test_12_allow_inherit_override
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
- User has entity allow page permission.
User granted page permission.
#### test_13_deny_inherit_override
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
- User has entity deny page permission.
User denied page permission.
#### test_40_entity_role_override_allow
#### test_80_fallback_override_allow
- Page permissions have inherit disabled.
- User has entity allow page permission.
- Role A has entity deny page permission.
- User has role A.
User granted page permission.
#### test_41_entity_role_override_deny
- Page permissions have inherit disabled.
- User has entity deny page permission.
- Page fallback has entity deny permission.
- Role A has entity allow page permission.
- User has role A.
- User has Role A.
User denied page permission.
User granted page permission.
#### test_42_entity_role_override_allow_via_inherit
#### test_81_fallback_override_deny
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity allow chapter permission.
- Page permissions have inherit disabled.
- Page fallback has entity allow permission.
- Role A has entity deny page permission.
- User has role A.
- User has Role A.
User granted page permission.
User denied page permission.
#### test_43_entity_role_override_deny_via_inherit
#### test_84_fallback_override_allow_multi_role
- Page permissions have inherit enabled.
- Chapter permissions have inherit disabled.
- User has entity deny chapter permission.
- Page permissions have inherit disabled.
- Page fallback has entity deny permission.
- Role A has entity allow page permission.
- User has role A.
User denied page permission.
#### test_50_role_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- User has entity allow page permission.
- User has Role A.
- Role B has no entity page permissions.
- User has Role A & B.
User granted page permission.
#### test_51_role_override_deny
#### test_85_fallback_override_deny_multi_role
- Page permissions have inherit enabled.
- Role A has all-page role permission.
- User has entity deny page permission.
- User has Role A.
User denied page permission.
#### test_60_inherited_role_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- User has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_61_inherited_role_override_deny
- Page permissions have inherit enabled.
- Role A has view-all page role permission.
- User has entity deny chapter permission.
- User has Role A.
User denied page permission.
#### test_61_inherited_role_override_deny_on_own
- Page permissions have inherit enabled.
- Role A has view-own page role permission.
- User has entity deny chapter permission.
- User has Role A.
- User owns Page.
User denied page permission.
#### test_70_all_override_allow
- Page permissions have inherit enabled.
- Role A has no page role permission.
- Page permissions have inherit disabled.
- Page fallback has entity allow permission.
- Role A has entity deny page permission.
- User has entity allow page permission.
- User has Role A.
User granted page permission.
#### test_71_all_override_deny
- Page permissions have inherit enabled.
- Role A has page-all role permission.
- Role A has entity allow page permission.
- User has entity deny page permission.
- User has Role A.
- Role B has no entity page permissions.
- User has Role A & B.
User denied page permission.
#### test_80_inherited_all_override_allow
#### test_86_fallback_override_allow_inherit
- Chapter permissions have inherit disabled.
- Page permissions have inherit enabled.
- Role A has no page role permission.
- Role A has entity deny chapter permission.
- User has entity allow chapter permission.
- User has Role A.
User granted page permission.
#### test_81_inherited_all_override_deny
- Page permissions have inherit enabled.
- Role A has view-all page role permission.
- Chapter fallback has entity deny permission.
- Role A has entity allow chapter permission.
- User has entity deny chapter permission.
- User has Role A.
User granted page permission.
#### test_87_fallback_override_deny_inherit
- Chapter permissions have inherit disabled.
- Page permissions have inherit enabled.
- Chapter fallback has entity allow permission.
- Role A has entity deny chapter permission.
- User has Role A.
User denied page permission.
#### test_88_fallback_override_allow_multi_role_inherit
- Chapter permissions have inherit disabled.
- Page permissions have inherit enabled.
- Chapter fallback has entity deny permission.
- Role A has entity allow chapter permission.
- Role B has no entity chapter permissions.
- User has Role A & B.
User granted page permission.
#### test_89_fallback_override_deny_multi_role_inherit
- Chapter permissions have inherit disabled.
- Page permissions have inherit enabled.
- Chapter fallback has entity allow permission.
- Role A has entity deny chapter permission.
- Role B has no entity chapter permissions.
- User has Role A & B.
User denied page permission.
#### test_90_fallback_overrides_parent_entity_role_deny
- Chapter permissions have inherit disabled.
- Page permissions have inherit disabled.
- Chapter fallback has entity deny permission.
- Page fallback has entity deny permission.
- Role A has entity allow chapter permission.
- User has Role A.
User denied page permission.
#### test_91_fallback_overrides_parent_entity_role_inherit
- Book permissions have inherit disabled.
- Chapter permissions have inherit disabled.
- Page permissions have inherit enabled.
- Book fallback has entity deny permission.
- Chapter fallback has entity deny permission.
- Role A has entity allow book permission.
- User has Role A.
User denied page permission.

1076
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,11 +16,11 @@
},
"devDependencies": {
"chokidar-cli": "^3.0",
"esbuild": "^0.15.12",
"esbuild": "^0.17.3",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.55.0"
"punycode": "^2.3.0",
"sass": "^1.57.0"
},
"dependencies": {
"clipboard": "^2.0.11",
@@ -28,7 +28,7 @@
"dropzone": "^5.9.3",
"markdown-it": "^13.0.1",
"markdown-it-task-lists": "^2.1.1",
"snabbdom": "^3.5.0",
"snabbdom": "^3.5.1",
"sortablejs": "^1.15.0"
}
}

69
public/dist/app.js vendored Normal file

File diff suppressed because one or more lines are too long

38
public/dist/code.js vendored Normal file

File diff suppressed because one or more lines are too long

1
public/dist/export-styles.css vendored Normal file

File diff suppressed because one or more lines are too long

1
public/dist/print-styles.css vendored Normal file
View File

@@ -0,0 +1 @@
:root{--color-primary: #206ea7;--color-primary-light: rgba(32,110,167,0.15);--color-link: #206ea7;--color-page: #206ea7;--color-page-draft: #7e50b1;--color-chapter: #af4d0d;--color-book: #077b70;--color-bookshelf: #a94747;--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(0, 0, 0,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E")}:root.dark-mode{--bg-disabled: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' height='100%25' width='100%25'%3E%3Cdefs%3E%3Cpattern id='doodad' width='19' height='19' viewBox='0 0 40 40' patternUnits='userSpaceOnUse' patternTransform='rotate(143)'%3E%3Crect width='100%25' height='100%25' fill='rgba(42, 67, 101,0)'/%3E%3Cpath d='M-10 30h60v20h-60zM-10-10h60v20h-60' fill='rgba(26, 32, 44,0)'/%3E%3Cpath d='M-10 10h60v20h-60zM-10-30h60v20h-60z' fill='rgba(255, 255, 255,0.05)'/%3E%3C/pattern%3E%3C/defs%3E%3Crect fill='url(%23doodad)' height='200%25' width='200%25'/%3E%3C/svg%3E");color-scheme:only dark}:root:not(.dark-mode){color-scheme:only light}header{display:none}html,body{font-size:12px;background-color:#fff}.page-content{margin:0 auto}.print-hidden{display:none !important}.tri-layout-container{grid-template-columns:1fr;grid-template-areas:"b";margin-inline-start:0;margin-inline-end:0;display:block}.card{box-shadow:none}.content-wrap.card{padding-inline-start:0;padding-inline-end:0}/*# sourceMappingURL=print-styles.css.map */

1
public/dist/styles.css vendored Normal file

File diff suppressed because one or more lines are too long

BIN
public/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

BIN
public/icon-180.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

BIN
public/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
public/icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

BIN
public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -45,7 +45,7 @@ Note: Listed services are not tested, vetted nor supported by the official BookS
<img width="400" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/diagramsnet.png" alt="Diagrams.net">
</a></td>
<td><a href="https://cloudabove.com/hosting" target="_blank">
<img height="100" src="https://raw.githubusercontent.com/BookStackApp/website/main/static/images/sponsors/cloudabove.svg" alt="Cloudabove">
<img height="100" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/cloudabove.png" alt="Cloudabove">
</a></td>
</tr></tbody></table>
@@ -55,6 +55,9 @@ Note: Listed services are not tested, vetted nor supported by the official BookS
<td><a href="https://www.stellarhosted.com/bookstack/" target="_blank">
<img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/stellarhosted.png" alt="Stellar Hosted">
</a></td>
<td><a href="https://www.practicali.be" target="_blank">
<img width="280" src="https://media.githubusercontent.com/media/BookStackApp/website/main/static/images/sponsors/practicali.png" alt="Stellar Hosted">
</a></td>
</tr></tbody></table>
## 🛣️ Road Map

View File

@@ -25,11 +25,14 @@ import 'codemirror/mode/properties/properties';
import 'codemirror/mode/python/python';
import 'codemirror/mode/ruby/ruby';
import 'codemirror/mode/rust/rust';
import 'codemirror/mode/scheme/scheme';
import 'codemirror/mode/shell/shell';
import 'codemirror/mode/smarty/smarty';
import 'codemirror/mode/sql/sql';
import 'codemirror/mode/stex/stex';
import 'codemirror/mode/swift/swift';
import 'codemirror/mode/toml/toml';
import 'codemirror/mode/twig/twig';
import 'codemirror/mode/vb/vb';
import 'codemirror/mode/vbscript/vbscript';
import 'codemirror/mode/xml/xml';
@@ -74,6 +77,8 @@ const modeMap = {
mdown: 'markdown',
markdown: 'markdown',
ml: 'mllike',
mssql: 'text/x-mssql',
mysql: 'text/x-mysql',
nginx: 'nginx',
octave: 'text/x-octave',
perl: 'perl',
@@ -86,19 +91,26 @@ const modeMap = {
php: (content) => {
return content.includes('<?php') ? 'php' : 'text/x-php';
},
pgsql: 'text/x-pgsql',
'pl/sql': 'text/x-plsql',
postgresql: 'text/x-pgsql',
py: 'python',
python: 'python',
ruby: 'ruby',
rust: 'rust',
rb: 'ruby',
rs: 'rust',
scheme: 'scheme',
shell: 'shell',
sh: 'shell',
smarty: 'smarty',
sql: 'text/x-sql',
sqlite: 'text/x-sqlite',
stext: 'text/x-stex',
swift: 'text/x-swift',
toml: 'toml',
ts: 'text/typescript',
twig: 'twig',
typescript: 'text/typescript',
vbs: 'vbscript',
vbscript: 'vbscript',

View File

@@ -45,7 +45,7 @@ export class Attachments extends Component {
this.stopEdit();
/** @var {Tabs} */
const tabs = window.$components.firstOnElement(this.mainTabs, 'tabs');
tabs.show('items');
tabs.show('attachment-panel-items');
window.$http.get(`/attachments/get/page/${this.pageId}`).then(resp => {
this.list.innerHTML = resp.data;
window.$components.init(this.list);

View File

@@ -1,4 +1,4 @@
import Sortable from "sortablejs";
import Sortable, {MultiDrag} from "sortablejs";
import {Component} from "./component";
import {htmlToDom} from "../services/dom";
@@ -37,6 +37,113 @@ const sortOperations = {
},
};
/**
* The available move actions.
* The active function indicates if the action is possible for the given item.
* The run function performs the move.
* @type {{up: {active(Element, ?Element, Element): boolean, run(Element, ?Element, Element)}}}
*/
const moveActions = {
up: {
active(elem, parent, book) {
return !(elem.previousElementSibling === null && !parent);
},
run(elem, parent, book) {
const newSibling = elem.previousElementSibling || parent;
newSibling.insertAdjacentElement('beforebegin', elem);
}
},
down: {
active(elem, parent, book) {
return !(elem.nextElementSibling === null && !parent);
},
run(elem, parent, book) {
const newSibling = elem.nextElementSibling || parent;
newSibling.insertAdjacentElement('afterend', elem);
}
},
next_book: {
active(elem, parent, book) {
return book.nextElementSibling !== null;
},
run(elem, parent, book) {
const newList = book.nextElementSibling.querySelector('ul');
newList.prepend(elem);
}
},
prev_book: {
active(elem, parent, book) {
return book.previousElementSibling !== null;
},
run(elem, parent, book) {
const newList = book.previousElementSibling.querySelector('ul');
newList.appendChild(elem);
}
},
next_chapter: {
active(elem, parent, book) {
return elem.dataset.type === 'page' && this.getNextChapter(elem, parent);
},
run(elem, parent, book) {
const nextChapter = this.getNextChapter(elem, parent);
nextChapter.querySelector('ul').prepend(elem);
},
getNextChapter(elem, parent) {
const topLevel = (parent || elem);
const topItems = Array.from(topLevel.parentElement.children);
const index = topItems.indexOf(topLevel);
return topItems.slice(index + 1).find(elem => elem.dataset.type === 'chapter');
}
},
prev_chapter: {
active(elem, parent, book) {
return elem.dataset.type === 'page' && this.getPrevChapter(elem, parent);
},
run(elem, parent, book) {
const prevChapter = this.getPrevChapter(elem, parent);
prevChapter.querySelector('ul').append(elem);
},
getPrevChapter(elem, parent) {
const topLevel = (parent || elem);
const topItems = Array.from(topLevel.parentElement.children);
const index = topItems.indexOf(topLevel);
return topItems.slice(0, index).reverse().find(elem => elem.dataset.type === 'chapter');
}
},
book_end: {
active(elem, parent, book) {
return parent || (parent === null && elem.nextElementSibling);
},
run(elem, parent, book) {
book.querySelector('ul').append(elem);
}
},
book_start: {
active(elem, parent, book) {
return parent || (parent === null && elem.previousElementSibling);
},
run(elem, parent, book) {
book.querySelector('ul').prepend(elem);
}
},
before_chapter: {
active(elem, parent, book) {
return parent;
},
run(elem, parent, book) {
parent.insertAdjacentElement('beforebegin', elem);
}
},
after_chapter: {
active(elem, parent, book) {
return parent;
},
run(elem, parent, book) {
parent.insertAdjacentElement('afterend', elem);
}
},
};
export class BookSort extends Component {
setup() {
@@ -44,15 +151,34 @@ export class BookSort extends Component {
this.sortContainer = this.$refs.sortContainer;
this.input = this.$refs.input;
Sortable.mount(new MultiDrag());
const initialSortBox = this.container.querySelector('.sort-box');
this.setupBookSortable(initialSortBox);
this.setupSortPresets();
this.setupMoveActions();
window.$events.listen('entity-select-confirm', this.bookSelect.bind(this));
window.$events.listen('entity-select-change', this.bookSelect.bind(this));
}
/**
* Setup the handlers for the preset sort type buttons.
* Set up the handlers for the item-level move buttons.
*/
setupMoveActions() {
// Handle move button click
this.container.addEventListener('click', event => {
if (event.target.matches('[data-move]')) {
const action = event.target.getAttribute('data-move');
const sortItem = event.target.closest('[data-id]');
this.runSortAction(sortItem, action);
}
});
this.updateMoveActionStateForAll();
}
/**
* Set up the handlers for the preset sort type buttons.
*/
setupSortPresets() {
let lastSort = '';
@@ -100,16 +226,19 @@ export class BookSort extends Component {
const newBookContainer = htmlToDom(resp.data);
this.sortContainer.append(newBookContainer);
this.setupBookSortable(newBookContainer);
this.updateMoveActionStateForAll();
const summary = newBookContainer.querySelector('summary');
summary.focus();
});
}
/**
* Setup the given book container element to have sortable items.
* Set up the given book container element to have sortable items.
* @param {Element} bookContainer
*/
setupBookSortable(bookContainer) {
const sortElems = [bookContainer.querySelector('.sort-list')];
sortElems.push(...bookContainer.querySelectorAll('.entity-list-item + ul'));
const sortElems = Array.from(bookContainer.querySelectorAll('.sort-list, .sortable-page-sublist'));
const bookGroupConfig = {
name: 'book',
@@ -125,22 +254,40 @@ export class BookSort extends Component {
}
};
for (let sortElem of sortElems) {
new Sortable(sortElem, {
for (const sortElem of sortElems) {
Sortable.create(sortElem, {
group: sortElem.classList.contains('sort-list') ? bookGroupConfig : chapterGroupConfig,
animation: 150,
fallbackOnBody: true,
swapThreshold: 0.65,
onSort: this.updateMapInput.bind(this),
onSort: (event) => {
this.ensureNoNestedChapters()
this.updateMapInput();
this.updateMoveActionStateForAll();
},
dragClass: 'bg-white',
ghostClass: 'primary-background-light',
multiDrag: true,
multiDragKey: 'CTRL',
multiDragKey: 'Control',
selectedClass: 'sortable-selected',
});
}
}
/**
* Handle nested chapters by moving them to the parent book.
* Needed since sorting with multi-sort only checks group rules based on the active item,
* not all in group, therefore need to manually check after a sort.
* Must be done before updating the map input.
*/
ensureNoNestedChapters() {
const nestedChapters = this.container.querySelectorAll('[data-type="chapter"] [data-type="chapter"]');
for (const chapter of nestedChapters) {
const parentChapter = chapter.parentElement.closest('[data-type="chapter"]');
parentChapter.insertAdjacentElement('afterend', chapter);
}
}
/**
* Update the input with our sort data.
*/
@@ -202,4 +349,38 @@ export class BookSort extends Component {
}
}
/**
* Run the given sort action up the provided sort item.
* @param {Element} item
* @param {String} action
*/
runSortAction(item, action) {
const parentItem = item.parentElement.closest('li[data-id]');
const parentBook = item.parentElement.closest('[data-type="book"]');
moveActions[action].run(item, parentItem, parentBook);
this.updateMapInput();
this.updateMoveActionStateForAll();
item.scrollIntoView({behavior: 'smooth', block: 'nearest'});
item.focus();
}
/**
* Update the state of the available move actions on this item.
* @param {Element} item
*/
updateMoveActionState(item) {
const parentItem = item.parentElement.closest('li[data-id]');
const parentBook = item.parentElement.closest('[data-type="book"]');
for (const [action, functions] of Object.entries(moveActions)) {
const moveButton = item.querySelector(`[data-move="${action}"]`);
moveButton.disabled = !functions.active(item, parentItem, parentBook);
}
}
updateMoveActionStateForAll() {
const items = this.container.querySelectorAll('[data-type="chapter"],[data-type="page"]');
for (const item of items) {
this.updateMoveActionState(item);
}
}
}

View File

@@ -10,8 +10,6 @@ export class EntityPermissions extends Component {
this.everyoneInheritToggle = this.$refs.everyoneInherit;
this.roleSelect = this.$refs.roleSelect;
this.roleContainer = this.$refs.roleContainer;
this.userContainer = this.$refs.userContainer;
this.userSelectContainer = this.$refs.userSelectContainer;
this.setupListeners();
}
@@ -20,7 +18,7 @@ export class EntityPermissions extends Component {
// "Everyone Else" inherit toggle
this.everyoneInheritToggle.addEventListener('change', event => {
const inherit = event.target.checked;
const permissions = document.querySelectorAll('input[name^="permissions[fallback]"]');
const permissions = document.querySelectorAll('input[name^="permissions[0]["]');
for (const permission of permissions) {
permission.disabled = inherit;
permission.checked = false;
@@ -30,7 +28,7 @@ export class EntityPermissions extends Component {
// Remove role row button click
this.container.addEventListener('click', event => {
const button = event.target.closest('button');
if (button && button.dataset.modelType) {
if (button && button.dataset.roleId) {
this.removeRowOnButtonClick(button)
}
});
@@ -42,14 +40,6 @@ export class EntityPermissions extends Component {
this.addRoleRow(roleId);
}
});
// User select change
this.userSelectContainer.querySelector('input[name="user_select"]').addEventListener('change', event => {
const userId = event.target.value;
if (userId) {
this.addUserRow(userId);
}
});
}
async addRoleRow(roleId) {
@@ -62,50 +52,23 @@ export class EntityPermissions extends Component {
}
// Get and insert new row
const resp = await window.$http.get(`/permissions/role-form-row/${this.entityType}/${roleId}`);
const resp = await window.$http.get(`/permissions/form-row/${this.entityType}/${roleId}`);
const row = htmlToDom(resp.data);
this.roleContainer.append(row);
this.roleSelect.disabled = false;
}
async addUserRow(userId) {
const exists = this.userContainer.querySelector(`[name^="permissions[user][${userId}]"]`) !== null;
if (exists) {
return;
}
const toggle = this.userSelectContainer.querySelector('.dropdown-search-toggle-select');
toggle.classList.add('disabled');
this.userContainer.style.pointerEvents = 'none';
// Get and insert new row
const resp = await window.$http.get(`/permissions/user-form-row/${this.entityType}/${userId}`);
const row = htmlToDom(resp.data);
this.userContainer.append(row);
toggle.classList.remove('disabled');
this.userContainer.style.pointerEvents = null;
/** @var {UserSelect} **/
const userSelect = window.$components.firstOnElement(this.userSelectContainer.querySelector('.dropdown-search'), 'user-select');
userSelect.reset();
}
removeRowOnButtonClick(button) {
const row = button.closest('.item-list-row');
const modelId = button.dataset.modelId;
const modelName = button.dataset.modelName;
const modelType = button.dataset.modelType;
const roleId = button.dataset.roleId;
const roleName = button.dataset.roleName;
const option = document.createElement('option');
option.value = modelId;
option.textContent = modelName;
if (modelType === 'role') {
this.roleSelect.append(option);
}
option.value = roleId;
option.textContent = roleName;
this.roleSelect.append(option);
row.remove();
}

View File

@@ -15,7 +15,6 @@ export class EntitySelector extends Component {
this.searchInput = this.$refs.search;
this.loading = this.$refs.loading;
this.resultsContainer = this.$refs.results;
this.addButton = this.$refs.add;
this.search = '';
this.lastClick = 0;
@@ -43,15 +42,6 @@ export class EntitySelector extends Component {
if (event.keyCode === 13) event.preventDefault();
});
if (this.addButton) {
this.addButton.addEventListener('click', event => {
if (this.selectedItemData) {
this.confirmSelection(this.selectedItemData);
this.unselectAll();
}
});
}
// Keyboard navigation
onChildEvent(this.$el, '[data-entity-type]', 'keydown', (e, el) => {
if (e.ctrlKey && e.code === 'Enter') {

View File

@@ -140,10 +140,9 @@ export class ImageManager extends Component {
}
setActiveFilterTab(filterName) {
this.filterTabs.forEach(t => t.classList.remove('selected'));
const activeTab = this.filterTabs.find(t => t.dataset.filter === filterName);
if (activeTab) {
activeTab.classList.add('selected');
for (const tab of this.filterTabs) {
const selected = tab.dataset.filter === filterName;
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
}
}

View File

@@ -41,7 +41,7 @@ export {PagePicker} from "./page-picker.js"
export {PermissionsTable} from "./permissions-table.js"
export {Pointer} from "./pointer.js"
export {Popup} from "./popup.js"
export {SettingAppColorPicker} from "./setting-app-color-picker.js"
export {SettingAppColorScheme} from "./setting-app-color-scheme.js"
export {SettingColorPicker} from "./setting-color-picker.js"
export {SettingHomepageControl} from "./setting-homepage-control.js"
export {ShelfSort} from "./shelf-sort.js"

View File

@@ -1,49 +0,0 @@
import {Component} from "./component";
export class SettingAppColorPicker extends Component {
setup() {
this.colorInput = this.$refs.input;
this.lightColorInput = this.$refs.lightInput;
this.colorInput.addEventListener('change', this.updateColor.bind(this));
this.colorInput.addEventListener('input', this.updateColor.bind(this));
}
/**
* Update the app colors as a preview, and create a light version of the color.
*/
updateColor() {
const hexVal = this.colorInput.value;
const rgb = this.hexToRgb(hexVal);
const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
this.lightColorInput.value = rgbLightVal;
const customStyles = document.getElementById('custom-styles');
const oldColor = customStyles.getAttribute('data-color');
const oldColorLight = customStyles.getAttribute('data-color-light');
customStyles.innerHTML = customStyles.innerHTML.split(oldColor).join(hexVal);
customStyles.innerHTML = customStyles.innerHTML.split(oldColorLight).join(rgbLightVal);
customStyles.setAttribute('data-color', hexVal);
customStyles.setAttribute('data-color-light', rgbLightVal);
}
/**
* Covert a hex color code to rgb components.
* @attribution https://stackoverflow.com/a/5624139
* @param {String} hex
* @returns {{r: Number, g: Number, b: Number}}
*/
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return {
r: result ? parseInt(result[1], 16) : 0,
g: result ? parseInt(result[2], 16) : 0,
b: result ? parseInt(result[3], 16) : 0
};
}
}

View File

@@ -0,0 +1,82 @@
import {Component} from "./component";
export class SettingAppColorScheme extends Component {
setup() {
this.container = this.$el;
this.mode = this.$opts.mode;
this.lightContainer = this.$refs.lightContainer;
this.darkContainer = this.$refs.darkContainer;
this.container.addEventListener('tabs-change', event => {
const panel = event.detail.showing;
const newMode = (panel === 'color-scheme-panel-light') ? 'light' : 'dark';
this.handleModeChange(newMode);
});
const onInputChange = (event) => {
this.updateAppColorsFromInputs();
if (event.target.name.startsWith('setting-app-color')) {
this.updateLightForInput(event.target);
}
};
this.container.addEventListener('change', onInputChange);
this.container.addEventListener('input', onInputChange);
}
handleModeChange(newMode) {
this.mode = newMode;
const isDark = (newMode === 'dark');
document.documentElement.classList.toggle('dark-mode', isDark);
this.updateAppColorsFromInputs();
}
updateAppColorsFromInputs() {
const inputContainer = this.mode === 'dark' ? this.darkContainer : this.lightContainer;
const inputs = inputContainer.querySelectorAll('input[type="color"]');
for (const input of inputs) {
const splitName = input.name.split('-');
const colorPos = splitName.indexOf('color');
let cssId = splitName.slice(1, colorPos).join('-');
if (cssId === 'app') {
cssId = 'primary';
}
const varName = '--color-' + cssId;
document.body.style.setProperty(varName, input.value);
}
}
/**
* Update the 'light' app color variant for the given input.
* @param {HTMLInputElement} input
*/
updateLightForInput(input) {
const lightName = input.name.replace('-color', '-color-light');
const hexVal = input.value;
const rgb = this.hexToRgb(hexVal);
const rgbLightVal = 'rgba('+ [rgb.r, rgb.g, rgb.b, '0.15'].join(',') +')';
console.log(input.name, lightName, hexVal, rgbLightVal)
const lightColorInput = this.container.querySelector(`input[name="${lightName}"][type="hidden"]`);
lightColorInput.value = rgbLightVal;
}
/**
* Covert a hex color code to rgb components.
* @attribution https://stackoverflow.com/a/5624139
* @param {String} hex
* @returns {{r: Number, g: Number, b: Number}}
*/
hexToRgb(hex) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return {
r: result ? parseInt(result[1], 16) : 0,
g: result ? parseInt(result[2], 16) : 0,
b: result ? parseInt(result[3], 16) : 0
};
}
}

View File

@@ -15,6 +15,6 @@ export class SettingColorPicker extends Component {
setValue(value) {
this.colorInput.value = value;
this.colorInput.dispatchEvent(new Event('change'));
this.colorInput.dispatchEvent(new Event('change', {bubbles: true}));
}
}

View File

@@ -1,49 +1,49 @@
import {onSelect} from "../services/dom";
import {Component} from "./component";
/**
* Tabs
* Works by matching 'tabToggle<Key>' with 'tabContent<Key>' sections.
* Uses accessible attributes to drive its functionality.
* On tab wrapping element:
* - role=tablist
* On tabs (Should be a button):
* - id
* - role=tab
* - aria-selected=true/false
* - aria-controls=<id-of-panel-section>
* On panels:
* - id
* - tabindex=0
* - role=tabpanel
* - aria-labelledby=<id-of-tab-for-panel>
* - hidden (If not shown by default).
*/
export class Tabs extends Component {
setup() {
this.tabContentsByName = {};
this.tabButtonsByName = {};
this.allContents = [];
this.allButtons = [];
this.container = this.$el;
this.tabs = Array.from(this.container.querySelectorAll('[role="tab"]'));
this.panels = Array.from(this.container.querySelectorAll('[role="tabpanel"]'));
for (const [key, elems] of Object.entries(this.$manyRefs || {})) {
if (key.startsWith('toggle')) {
const cleanKey = key.replace('toggle', '').toLowerCase();
onSelect(elems, e => this.show(cleanKey));
this.allButtons.push(...elems);
this.tabButtonsByName[cleanKey] = elems;
this.container.addEventListener('click', event => {
const button = event.target.closest('[role="tab"]');
if (button) {
this.show(button.getAttribute('aria-controls'));
}
if (key.startsWith('content')) {
const cleanKey = key.replace('content', '').toLowerCase();
this.tabContentsByName[cleanKey] = elems;
this.allContents.push(...elems);
}
}
});
}
show(key) {
this.allContents.forEach(c => {
c.classList.add('hidden');
c.classList.remove('selected');
});
this.allButtons.forEach(b => b.classList.remove('selected'));
const contents = this.tabContentsByName[key] || [];
const buttons = this.tabButtonsByName[key] || [];
if (contents.length > 0) {
contents.forEach(c => {
c.classList.remove('hidden')
c.classList.add('selected')
});
buttons.forEach(b => b.classList.add('selected'));
show(sectionId) {
for (const panel of this.panels) {
panel.toggleAttribute('hidden', panel.id !== sectionId);
}
for (const tab of this.tabs) {
const tabSection = tab.getAttribute('aria-controls');
const selected = tabSection === sectionId;
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
}
this.$emit('change', {showing: sectionId});
}
}

View File

@@ -10,10 +10,11 @@ export class TagManager extends Component {
}
setupListeners() {
this.container.addEventListener('change', event => {
this.container.addEventListener('input', event => {
/** @var {AddRemoveRows} **/
const addRemoveComponent = window.$components.firstOnElement(this.addRemoveComponentEl, 'add-remove-rows');
if (!this.hasEmptyRows()) {
if (!this.hasEmptyRows() && event.target.value) {
addRemoveComponent.add();
}
});

View File

@@ -8,9 +8,6 @@ export class UserSelect extends Component {
this.input = this.$refs.input;
this.userInfoContainer = this.$refs.userInfo;
this.initialValue = this.input.value;
this.initialContent = this.userInfoContainer.innerHTML;
onChildEvent(this.container, 'a.dropdown-search-item', 'click', this.selectUser.bind(this));
}
@@ -22,13 +19,6 @@ export class UserSelect extends Component {
this.hide();
}
reset() {
this.input.value = this.initialValue;
this.userInfoContainer.innerHTML = this.initialContent;
this.input.dispatchEvent(new Event('change', {bubbles: true}));
this.hide();
}
hide() {
/** @var {Dropdown} **/
const dropdown = window.$components.firstOnElement(this.container, 'dropdown');

View File

@@ -95,8 +95,16 @@ async function upload(imageData, pageUploadedToId) {
* @returns {Promise<string>}
*/
async function load(drawingId) {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
try {
const resp = await window.$http.get(window.baseUrl(`/images/drawio/base64/${drawingId}`));
return `data:image/png;base64,${resp.data.content}`;
} catch (error) {
if (error instanceof window.$http.HttpError) {
window.$events.showResponseError(error);
}
close();
throw error;
}
}
export default {show, close, upload, load};

View File

@@ -43,10 +43,8 @@ function emitPublic(targetElement, eventName, eventData) {
}
/**
* Notify of a http error.
* Check for standard scenarios such as validation errors and
* formats an error notification accordingly.
* @param {Error} error
* Notify of standard server-provided validation errors.
* @param {Object} error
*/
function showValidationErrors(error) {
if (!error.status) return;
@@ -56,6 +54,17 @@ function showValidationErrors(error) {
}
}
/**
* Notify standard server-provided error messages.
* @param {Object} error
*/
function showResponseError(error) {
if (!error.status) return;
if (error.status >= 400 && error.data && error.data.message) {
emit('error', error.data.message);
}
}
export default {
emit,
emitPublic,
@@ -63,4 +72,5 @@ export default {
success: (msg) => emit('success', msg),
error: (msg) => emit('error', msg),
showValidationErrors,
showResponseError,
}

View File

@@ -132,7 +132,7 @@ async function request(url, options = {}) {
};
if (!response.ok) {
throw returnData;
throw new HttpError(response, content);
}
return returnData;
@@ -159,10 +159,24 @@ async function getResponseContent(response) {
return await response.text();
}
class HttpError extends Error {
constructor(response, content) {
super(response.statusText);
this.data = content;
this.headers = response.headers;
this.redirected = response.redirected;
this.status = response.status;
this.statusText = response.statusText;
this.url = response.url;
this.original = response;
}
}
export default {
get: get,
post: post,
put: put,
patch: patch,
delete: performDelete,
HttpError: HttpError,
};

View File

@@ -57,6 +57,12 @@ export class KeyboardNavigationHandler {
* @param {KeyboardEvent} event
*/
#keydownHandler(event) {
// Ignore certain key events in inputs to allow text editing.
if (event.target.matches('input') && (event.key === 'ArrowRight' || event.key === 'ArrowLeft')) {
return;
}
if (event.key === 'ArrowDown' || event.key === 'ArrowRight') {
this.focusNext();
event.preventDefault();
@@ -80,7 +86,7 @@ export class KeyboardNavigationHandler {
*/
#getFocusable() {
const focusable = [];
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"]),input:not([type=hidden])';
const selector = '[tabindex]:not([tabindex="-1"]),[href],button:not([tabindex="-1"],[disabled]),input:not([type=hidden])';
for (const container of this.containers) {
focusable.push(...container.querySelectorAll(selector))
}

View File

@@ -34,7 +34,7 @@ export function scrollAndHighlightElement(element) {
if (!element) return;
element.scrollIntoView({behavior: 'smooth'});
const color = document.getElementById('custom-styles').getAttribute('data-color-light');
const color = getComputedStyle(document.body).getPropertyValue('--color-primary-light');
const initColor = window.getComputedStyle(element).getPropertyValue('background-color');
element.style.backgroundColor = color;
setTimeout(() => {

View File

@@ -89,7 +89,7 @@ function drawingInit() {
return Promise.resolve('');
}
let drawingId = currentNode.getAttribute('drawio-diagram');
const drawingId = currentNode.getAttribute('drawio-diagram');
return DrawIO.load(drawingId);
}

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