Compare commits

...

153 Commits

Author SHA1 Message Date
Dan Brown
92d15d9cf2 Updated version and assets for release v25.07 2025-07-30 09:46:37 +01:00
Dan Brown
b06147fef7 Merge branch 'development' into release 2025-07-30 09:45:40 +01:00
Dan Brown
776ec7b9e7 Updated translations with latest Crowdin changes (#5696) 2025-07-30 09:36:34 +01:00
Dan Brown
8aa6bdc8ab Updated translator & dependency attribution before release v25.07 2025-07-30 09:27:17 +01:00
Dan Brown
4ab17157b1 API: Added ZIP export endpoint comments 2025-07-30 09:13:58 +01:00
Dan Brown
6d7ffab115 Deps: Updated PHP composer dependancy versions, fixed test namespaces 2025-07-27 11:24:54 +01:00
Dan Brown
c8cfec96dc Merge pull request #5731 from BookStackApp/lexical_jul25
New WYSIWYG editor changes for July 2025
2025-07-26 10:08:44 +01:00
Dan Brown
d145efb6f6 Lexical: Updated tests after link changes 2025-07-25 14:25:02 +01:00
Dan Brown
c54101c603 Lexical: Updated URL handling, added mouse handling
- Removed URL protocol allow-list to allow any as per old editor.
- Added mouse handling, so that clicks below many last hard-to-escape
  block types will add an empty new paragraph for easy escaping &
  editing.
2025-07-25 13:58:48 +01:00
Dan Brown
865e5aecc9 Lexical: Source code input changes
- Increased default source code view size.
- Updated HTML generation to output each top-level block on its own
  line.
2025-07-24 17:24:59 +01:00
Dan Brown
ae4d1d804a Lexical: Table cell bg and format setting fixes
- Updated table cell background color setting to be stable by
  specifically using the background property over the general styles.
- Updated format shorcuts to be correct header levels as per old editor
  and format menu.
- Updated format changes to properly update UI afterwards.
2025-07-24 16:51:11 +01:00
Dan Brown
5fc19b0edf Lexical: Fixed highlight format action, changed label 2025-07-24 13:48:00 +01:00
Dan Brown
0a73b70b64 Merge pull request #5725 from BookStackApp/md_plaintext
MarkDown Editor: TypeScript Conversion & Plaintext Editor
2025-07-23 15:48:10 +01:00
Dan Brown
2668aae09b TypeScript: Updated compile target, addressed issues 2025-07-23 15:41:55 +01:00
Dan Brown
3b9c0b34ae MD Editor: Fixed plaintext dark styles, updated npm packages 2025-07-23 14:59:26 +01:00
Dan Brown
53f32849a9 MD Editor: Last tests/check over plaintext use/switching 2025-07-23 14:49:41 +01:00
Dan Brown
7ca8bdc231 MD Editor: Added custom textarea undo/redo, updated positioning methods 2025-07-23 12:17:36 +01:00
Dan Brown
6621d55f3d MD Editor: Worked to improve/fix positioning code
Still pending testing. Old logic did not work when lines would wrap, so
changing things to a character/line measuring technique.
Fixed some other isues too while testing shortcuts.
2025-07-22 16:42:47 +01:00
Dan Brown
d55db06c01 MD Editor: Added plaintext/cm switching
Also aligned the construction of the inputs where possible.
2025-07-22 10:34:29 +01:00
Dan Brown
6b4b500a33 MD Editor: Added plaintext input implementation 2025-07-21 18:53:22 +01:00
Dan Brown
5ffec2c52d MD Editor: Updated actions to use input interface 2025-07-21 14:24:51 +01:00
Dan Brown
ec07793cda MD Editor: Started work on input interface
Created implementation for codemirror, yet to use it.
2025-07-21 11:49:58 +01:00
Dan Brown
61adc735c8 MD Editor: Finished conversion to Typescript 2025-07-20 15:05:19 +01:00
Dan Brown
7bbf591a7f MD Editor: Starting conversion to typescript 2025-07-20 12:33:22 +01:00
Dan Brown
61f8d18af5 Changelog: Tweaked spacing, count and element referencing
During review of #5663
2025-07-19 14:53:02 +01:00
Dan Brown
f786d25f2e Merge branch 'enhance-changelog-textarea' of github.com:shresthkapoor7/BookStack into shresthkapoor7-enhance-changelog-textarea 2025-07-19 14:39:57 +01:00
Dan Brown
e62f4426ea Merge pull request #5721 from BookStackApp/zip_export_api_endpoints
API: ZIP Import/Export
2025-07-18 16:34:10 +01:00
Dan Brown
32ba3a591f ZIP Imports: Added API examples, finished testing
Also updated some types on a couple of controllers.
2025-07-18 16:19:14 +01:00
Dan Brown
73025719a4 ZIP Imports: Added API test cases 2025-07-18 14:05:32 +01:00
Dan Brown
d55684531f API: Added zip export tests, reorganised tests
Extracted an extra method into helper for reuse.
2025-07-18 10:58:10 +01:00
Dan Brown
d15eb129b0 API: Initial review pass of zip import/export endpoints
Review of #5592
2025-07-18 09:54:49 +01:00
Dan Brown
3626a2265b Merge branch 'development' of github.com:LM-Nishant/BookStack into LM-Nishant-development 2025-07-18 09:19:32 +01:00
Dan Brown
d13abc7e1d Mail: Removed custom symfony/mailer fork
Moved to standard symfony mailer now that my patches have been
upstreamed. This changes the config to work with the symfony option,
following the same overall logic.
Also updated testing to allow test runs via mulitple custom env options.

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

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

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

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

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

Related to #5269
2025-05-03 20:30:50 +01:00
Dan Brown
c82fa33210 Comments: Further range of content reference ux improvements
- Added reference indicator to comment create form.
  - Added remove action.
- Extracted reference text to translations.
- Changed reference hash to be text-based instead of HTML based.
- Added reference display for newly added comments.
- Handled reference marker delete on comment delete.
2025-05-01 17:22:12 +01:00
Dan Brown
15c79c38db Comments: Addressed a range of edge cases and ux issues for references
Handles only display and handling references when they're in the active
tab, while handling proper removal when made not visible.
2025-05-01 16:33:42 +01:00
Dan Brown
e7dcc2dcdf Comments: Moved to tab UI, Converted tabs component to ts 2025-04-30 17:42:09 +01:00
Dan Brown
099f6104d0 Comments: Started archive display, created mode for tree node 2025-04-28 20:09:18 +01:00
Dan Brown
8bdf948743 Comments: Added archive endpoints, messages, Js actions and tests 2025-04-28 15:37:09 +01:00
Dan Brown
e8f44186a8 Comments: Split out page comment reference logic to own component
Started support for editor view.
Moved comment elements to be added relative to content area instad of
specific target reference element.
Added relocating on screen size change.
2025-04-27 16:51:24 +01:00
Dan Brown
ecda4e1d6f Comments: Added reference marker to comments 2025-04-26 21:05:54 +01:00
nchoudhary@logicmines.in
64da80cbf4 added routes for zip export 2025-04-25 13:00:06 +05:30
nchoudhary@logicmines.in
5fa728f28a Develop functionality to import ZIP files. Create an API controller and define a route entry for handling the import process. Implement logic to read the list of files within the ZIP, process the directory structure, and automatically create associated pages, chapters, and books based on the ZIP file's contents. 2025-04-25 12:48:34 +05:30
nchoudhary@logicmines.in
c61ce8dee4 Implement functionality to export a book, along with its pages and chapters, as a ZIP file. 2025-04-25 12:45:09 +05:30
Dan Brown
f656a82fe7 Comments: Styled content comments & improved interaction 2025-04-24 13:21:23 +01:00
Dan Brown
5bfba281fc Comments: Started inline comment display windows 2025-04-21 14:04:41 +01:00
Dan Brown
18ede9bbd3 Comments: Added inline comment marker/highlight logic 2025-04-19 14:07:52 +01:00
Dan Brown
2e7544a865 Comments: Converted comment component to TS 2025-04-19 12:46:47 +01:00
Dan Brown
5e3c3ad634 Comments: Added back-end content reference handling
Also added archived property, to be added.
2025-04-18 21:13:49 +01:00
Dan Brown
add238fe9f Comments & Pointer: Converted components to typescript
Made changes for dom and translation services for easier usage
considering types.
trans_choice updated to allow default count replacement data as per
Laravel's default behaviour.
2025-04-18 20:42:56 +01:00
Dan Brown
8d159f77e4 Comments: Started logic for content references
Adds button for comments to pointer.
Adds logic to generate a content reference point.
2025-04-18 15:01:57 +01:00
Talstra Ruben SRSNL
da82e70ca3 Add optional OIDC avatar fetching from the “picture” claim 2025-01-20 17:21:46 +01:00
517 changed files with 10820 additions and 4739 deletions

View File

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

View File

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

View File

@@ -484,3 +484,19 @@ Ilya Shaulov (ishaulov) :: Russian
Konstantin Bobkov (b.konstantv) :: Russian
Ruben Sutter (rubensutter) :: German
jellium :: French
Qxlkdr :: Swedish
Hari (muhhari) :: Indonesian
仙君御 (xjy) :: Chinese Simplified
TapioM :: Finnish
lingb58 :: Chinese Traditional
Angel Pandey (angel-pandey) :: Nepali
Supriya Shrestha (supriyashrestha) :: Nepali
gprabhat :: Nepali
CellCat :: Chinese Simplified
Al Desrahim (aldesrahim) :: Indonesian
ahmad abbaspour (deshneh.dar.diss) :: Persian
Erjon K. (ekr) :: Albanian
LiZerui (iamzrli) :: Chinese Traditional
Ticker (ticker.com) :: Hebrew
CrazyComputer :: Chinese Simplified
Firr (FirrV) :: Russian

View File

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

View File

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

View File

@@ -4,6 +4,8 @@ namespace BookStack\Activity;
use BookStack\Activity\Models\Comment;
use BookStack\Entities\Models\Entity;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PrettyException;
use BookStack\Facades\Activity as ActivityService;
use BookStack\Util\HtmlDescriptionFilter;
@@ -20,7 +22,7 @@ class CommentRepo
/**
* Create a new comment on an entity.
*/
public function create(Entity $entity, string $html, ?int $parent_id): Comment
public function create(Entity $entity, string $html, ?int $parentId, string $contentRef): Comment
{
$userId = user()->id;
$comment = new Comment();
@@ -29,7 +31,8 @@ class CommentRepo
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id;
$comment->parent_id = $parentId;
$comment->content_ref = preg_match('/^bkmrk-(.*?):\d+:(\d*-\d*)?$/', $contentRef) === 1 ? $contentRef : '';
$entity->comments()->save($comment);
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
@@ -52,6 +55,41 @@ class CommentRepo
return $comment;
}
/**
* Archive an existing comment.
*/
public function archive(Comment $comment): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be archived.', '/', 400);
}
$comment->archived = true;
$comment->save();
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
return $comment;
}
/**
* Un-archive an existing comment.
*/
public function unarchive(Comment $comment): Comment
{
if ($comment->parent_id) {
throw new NotifyException('Only top-level comments can be un-archived.', '/', 400);
}
$comment->archived = false;
$comment->save();
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
return $comment;
}
/**
* Delete a comment from the system.
*/

View File

@@ -3,6 +3,8 @@
namespace BookStack\Activity\Controllers;
use BookStack\Activity\CommentRepo;
use BookStack\Activity\Tools\CommentTree;
use BookStack\Activity\Tools\CommentTreeNode;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Http\Controller;
use Illuminate\Http\Request;
@@ -26,6 +28,7 @@ class CommentController extends Controller
$input = $this->validate($request, [
'html' => ['required', 'string'],
'parent_id' => ['nullable', 'integer'],
'content_ref' => ['string'],
]);
$page = $this->pageQueries->findVisibleById($pageId);
@@ -40,14 +43,12 @@ class CommentController extends Controller
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null);
$contentRef = $input['content_ref'] ?? '';
$comment = $this->commentRepo->create($page, $input['html'], $input['parent_id'] ?? null, $contentRef);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => [
'comment' => $comment,
'children' => [],
]
'branch' => new CommentTreeNode($comment, 0, []),
]);
}
@@ -74,6 +75,46 @@ class CommentController extends Controller
]);
}
/**
* Mark a comment as archived.
*/
public function archive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('page-view', $comment->entity);
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
$this->showPermissionError();
}
$this->commentRepo->archive($comment);
$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}
/**
* Unmark a comment as archived.
*/
public function unarchive(int $id)
{
$comment = $this->commentRepo->getById($id);
$this->checkOwnablePermission('page-view', $comment->entity);
if (!userCan('comment-update', $comment) && !userCan('comment-delete', $comment)) {
$this->showPermissionError();
}
$this->commentRepo->unarchive($comment);
$tree = new CommentTree($comment->entity);
return view('comments.comment-branch', [
'readOnly' => false,
'branch' => $tree->getCommentNodeForId($id),
]);
}
/**
* Delete a comment from the system.
*/

View File

@@ -19,6 +19,8 @@ use Illuminate\Database\Eloquent\Relations\MorphTo;
* @property int $entity_id
* @property int $created_by
* @property int $updated_by
* @property string $content_ref
* @property bool $archived
*/
class Comment extends Model implements Loggable
{

View File

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

View File

@@ -9,7 +9,7 @@ class CommentTree
{
/**
* The built nested tree structure array.
* @var array{comment: Comment, depth: int, children: array}[]
* @var CommentTreeNode[]
*/
protected array $tree;
protected array $comments;
@@ -28,7 +28,7 @@ class CommentTree
public function empty(): bool
{
return count($this->tree) === 0;
return count($this->getActive()) === 0;
}
public function count(): int
@@ -36,9 +36,35 @@ class CommentTree
return count($this->comments);
}
public function get(): array
public function getActive(): array
{
return $this->tree;
return array_filter($this->tree, fn (CommentTreeNode $node) => !$node->comment->archived);
}
public function activeThreadCount(): int
{
return count($this->getActive());
}
public function getArchived(): array
{
return array_filter($this->tree, fn (CommentTreeNode $node) => $node->comment->archived);
}
public function archivedThreadCount(): int
{
return count($this->getArchived());
}
public function getCommentNodeForId(int $commentId): ?CommentTreeNode
{
foreach ($this->tree as $node) {
if ($node->comment->id === $commentId) {
return $node;
}
}
return null;
}
public function canUpdateAny(): bool
@@ -54,6 +80,7 @@ class CommentTree
/**
* @param Comment[] $comments
* @return CommentTreeNode[]
*/
protected function createTree(array $comments): array
{
@@ -77,26 +104,22 @@ class CommentTree
$tree = [];
foreach ($childMap[0] ?? [] as $childId) {
$tree[] = $this->createTreeForId($childId, 0, $byId, $childMap);
$tree[] = $this->createTreeNodeForId($childId, 0, $byId, $childMap);
}
return $tree;
}
protected function createTreeForId(int $id, int $depth, array &$byId, array &$childMap): array
protected function createTreeNodeForId(int $id, int $depth, array &$byId, array &$childMap): CommentTreeNode
{
$childIds = $childMap[$id] ?? [];
$children = [];
foreach ($childIds as $childId) {
$children[] = $this->createTreeForId($childId, $depth + 1, $byId, $childMap);
$children[] = $this->createTreeNodeForId($childId, $depth + 1, $byId, $childMap);
}
return [
'comment' => $byId[$id],
'depth' => $depth,
'children' => $children,
];
return new CommentTreeNode($byId[$id], $depth, $children);
}
protected function loadComments(): array

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
namespace BookStack\Api;
use BookStack\App\AppVersion;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Contracts\Container\BindingResolutionException;
@@ -25,7 +26,7 @@ class ApiDocsGenerator
*/
public static function generateConsideringCache(): Collection
{
$appVersion = trim(file_get_contents(base_path('version')));
$appVersion = AppVersion::get();
$cacheKey = 'api-docs::' . $appVersion;
$isProduction = config('app.env') === 'production';
$cacheVal = $isProduction ? Cache::get($cacheKey) : null;

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

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

View File

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

View File

@@ -1,5 +1,6 @@
<?php
use BookStack\App\AppVersion;
use BookStack\App\Model;
use BookStack\Facades\Theme;
use BookStack\Permissions\PermissionApplicator;
@@ -13,12 +14,7 @@ use BookStack\Users\Models\User;
*/
function versioned_asset(string $file = ''): string
{
static $version = null;
if (is_null($version)) {
$versionFile = base_path('version');
$version = trim(file_get_contents($versionFile));
}
$version = AppVersion::get();
$additional = '';
if (config('app.env') === 'development') {

View File

@@ -32,9 +32,9 @@ return [
'local' => [
'driver' => 'local',
'root' => public_path(),
'visibility' => 'public',
'serve' => false,
'throw' => true,
'directory_visibility' => 'public',
],
'local_secure_attachments' => [
@@ -47,7 +47,6 @@ return [
'local_secure_images' => [
'driver' => 'local',
'root' => storage_path('uploads/images/'),
'visibility' => 'public',
'serve' => false,
'throw' => true,
],

View File

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

View File

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

View File

@@ -18,6 +18,7 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use BookStack\Util\SimpleListOptions;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
@@ -263,7 +264,9 @@ class BookController extends Controller
$this->checkPermission('bookshelf-create-all');
$this->checkPermission('book-create-all');
$shelf = $transformer->transformBookToShelf($book);
$shelf = (new DatabaseTransaction(function () use ($book, $transformer) {
return $transformer->transformBookToShelf($book);
}))->run();
return redirect($shelf->getUrl());
}

View File

@@ -9,12 +9,11 @@ use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\ApiController;
use Exception;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected $rules = [
protected array $rules = [
'create' => [
'book_id' => ['required', 'integer'],
'name' => ['required', 'string', 'max:255'],

View File

@@ -18,6 +18,7 @@ use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
@@ -269,7 +270,9 @@ class ChapterController extends Controller
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->checkPermission('book-create-all');
$book = $transformer->transformChapterToBook($chapter);
$book = (new DatabaseTransaction(function () use ($chapter, $transformer) {
return $transformer->transformChapterToBook($chapter);
}))->run();
return redirect($book->getUrl());
}

View File

@@ -12,7 +12,7 @@ use Illuminate\Http\Request;
class PageApiController extends ApiController
{
protected $rules = [
protected array $rules = [
'create' => [
'book_id' => ['required_without:chapter_id', 'integer'],
'chapter_id' => ['required_without:book_id', 'integer'],

View File

@@ -17,6 +17,7 @@ use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Tools\PageEditorData;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Http\Controller;
use BookStack\References\ReferenceFetcher;
@@ -196,7 +197,7 @@ class PageController extends Controller
public function edit(Request $request, string $bookSlug, string $pageSlug)
{
$page = $this->queries->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-update', $page, $page->getUrl());
$editorData = new PageEditorData($page, $this->entityQueries, $request->query('editor', ''));
if ($editorData->getWarnings()) {

View File

@@ -77,7 +77,6 @@ class BaseRepo
$entity->touch();
}
$entity->rebuildPermissions();
$entity->indexForSearch();
$this->referenceStore->updateForEntity($entity);
@@ -139,7 +138,7 @@ class BaseRepo
/**
* Sort the parent of the given entity, if any auto sort actions are set for it.
* Typical ran during create/update/insert events.
* Typically ran during create/update/insert events.
*/
public function sortParent(Entity $entity): void
{

View File

@@ -10,6 +10,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Activity;
use BookStack\Sorting\SortRule;
use BookStack\Uploads\ImageRepo;
use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Http\UploadedFile;
@@ -28,19 +29,22 @@ class BookRepo
*/
public function create(array $input): Book
{
$book = new Book();
$this->baseRepo->create($book, $input);
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::BOOK_CREATE, $book);
return (new DatabaseTransaction(function () use ($input) {
$book = new Book();
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
$book->sort_rule_id = $defaultBookSortSetting;
$book->save();
}
$this->baseRepo->create($book, $input);
$this->baseRepo->updateCoverImage($book, $input['image'] ?? null);
$this->baseRepo->updateDefaultTemplate($book, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::BOOK_CREATE, $book);
return $book;
$defaultBookSortSetting = intval(setting('sorting-book-default', '0'));
if ($defaultBookSortSetting && SortRule::query()->find($defaultBookSortSetting)) {
$book->sort_rule_id = $defaultBookSortSetting;
$book->save();
}
return $book;
}))->run();
}
/**

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Facades\Activity;
use BookStack\Util\DatabaseTransaction;
use Exception;
class BookshelfRepo
@@ -23,13 +24,14 @@ class BookshelfRepo
*/
public function create(array $input, array $bookIds): Bookshelf
{
$shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input);
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
$this->updateBooks($shelf, $bookIds);
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
return $shelf;
return (new DatabaseTransaction(function () use ($input, $bookIds) {
$shelf = new Bookshelf();
$this->baseRepo->create($shelf, $input);
$this->baseRepo->updateCoverImage($shelf, $input['image'] ?? null);
$this->updateBooks($shelf, $bookIds);
Activity::add(ActivityType::BOOKSHELF_CREATE, $shelf);
return $shelf;
}))->run();
}
/**

View File

@@ -11,6 +11,7 @@ use BookStack\Entities\Tools\TrashCan;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use BookStack\Util\DatabaseTransaction;
use Exception;
class ChapterRepo
@@ -27,16 +28,18 @@ class ChapterRepo
*/
public function create(array $input, Book $parentBook): Chapter
{
$chapter = new Chapter();
$chapter->book_id = $parentBook->id;
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
return (new DatabaseTransaction(function () use ($input, $parentBook) {
$chapter = new Chapter();
$chapter->book_id = $parentBook->id;
$chapter->priority = (new BookContents($parentBook))->getLastPriority() + 1;
$this->baseRepo->create($chapter, $input);
$this->baseRepo->updateDefaultTemplate($chapter, intval($input['default_template_id'] ?? null));
Activity::add(ActivityType::CHAPTER_CREATE, $chapter);
$this->baseRepo->sortParent($chapter);
$this->baseRepo->sortParent($chapter);
return $chapter;
return $chapter;
}))->run();
}
/**
@@ -88,12 +91,14 @@ class ChapterRepo
throw new PermissionsException('User does not have permission to create a chapter within the chosen book');
}
$chapter->changeBook($parent->id);
$chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
return (new DatabaseTransaction(function () use ($chapter, $parent) {
$chapter->changeBook($parent->id);
$chapter->rebuildPermissions();
Activity::add(ActivityType::CHAPTER_MOVE, $chapter);
$this->baseRepo->sortParent($chapter);
$this->baseRepo->sortParent($chapter);
return $parent;
return $parent;
}))->run();
}
}

View File

@@ -18,6 +18,7 @@ use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use BookStack\References\ReferenceStore;
use BookStack\References\ReferenceUpdater;
use BookStack\Util\DatabaseTransaction;
use Exception;
class PageRepo
@@ -61,8 +62,10 @@ class PageRepo
]);
}
$page->save();
$page->refresh()->rebuildPermissions();
(new DatabaseTransaction(function () use ($page) {
$page->save();
$page->refresh()->rebuildPermissions();
}))->run();
return $page;
}
@@ -72,26 +75,29 @@ class PageRepo
*/
public function publishDraft(Page $draft, array $input): Page
{
$draft->draft = false;
$draft->revision_count = 1;
$draft->priority = $this->getNewPriority($draft);
$this->updateTemplateStatusAndContentFromInput($draft, $input);
$this->baseRepo->update($draft, $input);
return (new DatabaseTransaction(function () use ($draft, $input) {
$draft->draft = false;
$draft->revision_count = 1;
$draft->priority = $this->getNewPriority($draft);
$this->updateTemplateStatusAndContentFromInput($draft, $input);
$this->baseRepo->update($draft, $input);
$draft->rebuildPermissions();
$summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
$this->revisionRepo->storeNewForPage($draft, $summary);
$draft->refresh();
$summary = trim($input['summary'] ?? '') ?: trans('entities.pages_initial_revision');
$this->revisionRepo->storeNewForPage($draft, $summary);
$draft->refresh();
Activity::add(ActivityType::PAGE_CREATE, $draft);
$this->baseRepo->sortParent($draft);
Activity::add(ActivityType::PAGE_CREATE, $draft);
$this->baseRepo->sortParent($draft);
return $draft;
return $draft;
}))->run();
}
/**
* Directly update the content for the given page from the provided input.
* Used for direct content access in a way that performs required changes
* (Search index & reference regen) without performing an official update.
* (Search index and reference regen) without performing an official update.
*/
public function setContentFromInput(Page $page, array $input): void
{
@@ -116,7 +122,7 @@ class PageRepo
$page->revision_count++;
$page->save();
// Remove all update drafts for this user & page.
// Remove all update drafts for this user and page.
$this->revisionRepo->deleteDraftsForCurrentUser($page);
// Save a revision after updating
@@ -269,16 +275,18 @@ class PageRepo
throw new PermissionsException('User does not have permission to create a page within the new parent');
}
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
$page->changeBook($newBookId);
$page->rebuildPermissions();
return (new DatabaseTransaction(function () use ($page, $parent) {
$page->chapter_id = ($parent instanceof Chapter) ? $parent->id : null;
$newBookId = ($parent instanceof Chapter) ? $parent->book->id : $parent->id;
$page->changeBook($newBookId);
$page->rebuildPermissions();
Activity::add(ActivityType::PAGE_MOVE, $page);
Activity::add(ActivityType::PAGE_MOVE, $page);
$this->baseRepo->sortParent($page);
$this->baseRepo->sortParent($page);
return $parent;
return $parent;
}))->run();
}
/**

View File

@@ -13,17 +13,12 @@ use BookStack\Facades\Activity;
class HierarchyTransformer
{
protected BookRepo $bookRepo;
protected BookshelfRepo $shelfRepo;
protected Cloner $cloner;
protected TrashCan $trashCan;
public function __construct(BookRepo $bookRepo, BookshelfRepo $shelfRepo, Cloner $cloner, TrashCan $trashCan)
{
$this->bookRepo = $bookRepo;
$this->shelfRepo = $shelfRepo;
$this->cloner = $cloner;
$this->trashCan = $trashCan;
public function __construct(
protected BookRepo $bookRepo,
protected BookshelfRepo $shelfRepo,
protected Cloner $cloner,
protected TrashCan $trashCan
) {
}
/**

View File

@@ -15,6 +15,7 @@ use BookStack\Exceptions\NotifyException;
use BookStack\Facades\Activity;
use BookStack\Uploads\AttachmentService;
use BookStack\Uploads\ImageService;
use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Carbon;
@@ -357,25 +358,26 @@ class TrashCan
/**
* Destroy the given entity.
* Returns the number of total entities destroyed in the operation.
*
* @throws Exception
*/
public function destroyEntity(Entity $entity): int
{
if ($entity instanceof Page) {
return $this->destroyPage($entity);
}
if ($entity instanceof Chapter) {
return $this->destroyChapter($entity);
}
if ($entity instanceof Book) {
return $this->destroyBook($entity);
}
if ($entity instanceof Bookshelf) {
return $this->destroyShelf($entity);
}
$result = (new DatabaseTransaction(function () use ($entity) {
if ($entity instanceof Page) {
return $this->destroyPage($entity);
} else if ($entity instanceof Chapter) {
return $this->destroyChapter($entity);
} else if ($entity instanceof Book) {
return $this->destroyBook($entity);
} else if ($entity instanceof Bookshelf) {
return $this->destroyShelf($entity);
}
return null;
}))->run();
return 0;
return $result ?? 0;
}
/**

View File

@@ -2,6 +2,7 @@
namespace BookStack\Exceptions;
use BookStack\App\AppVersion;
use Illuminate\Contracts\Foundation\ExceptionRenderer;
class BookStackExceptionHandlerPage implements ExceptionRenderer
@@ -30,9 +31,7 @@ class BookStackExceptionHandlerPage implements ExceptionRenderer
return [
'PHP Version' => phpversion(),
'BookStack Version' => $this->safeReturn(function () {
$versionFile = base_path('version');
return trim(file_get_contents($versionFile));
return AppVersion::get();
}, 'unknown'),
'Theme Configured' => $this->safeReturn(function () {
return config('view.theme');

View File

@@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers;
use BookStack\Entities\Queries\BookQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\ApiController;
use Throwable;
@@ -63,4 +64,15 @@ class BookExportApiController extends ApiController
return $this->download()->directly($markdown, $book->slug . '.md');
}
/**
* Export a book as a contained ZIP export file.
*/
public function exportZip(int $id, ZipExportBuilder $builder)
{
$book = $this->queries->findVisibleByIdOrFail($id);
$zip = $builder->buildForBook($book);
return $this->download()->streamedFileDirectly($zip, $book->slug . '.zip', true);
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\ApiController;
use Throwable;
@@ -63,4 +64,15 @@ class ChapterExportApiController extends ApiController
return $this->download()->directly($markdown, $chapter->slug . '.md');
}
/**
* Export a chapter as a contained ZIP file.
*/
public function exportZip(int $id, ZipExportBuilder $builder)
{
$chapter = $this->queries->findVisibleByIdOrFail($id);
$zip = $builder->buildForChapter($chapter);
return $this->download()->streamedFileDirectly($zip, $chapter->slug . '.zip', true);
}
}

View File

@@ -0,0 +1,144 @@
<?php
declare(strict_types=1);
namespace BookStack\Exports\Controllers;
use BookStack\Exceptions\ZipImportException;
use BookStack\Exceptions\ZipValidationException;
use BookStack\Exports\ImportRepo;
use BookStack\Http\ApiController;
use BookStack\Uploads\AttachmentService;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Response;
class ImportApiController extends ApiController
{
public function __construct(
protected ImportRepo $imports,
) {
$this->middleware('can:content-import');
}
/**
* List existing ZIP imports visible to the user.
* Requires permission to import content.
*/
public function list(): JsonResponse
{
$query = $this->imports->queryVisible();
return $this->apiListingResponse($query, [
'id', 'name', 'size', 'type', 'created_by', 'created_at', 'updated_at'
]);
}
/**
* Start a new import from a ZIP file.
* This does not actually run the import since that is performed via the "run" endpoint.
* This uploads, validates and stores the ZIP file so it's ready to be imported.
*
* This "file" parameter must be a BookStack-compatible ZIP file, and this must be
* sent via a 'multipart/form-data' type request.
*
* Requires permission to import content.
*/
public function create(Request $request): JsonResponse
{
$this->validate($request, $this->rules()['create']);
$file = $request->file('file');
try {
$import = $this->imports->storeFromUpload($file);
} catch (ZipValidationException $exception) {
$message = "ZIP upload failed with the following validation errors: \n" . $this->formatErrors($exception->errors);
return $this->jsonError($message, 422);
}
return response()->json($import);
}
/**
* Read details of a pending ZIP import.
* The "details" property contains high-level metadata regarding the ZIP import content,
* and the structure of this will change depending on import "type".
* Requires permission to import content.
*/
public function read(int $id): JsonResponse
{
$import = $this->imports->findVisible($id);
$import->setAttribute('details', $import->decodeMetadata());
return response()->json($import);
}
/**
* Run the import process for an uploaded ZIP import.
* The "parent_id" and "parent_type" parameters are required when the import type is "chapter" or "page".
* On success, this endpoint returns the imported item.
* Requires permission to import content.
*/
public function run(int $id, Request $request): JsonResponse
{
$import = $this->imports->findVisible($id);
$parent = null;
$rules = $this->rules()['run'];
if ($import->type === 'page' || $import->type === 'chapter') {
$rules['parent_type'][] = 'required';
$rules['parent_id'][] = 'required';
$data = $this->validate($request, $rules);
$parent = "{$data['parent_type']}:{$data['parent_id']}";
}
try {
$entity = $this->imports->runImport($import, $parent);
} catch (ZipImportException $exception) {
$message = "ZIP import failed with the following errors: \n" . $this->formatErrors($exception->errors);
return $this->jsonError($message);
}
return response()->json($entity->withoutRelations());
}
/**
* Delete a pending ZIP import from the system.
* Requires permission to import content.
*/
public function delete(int $id): Response
{
$import = $this->imports->findVisible($id);
$this->imports->deleteImport($import);
return response('', 204);
}
protected function rules(): array
{
return [
'create' => [
'file' => ['required', ...AttachmentService::getFileValidationRules()],
],
'run' => [
'parent_type' => ['string', 'in:book,chapter'],
'parent_id' => ['int'],
],
];
}
protected function formatErrors(array $errors): string
{
$parts = [];
foreach ($errors as $key => $error) {
if (is_string($key)) {
$parts[] = "[{$key}] {$error}";
} else {
$parts[] = $error;
}
}
return implode("\n", $parts);
}
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Exports\Controllers;
use BookStack\Entities\Queries\PageQueries;
use BookStack\Exports\ExportFormatter;
use BookStack\Exports\ZipExports\ZipExportBuilder;
use BookStack\Http\ApiController;
use Throwable;
@@ -63,4 +64,15 @@ class PageExportApiController extends ApiController
return $this->download()->directly($markdown, $page->slug . '.md');
}
/**
* Export a page as a contained ZIP file.
*/
public function exportZip(int $id, ZipExportBuilder $builder)
{
$page = $this->queries->findVisibleByIdOrFail($id);
$zip = $builder->buildForPage($page);
return $this->download()->streamedFileDirectly($zip, $page->slug . '.zip', true);
}
}

View File

@@ -28,6 +28,8 @@ class Import extends Model implements Loggable
{
use HasFactory;
protected $hidden = ['metadata'];
public function getSizeString(): string
{
$mb = round($this->size / 1000000, 2);

View File

@@ -17,6 +17,7 @@ use BookStack\Exports\ZipExports\ZipExportValidator;
use BookStack\Exports\ZipExports\ZipImportRunner;
use BookStack\Facades\Activity;
use BookStack\Uploads\FileStorage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@@ -34,6 +35,11 @@ class ImportRepo
* @return Collection<Import>
*/
public function getVisibleImports(): Collection
{
return $this->queryVisible()->get();
}
public function queryVisible(): Builder
{
$query = Import::query();
@@ -41,7 +47,7 @@ class ImportRepo
$query->where('created_by', user()->id);
}
return $query->get();
return $query;
}
public function findVisible(int $id): Import

View File

@@ -2,6 +2,7 @@
namespace BookStack\Exports\ZipExports;
use BookStack\App\AppVersion;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
@@ -70,7 +71,7 @@ class ZipExportBuilder
$this->data['exported_at'] = date(DATE_ATOM);
$this->data['instance'] = [
'id' => setting('instance-id', ''),
'version' => trim(file_get_contents(base_path('version'))),
'version' => AppVersion::get(),
];
$zipFile = tempnam(sys_get_temp_dir(), 'bszip-');

View File

@@ -8,7 +8,7 @@ use Illuminate\Http\JsonResponse;
abstract class ApiController extends Controller
{
protected $rules = [];
protected array $rules = [];
/**
* Provide a paginated listing JSON response in a standard format

View File

@@ -49,13 +49,13 @@ abstract class Controller extends BaseController
* On a permission error redirect to home and display.
* the error as a notification.
*
* @return never
* @throws NotifyException
*/
protected function showPermissionError()
protected function showPermissionError(string $redirectLocation = '/'): never
{
$message = request()->wantsJson() ? trans('errors.permissionJson') : trans('errors.permission');
throw new NotifyException($message, '/', 403);
throw new NotifyException($message, $redirectLocation, 403);
}
/**
@@ -81,10 +81,10 @@ abstract class Controller extends BaseController
/**
* Check the current user's permissions against an ownable item otherwise throw an exception.
*/
protected function checkOwnablePermission(string $permission, Model $ownable): void
protected function checkOwnablePermission(string $permission, Model $ownable, string $redirectLocation = '/'): void
{
if (!userCan($permission, $ownable)) {
$this->showPermissionError();
$this->showPermissionError($redirectLocation);
}
}
@@ -163,7 +163,7 @@ abstract class Controller extends BaseController
*/
protected function getImageValidationRules(): array
{
return ['image_extension', 'mimes:jpeg,png,gif,webp', 'max:' . (config('app.upload_limit') * 1000)];
return ['image_extension', 'mimes:jpeg,png,gif,webp,avif', 'max:' . (config('app.upload_limit') * 1000)];
}
/**

View File

@@ -16,7 +16,7 @@ class ContentPermissionApiController extends ApiController
) {
}
protected $rules = [
protected array $rules = [
'update' => [
'owner_id' => ['int'],

View File

@@ -29,7 +29,7 @@ class JointPermissionBuilder
/**
* Re-generate all entity permission from scratch.
*/
public function rebuildForAll()
public function rebuildForAll(): void
{
JointPermission::query()->truncate();
@@ -51,7 +51,7 @@ class JointPermissionBuilder
/**
* Rebuild the entity jointPermissions for a particular entity.
*/
public function rebuildForEntity(Entity $entity)
public function rebuildForEntity(Entity $entity): void
{
$entities = [$entity];
if ($entity instanceof Book) {
@@ -119,7 +119,7 @@ class JointPermissionBuilder
/**
* Build joint permissions for the given book and role combinations.
*/
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false)
protected function buildJointPermissionsForBooks(EloquentCollection $books, array $roles, bool $deleteOld = false): void
{
$entities = clone $books;
@@ -143,7 +143,7 @@ class JointPermissionBuilder
/**
* Rebuild the entity jointPermissions for a collection of entities.
*/
protected function buildJointPermissionsForEntities(array $entities)
protected function buildJointPermissionsForEntities(array $entities): void
{
$roles = Role::query()->get()->values()->all();
$this->deleteManyJointPermissionsForEntities($entities);
@@ -155,21 +155,19 @@ class JointPermissionBuilder
*
* @param Entity[] $entities
*/
protected function deleteManyJointPermissionsForEntities(array $entities)
protected function deleteManyJointPermissionsForEntities(array $entities): void
{
$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();
}
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();
}
});
}
}
/**
@@ -195,7 +193,7 @@ class JointPermissionBuilder
* @param Entity[] $originalEntities
* @param Role[] $roles
*/
protected function createManyJointPermissions(array $originalEntities, array $roles)
protected function createManyJointPermissions(array $originalEntities, array $roles): void
{
$entities = $this->entitiesToSimpleEntities($originalEntities);
$jointPermissions = [];
@@ -225,11 +223,9 @@ class JointPermissionBuilder
}
}
DB::transaction(function () use ($jointPermissions) {
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
DB::table('joint_permissions')->insert($jointPermissionChunk);
}
});
foreach (array_chunk($jointPermissions, 1000) as $jointPermissionChunk) {
DB::table('joint_permissions')->insert($jointPermissionChunk);
}
}
/**

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Http\Controller;
use BookStack\Permissions\Models\EntityPermission;
use BookStack\Users\Models\Role;
use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
class PermissionsController extends Controller
@@ -40,7 +41,9 @@ class PermissionsController extends Controller
$page = $this->queries->pages->findVisibleBySlugsOrFail($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$this->permissionsUpdater->updateFromPermissionsForm($page, $request);
(new DatabaseTransaction(function () use ($page, $request) {
$this->permissionsUpdater->updateFromPermissionsForm($page, $request);
}))->run();
$this->showSuccessNotification(trans('entities.pages_permissions_success'));
@@ -70,7 +73,9 @@ class PermissionsController extends Controller
$chapter = $this->queries->chapters->findVisibleBySlugsOrFail($bookSlug, $chapterSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
(new DatabaseTransaction(function () use ($chapter, $request) {
$this->permissionsUpdater->updateFromPermissionsForm($chapter, $request);
}))->run();
$this->showSuccessNotification(trans('entities.chapters_permissions_success'));
@@ -100,7 +105,9 @@ class PermissionsController extends Controller
$book = $this->queries->books->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $book);
$this->permissionsUpdater->updateFromPermissionsForm($book, $request);
(new DatabaseTransaction(function () use ($book, $request) {
$this->permissionsUpdater->updateFromPermissionsForm($book, $request);
}))->run();
$this->showSuccessNotification(trans('entities.books_permissions_updated'));
@@ -130,7 +137,9 @@ class PermissionsController extends Controller
$shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
(new DatabaseTransaction(function () use ($shelf, $request) {
$this->permissionsUpdater->updateFromPermissionsForm($shelf, $request);
}))->run();
$this->showSuccessNotification(trans('entities.shelves_permissions_updated'));
@@ -145,7 +154,10 @@ class PermissionsController extends Controller
$shelf = $this->queries->shelves->findVisibleBySlugOrFail($slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$updateCount = $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
$updateCount = (new DatabaseTransaction(function () use ($shelf) {
return $this->permissionsUpdater->updateBookPermissionsFromShelf($shelf);
}))->run();
$this->showSuccessNotification(trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($shelf->getUrl());

View File

@@ -7,6 +7,7 @@ use BookStack\Exceptions\PermissionsException;
use BookStack\Facades\Activity;
use BookStack\Permissions\Models\RolePermission;
use BookStack\Users\Models\Role;
use BookStack\Util\DatabaseTransaction;
use Exception;
use Illuminate\Database\Eloquent\Collection;
@@ -48,38 +49,42 @@ class PermissionsRepo
*/
public function saveNewRole(array $roleData): Role
{
$role = new Role($roleData);
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->save();
return (new DatabaseTransaction(function () use ($roleData) {
$role = new Role($roleData);
$role->mfa_enforced = boolval($roleData['mfa_enforced'] ?? false);
$role->save();
$permissions = $roleData['permissions'] ?? [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
$permissions = $roleData['permissions'] ?? [];
$this->assignRolePermissions($role, $permissions);
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_CREATE, $role);
Activity::add(ActivityType::ROLE_CREATE, $role);
return $role;
return $role;
}))->run();
}
/**
* Updates an existing role.
* Ensures Admin system role always have core permissions.
* Ensures the Admin system role always has core permissions.
*/
public function updateRole($roleId, array $roleData): Role
{
$role = $this->getRoleById($roleId);
if (isset($roleData['permissions'])) {
$this->assignRolePermissions($role, $roleData['permissions']);
}
return (new DatabaseTransaction(function () use ($role, $roleData) {
if (isset($roleData['permissions'])) {
$this->assignRolePermissions($role, $roleData['permissions']);
}
$role->fill($roleData);
$role->save();
$this->permissionBuilder->rebuildForRole($role);
$role->fill($roleData);
$role->save();
$this->permissionBuilder->rebuildForRole($role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
Activity::add(ActivityType::ROLE_UPDATE, $role);
return $role;
return $role;
}))->run();
}
/**
@@ -114,7 +119,7 @@ class PermissionsRepo
/**
* Delete a role from the system.
* Check it's not an admin role or set as default before deleting.
* If a migration Role ID is specified the users assign to the current role
* If a migration Role ID is specified, the users assigned to the current role
* will be added to the role of the specified id.
*
* @throws PermissionsException
@@ -131,17 +136,19 @@ class PermissionsRepo
throw new PermissionsException(trans('errors.role_registration_default_cannot_delete'));
}
if ($migrateRoleId !== 0) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();
$newRole->users()->sync($users);
(new DatabaseTransaction(function () use ($migrateRoleId, $role) {
if ($migrateRoleId !== 0) {
$newRole = Role::query()->find($migrateRoleId);
if ($newRole) {
$users = $role->users()->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}
}
$role->entityPermissions()->delete();
$role->jointPermissions()->delete();
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
$role->entityPermissions()->delete();
$role->jointPermissions()->delete();
Activity::add(ActivityType::ROLE_DELETE, $role);
$role->delete();
}))->run();
}
}

View File

@@ -9,7 +9,7 @@ use Illuminate\Http\Request;
class SearchApiController extends ApiController
{
protected $rules = [
protected array $rules = [
'all' => [
'query' => ['required'],
'page' => ['integer', 'min:1'],

View File

@@ -160,7 +160,9 @@ class SearchIndex
/** @var DOMNode $child */
foreach ($doc->getBodyChildren() as $child) {
$nodeName = $child->nodeName;
$termCounts = $this->textToTermCountMap(trim($child->textContent));
$text = trim($child->textContent);
$text = str_replace("\u{00A0}", ' ', $text);
$termCounts = $this->textToTermCountMap($text);
foreach ($termCounts as $term => $count) {
$scoreChange = $count * ($elementScoreAdjustmentMap[$nodeName] ?? 1);
$scoresByTerm[$term] = ($scoresByTerm[$term] ?? 0) + $scoreChange;

View File

@@ -3,6 +3,7 @@
namespace BookStack\Settings;
use BookStack\Activity\ActivityType;
use BookStack\App\AppVersion;
use BookStack\Entities\Tools\TrashCan;
use BookStack\Http\Controller;
use BookStack\References\ReferenceStore;
@@ -19,14 +20,11 @@ class MaintenanceController extends Controller
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
// Recycle bin details
$recycleStats = $trashCan->getTrashedCounts();
return view('settings.maintenance', [
'version' => $version,
'version' => AppVersion::get(),
'recycleStats' => $recycleStats,
]);
}

View File

@@ -3,6 +3,7 @@
namespace BookStack\Settings;
use BookStack\Activity\ActivityType;
use BookStack\App\AppVersion;
use BookStack\Http\Controller;
use BookStack\Users\Models\User;
use Illuminate\Http\Request;
@@ -26,12 +27,9 @@ class SettingController extends Controller
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.settings'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings.categories.' . $category, [
'category' => $category,
'version' => $version,
'version' => AppVersion::get(),
'guestUser' => User::getGuest(),
]);
}

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Queries\BookQueries;
use BookStack\Entities\Tools\BookContents;
use BookStack\Facades\Activity;
use BookStack\Http\Controller;
use BookStack\Util\DatabaseTransaction;
use Illuminate\Http\Request;
class BookSortController extends Controller
@@ -55,16 +56,18 @@ class BookSortController extends Controller
// Sort via map
if ($request->filled('sort-tree')) {
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$booksInvolved = $sorter->sortUsingMap($sortMap);
(new DatabaseTransaction(function () use ($book, $request, $sorter, &$loggedActivityForBook) {
$sortMap = BookSortMap::fromJson($request->get('sort-tree'));
$booksInvolved = $sorter->sortUsingMap($sortMap);
// Rebuild permissions and add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
if ($bookInvolved->id === $book->id) {
$loggedActivityForBook = true;
// Add activity for involved books.
foreach ($booksInvolved as $bookInvolved) {
Activity::add(ActivityType::BOOK_SORT, $bookInvolved);
if ($bookInvolved->id === $book->id) {
$loggedActivityForBook = true;
}
}
}
}))->run();
}
if ($request->filled('auto-sort')) {

View File

@@ -2,7 +2,6 @@
namespace BookStack\Sorting;
use BookStack\App\Model;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Chapter;

View File

@@ -50,6 +50,7 @@ class LocaleManager
'ku' => 'ku_TR',
'lt' => 'lt_LT',
'lv' => 'lv_LV',
'ne' => 'ne_NP',
'nb' => 'nb_NO',
'nl' => 'nl_NL',
'nn' => 'nn_NO',

View File

@@ -6,6 +6,7 @@ use BookStack\Exceptions\ImageUploadException;
use Exception;
use GuzzleHttp\Psr7\Utils;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use Intervention\Image\Decoders\BinaryImageDecoder;
use Intervention\Image\Drivers\Gd\Decoders\NativeObjectDecoder;
use Intervention\Image\Drivers\Gd\Driver;
@@ -93,8 +94,8 @@ class ImageResizer
$imageData = $disk->get($imagePath);
// Do not resize apng images where we're not cropping
if ($keepRatio && $this->isApngData($image, $imageData)) {
// Do not resize animated images where we're not cropping
if ($keepRatio && $this->isAnimated($image, $imageData)) {
Cache::put($thumbCacheKey, $image->path, static::THUMBNAIL_CACHE_TIME);
return $this->storage->getPublicUrl($image->path);
@@ -240,15 +241,50 @@ class ImageResizer
/**
* Check if the given image and image data is apng.
*/
protected function isApngData(Image $image, string &$imageData): bool
protected function isApngData(string &$imageData): bool
{
$isPng = strtolower(pathinfo($image->path, PATHINFO_EXTENSION)) === 'png';
if (!$isPng) {
return false;
}
$initialHeader = substr($imageData, 0, strpos($imageData, 'IDAT'));
return str_contains($initialHeader, 'acTL');
}
/**
* Check if the given avif image data represents an animated image.
* This is based up the answer here: https://stackoverflow.com/a/79457313
*/
protected function isAnimatedAvifData(string &$imageData): bool
{
$stszPos = strpos($imageData, 'stsz');
if ($stszPos === false) {
return false;
}
// Look 12 bytes after the start of 'stsz'
$start = $stszPos + 12;
$end = $start + 4;
if ($end > strlen($imageData) - 1) {
return false;
}
$data = substr($imageData, $start, 4);
$count = unpack('Nvalue', $data)['value'];
return $count > 1;
}
/**
* Check if the given image is animated.
*/
protected function isAnimated(Image $image, string &$imageData): bool
{
$extension = strtolower(pathinfo($image->path, PATHINFO_EXTENSION));
if ($extension === 'png') {
return $this->isApngData($imageData);
}
if ($extension === 'avif') {
return $this->isAnimatedAvifData($imageData);
}
return false;
}
}

View File

@@ -13,7 +13,7 @@ use Symfony\Component\HttpFoundation\StreamedResponse;
class ImageService
{
protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
protected static array $supportedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'avif'];
public function __construct(
protected ImageStorage $storage,

View File

@@ -5,6 +5,9 @@ namespace BookStack\Uploads;
use BookStack\Util\FilePathNormalizer;
use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Log;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\Visibility;
use Symfony\Component\HttpFoundation\StreamedResponse;
class ImageStorageDisk
@@ -74,12 +77,19 @@ class ImageStorageDisk
$path = $this->adjustPathForDisk($path);
$this->filesystem->put($path, $data);
// Set visibility when a non-AWS-s3, s3-like storage option is in use.
// Done since this call can break s3-like services but desired for other image stores.
// Attempting to set ACL during above put request requires different permissions
// hence would technically be a breaking change for actual s3 usage.
// Set public visibility to ensure public access on S3, or that the file is accessible
// to other processes (like web-servers) for local file storage options.
// We avoid attempting this for (non-AWS) s3-like systems (even in a try-catch) as
// we've always avoided setting permissions for s3-like due to potential issues,
// with docs advising setting pre-configured permissions instead.
// We also don't do this as the default filesystem/driver level as that can technically
// require different ACLs for S3, and this provides us more logical control.
if ($makePublic && !$this->isS3Like()) {
$this->filesystem->setVisibility($path, 'public');
try {
$this->filesystem->setVisibility($path, Visibility::PUBLIC);
} catch (UnableToSetVisibility $e) {
Log::warning("Unable to set visibility for image upload with relative path: {$path}");
}
}
}

View File

@@ -5,6 +5,7 @@ namespace BookStack\Uploads;
use BookStack\Exceptions\HttpFetchException;
use BookStack\Http\HttpRequestService;
use BookStack\Users\Models\User;
use BookStack\Util\WebSafeMimeSniffer;
use Exception;
use GuzzleHttp\Psr7\Request;
use Illuminate\Support\Facades\Log;
@@ -53,6 +54,33 @@ class UserAvatars
}
}
/**
* Assign a new avatar image to the given user by fetching from a remote URL.
*/
public function assignToUserFromUrl(User $user, string $avatarUrl): void
{
try {
$this->destroyAllForUser($user);
$imageData = $this->getAvatarImageData($avatarUrl);
$mime = (new WebSafeMimeSniffer())->sniff($imageData);
[$format, $type] = explode('/', $mime, 2);
if ($format !== 'image' || !ImageService::isExtensionSupported($type)) {
return;
}
$avatar = $this->createAvatarImageFromData($user, $imageData, $type);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
Log::error('Failed to save user avatar image from URL', [
'exception' => $e->getMessage(),
'url' => $avatarUrl,
'user_id' => $user->id,
]);
}
}
/**
* Destroy all user avatars uploaded to the given user.
*/
@@ -105,7 +133,7 @@ class UserAvatars
}
/**
* Gets an image from url and returns it as a string of image data.
* Get an image from a URL and return it as a string of image data.
*
* @throws HttpFetchException
*/
@@ -113,7 +141,19 @@ class UserAvatars
{
try {
$client = $this->http->buildClient(5);
$response = $client->sendRequest(new Request('GET', $url));
$responseCount = 0;
do {
$response = $client->sendRequest(new Request('GET', $url));
$responseCount++;
$isRedirect = ($response->getStatusCode() === 301 || $response->getStatusCode() === 302);
$url = $response->getHeader('Location')[0] ?? '';
} while ($responseCount < 3 && $isRedirect && is_string($url) && str_starts_with($url, 'http'));
if ($responseCount === 3) {
throw new HttpFetchException("Failed to fetch image, max redirect limit of 3 tries reached. Last fetched URL: {$url}");
}
if ($response->getStatusCode() !== 200) {
throw new HttpFetchException(trans('errors.cannot_get_image_from_url', ['url' => $url]));
}

View File

@@ -16,7 +16,7 @@ class RoleApiController extends ApiController
'display_name', 'description', 'mfa_enforced', 'external_auth_id', 'created_at', 'updated_at',
];
protected $rules = [
protected array $rules = [
'create' => [
'display_name' => ['required', 'string', 'min:3', 'max:180'],
'description' => ['string', 'max:180'],

View File

@@ -45,6 +45,7 @@ use Illuminate\Support\Collection;
* @property string $system_name
* @property Collection $roles
* @property Collection $mfaValues
* @property ?Image $avatar
*/
class User extends Model implements AuthenticatableContract, CanResetPasswordContract, Loggable, Sluggable
{

View File

@@ -0,0 +1,42 @@
<?php
namespace BookStack\Util;
use Closure;
use Illuminate\Support\Facades\DB;
use Throwable;
/**
* Run the given code within a database transactions.
* Wraps Laravel's own transaction method, but sets a specific runtime isolation method.
* This sets a session level since this won't cause issues if already within a transaction,
* and this should apply to the next transactions anyway.
*
* "READ COMMITTED" ensures that changes from other transactions can be read within
* a transaction, even if started afterward (and for example, it was blocked by the initial
* transaction). This is quite important for things like permission generation, where we would
* want to consider the changes made by other committed transactions by the time we come to
* regenerate permission access.
*
* @throws Throwable
* @template TReturn of mixed
*/
class DatabaseTransaction
{
/**
* @param (Closure(static): TReturn) $callback
*/
public function __construct(
protected Closure $callback
) {
}
/**
* @return TReturn
*/
public function run(): mixed
{
DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');
return DB::transaction($this->callback);
}
}

View File

@@ -4,7 +4,6 @@ namespace BookStack\Util;
use DOMAttr;
use DOMElement;
use DOMNamedNodeMap;
use DOMNode;
/**
@@ -25,6 +24,7 @@ class HtmlDescriptionFilter
'ul' => [],
'li' => [],
'strong' => [],
'span' => [],
'em' => [],
'br' => [],
];
@@ -59,7 +59,6 @@ class HtmlDescriptionFilter
return;
}
/** @var DOMNamedNodeMap $attrs */
$attrs = $element->attributes;
for ($i = $attrs->length - 1; $i >= 0; $i--) {
/** @var DOMAttr $attr */
@@ -70,7 +69,8 @@ class HtmlDescriptionFilter
}
}
foreach ($element->childNodes as $child) {
$childNodes = [...$element->childNodes];
foreach ($childNodes as $child) {
if ($child instanceof DOMElement) {
static::filterElement($child);
}

Binary file not shown.

View File

@@ -38,8 +38,7 @@
"socialiteproviders/microsoft-azure": "^5.1",
"socialiteproviders/okta": "^4.2",
"socialiteproviders/twitch": "^5.3",
"ssddanbrown/htmldiff": "^1.0.2",
"ssddanbrown/symfony-mailer": "7.2.x-dev"
"ssddanbrown/htmldiff": "^2.0.0"
},
"require-dev": {
"fakerphp/faker": "^1.21",

1066
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -27,6 +27,8 @@ class CommentFactory extends Factory
'html' => $html,
'parent_id' => null,
'local_id' => 1,
'content_ref' => '',
'archived' => false,
];
}
}

View File

@@ -24,6 +24,7 @@ class ImportFactory extends Factory
'path' => 'uploads/files/imports/' . Str::random(10) . '.zip',
'name' => $this->faker->words(3, true),
'type' => 'book',
'size' => rand(1, 1001),
'metadata' => '{"name": "My book"}',
'created_at' => User::factory(),
];

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->string('content_ref');
$table->boolean('archived')->index();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('comments', function (Blueprint $table) {
$table->dropColumn('content_ref');
$table->dropColumn('archived');
});
}
};

View File

@@ -0,0 +1,4 @@
{
"parent_type": "book",
"parent_id": 28
}

View File

@@ -0,0 +1,10 @@
{
"type": "chapter",
"name": "Pension Providers",
"created_by": 1,
"size": 2757,
"path": "uploads\/files\/imports\/ghnxmS3u9QxLWu82.zip",
"updated_at": "2025-07-18T14:50:27.000000Z",
"created_at": "2025-07-18T14:50:27.000000Z",
"id": 31
}

View File

@@ -0,0 +1,23 @@
{
"data": [
{
"id": 25,
"name": "IT Department",
"size": 618462,
"type": "book",
"created_by": 1,
"created_at": "2024-12-20T18:40:38.000000Z",
"updated_at": "2024-12-20T18:40:38.000000Z"
},
{
"id": 27,
"name": "Clients",
"size": 15364,
"type": "chapter",
"created_by": 1,
"created_at": "2025-03-20T12:41:44.000000Z",
"updated_at": "2025-03-20T12:41:44.000000Z"
}
],
"total": 2
}

View File

@@ -0,0 +1,51 @@
{
"id": 25,
"name": "IT Department",
"path": "uploads\/files\/imports\/7YOpZ6sGIEbYdRFL.zip",
"size": 618462,
"type": "book",
"created_by": 1,
"created_at": "2024-12-20T18:40:38.000000Z",
"updated_at": "2024-12-20T18:40:38.000000Z",
"details": {
"id": 4,
"name": "IT Department",
"chapters": [
{
"id": 3,
"name": "Server Systems",
"priority": 1,
"pages": [
{
"id": 22,
"name": "prod-aws-stonehawk",
"priority": 0,
"attachments": [],
"images": [],
"tags": []
}
],
"tags": []
}
],
"pages": [
{
"id": 23,
"name": "Member Onboarding Guide",
"priority": 0,
"attachments": [],
"images": [],
"tags": []
},
{
"id": 25,
"name": "IT Holiday Party Event",
"priority": 2,
"attachments": [],
"images": [],
"tags": []
}
],
"tags": []
}
}

View File

@@ -0,0 +1,14 @@
{
"id": 1067,
"book_id": 28,
"slug": "pension-providers",
"name": "Pension Providers",
"description": "Details on the various pension providers that are available",
"priority": 7,
"created_at": "2025-07-18T14:53:35.000000Z",
"updated_at": "2025-07-18T14:53:36.000000Z",
"created_by": 1,
"updated_by": 1,
"owned_by": 1,
"default_template_id": null
}

View File

@@ -0,0 +1,7 @@
{
"version": "v25.02.4",
"instance_id": "1234abcd-cc12-7808-af0a-264cb0cbd611",
"app_name": "My BookStack Instance",
"app_logo": "https://docs.example.com/uploads/images/system/2025-05/cat-icon.png",
"base_url": "https://docs.example.com"
}

View File

@@ -13,7 +13,7 @@ const entryPoints = {
app: path.join(__dirname, '../../resources/js/app.ts'),
code: path.join(__dirname, '../../resources/js/code/index.mjs'),
'legacy-modes': path.join(__dirname, '../../resources/js/code/legacy-modes.mjs'),
markdown: path.join(__dirname, '../../resources/js/markdown/index.mjs'),
markdown: path.join(__dirname, '../../resources/js/markdown/index.mts'),
wysiwyg: path.join(__dirname, '../../resources/js/wysiwyg/index.ts'),
};

View File

@@ -1 +1 @@
70b9287e1ed4dfaece1e37aa2c75d714d6858c0d2a03ebf7095c15d18bd70af9
ecb1c038267dde9336e703e14a89d43848880ebb630465e313b4726570f2db04

View File

@@ -345,7 +345,7 @@ Link: tj/co
codemirror
License: MIT
License File: node_modules/codemirror/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <*******@*****.***> and others
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: https://github.com/codemirror/basic-setup.git
Link: https://github.com/codemirror/basic-setup.git
-----------
@@ -711,13 +711,13 @@ eslint-scope
License: BSD-2-Clause
License File: node_modules/eslint-scope/LICENSE
Copyright: Copyright (C) 2012-2013 Yusuke Suzuki (twitter: @Constellation) and other contributors.
Source: eslint/js
Source: https://github.com/eslint/js.git
Link: https://github.com/eslint/js/blob/main/packages/eslint-scope/README.md
-----------
eslint-visitor-keys
License: Apache-2.0
License File: node_modules/eslint-visitor-keys/LICENSE
Source: eslint/js
Source: https://github.com/eslint/js.git
Link: https://github.com/eslint/js/blob/main/packages/eslint-visitor-keys/README.md
-----------
eslint
@@ -731,7 +731,7 @@ License: BSD-2-Clause
License File: node_modules/espree/LICENSE
Copyright: Copyright (c) Open JS Foundation
All rights reserved.
Source: eslint/js
Source: https://github.com/eslint/js.git
Link: https://github.com/eslint/js/blob/main/packages/espree/README.md
-----------
esprima
@@ -1252,6 +1252,13 @@ Copyright: Copyright (c) 2019 Inspect JS
Source: git+https://github.com/inspect-js/is-map.git
Link: https://github.com/inspect-js/is-map#readme
-----------
is-negative-zero
License: MIT
License File: node_modules/is-negative-zero/LICENSE
Copyright: Copyright (c) 2014 Jordan Harband
Source: git://github.com/inspect-js/is-negative-zero.git
Link: https://github.com/inspect-js/is-negative-zero
-----------
is-number-object
License: MIT
License File: node_modules/is-number-object/LICENSE
@@ -1751,13 +1758,6 @@ Copyright: Copyright (c) Isaac Z. Schlueter and Contributors
Source: git://github.com/isaacs/node-lru-cache.git
Link: git://github.com/isaacs/node-lru-cache.git
-----------
magic-string
License: MIT
License File: node_modules/magic-string/LICENSE
Copyright: Copyright 2018 Rich Harris
Source: https://github.com/rich-harris/magic-string
Link: https://github.com/rich-harris/magic-string
-----------
make-dir
License: MIT
License File: node_modules/make-dir/license
@@ -1935,9 +1935,9 @@ Link: sindresorhus/npm-run-path
nwsapi
License: MIT
License File: node_modules/nwsapi/LICENSE
Copyright: Copyright (c) 2007-2024 Diego Perini (http://www.iport.it/)
Copyright: Copyright (c) 2007-2025 Diego Perini (http://www.iport.it/)
Source: git://github.com/dperini/nwsapi.git
Link: http://javascript.nwbox.com/nwsapi/
Link: https://javascript.nwbox.com/nwsapi/
-----------
object-inspect
License: MIT
@@ -2281,18 +2281,6 @@ Copyright: Copyright (c) 2012 James Halliday
Source: git://github.com/browserify/resolve.git
Link: git://github.com/browserify/resolve.git
-----------
rollup-plugin-dts
License: LGPL-3.0
Source: git+https://github.com/Swatinem/rollup-plugin-dts.git
Link: https://github.com/Swatinem/rollup-plugin-dts#readme
-----------
rollup
License: MIT
License File: node_modules/rollup/LICENSE.md
Copyright: Copyright (c) 2017 [these people](https://github.com/rollup/rollup/graphs/contributors)
Source: rollup/rollup
Link: https://rollupjs.org/
-----------
safe-array-concat
License: MIT
License File: node_modules/safe-array-concat/LICENSE
@@ -2512,6 +2500,13 @@ Copyright: Copyright (c) 2016-2022 Isaac Z. Schlueter <*@***.**>, James Talmage
Source: tapjs/stack-utils
Link: tapjs/stack-utils
-----------
stop-iteration-iterator
License: MIT
License File: node_modules/stop-iteration-iterator/LICENSE
Copyright: Copyright (c) 2023 Jordan Harband
Source: git+https://github.com/ljharb/stop-iteration-iterator.git
Link: https://github.com/ljharb/stop-iteration-iterator#readme
-----------
string-length
License: MIT
License File: node_modules/string-length/license
@@ -3011,6 +3006,13 @@ Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git
Link: https://github.com/babel/babel.git
-----------
@babel/helper-globals
License: MIT
License File: node_modules/@babel/helper-globals/LICENSE
Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Source: https://github.com/babel/babel.git
Link: https://github.com/babel/babel.git
-----------
@babel/helper-module-imports
License: MIT
License File: node_modules/@babel/helper-module-imports/LICENSE
@@ -3057,6 +3059,7 @@ Link: https://github.com/babel/babel.git
License: MIT
License File: node_modules/@babel/helpers/LICENSE
Copyright: Copyright (c) 2014-present Sebastian McKenzie and other contributors
Copyright (c) 2014-present, Facebook, Inc. (ONLY ./src/helpers/regenerator* files)
Source: https://github.com/babel/babel.git
Link: https://babel.dev/docs/en/next/babel-helpers
-----------
@@ -3251,7 +3254,7 @@ Link: https://github.com/codemirror/lang-javascript.git
@codemirror/lang-json
License: MIT
License File: node_modules/@codemirror/lang-json/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <*******@*****.***> and others
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: https://github.com/codemirror/lang-json.git
Link: https://github.com/codemirror/lang-json.git
-----------
@@ -3265,7 +3268,7 @@ Link: https://github.com/codemirror/lang-markdown.git
@codemirror/lang-php
License: MIT
License File: node_modules/@codemirror/lang-php/LICENSE
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <*******@*****.***> and others
Copyright: Copyright (C) 2018-2021 by Marijn Haverbeke <******@*********.******> and others
Source: https://github.com/codemirror/lang-php.git
Link: https://github.com/codemirror/lang-php.git
-----------
@@ -3355,13 +3358,19 @@ Link: https://github.com/eslint-community/regexpp#readme
License: Apache-2.0
License File: node_modules/@eslint/config-array/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
Link: https://github.com/eslint/rewrite/tree/main/packages/config-array#readme
-----------
@eslint/config-helpers
License: Apache-2.0
License File: node_modules/@eslint/config-helpers/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite/tree/main/packages/config-helpers#readme
-----------
@eslint/core
License: Apache-2.0
License File: node_modules/@eslint/core/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
Link: https://github.com/eslint/rewrite/tree/main/packages/core#readme
-----------
@eslint/eslintrc
License: MIT
@@ -3385,7 +3394,7 @@ Link: https://github.com/eslint/rewrite#readme
License: Apache-2.0
License File: node_modules/@eslint/plugin-kit/LICENSE
Source: git+https://github.com/eslint/rewrite.git
Link: https://github.com/eslint/rewrite#readme
Link: https://github.com/eslint/rewrite/tree/main/packages/plugin-kit#readme
-----------
@humanfs/core
License: Apache-2.0
@@ -3526,9 +3535,9 @@ Link: https://github.com/jestjs/jest.git
@jridgewell/gen-mapping
License: MIT
License File: node_modules/@jridgewell/gen-mapping/LICENSE
Copyright: Copyright 2022 Justin Ridgewell <**********@******.***>
Source: https://github.com/jridgewell/gen-mapping
Link: https://github.com/jridgewell/gen-mapping
Copyright: Copyright 2024 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/sourcemaps.git
Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/gen-mapping
-----------
@jridgewell/resolve-uri
License: MIT
@@ -3537,26 +3546,19 @@ Copyright: Copyright 2019 Justin Ridgewell <**********@******.***>
Source: https://github.com/jridgewell/resolve-uri
Link: https://github.com/jridgewell/resolve-uri
-----------
@jridgewell/set-array
License: MIT
License File: node_modules/@jridgewell/set-array/LICENSE
Copyright: Copyright 2022 Justin Ridgewell <**********@******.***>
Source: https://github.com/jridgewell/set-array
Link: https://github.com/jridgewell/set-array
-----------
@jridgewell/sourcemap-codec
License: MIT
License File: node_modules/@jridgewell/sourcemap-codec/LICENSE
Copyright: Copyright (c) 2015 Rich Harris
Source: git+https://github.com/jridgewell/sourcemap-codec.git
Link: git+https://github.com/jridgewell/sourcemap-codec.git
Copyright: Copyright 2024 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/sourcemaps.git
Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/sourcemap-codec
-----------
@jridgewell/trace-mapping
License: MIT
License File: node_modules/@jridgewell/trace-mapping/LICENSE
Copyright: Copyright 2022 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/trace-mapping.git
Link: git+https://github.com/jridgewell/trace-mapping.git
Copyright: Copyright 2024 Justin Ridgewell <******@*********.****>
Source: git+https://github.com/jridgewell/sourcemaps.git
Link: https://github.com/jridgewell/sourcemaps/tree/main/packages/trace-mapping
-----------
@lezer/common
License: MIT
@@ -3635,13 +3637,6 @@ Copyright: Copyright (C) 2018 by Marijn Haverbeke <******@*********.******> and
Source: https://github.com/lezer-parser/xml.git
Link: https://github.com/lezer-parser/xml.git
-----------
@marijn/buildtool
License: MIT
License File: node_modules/@marijn/buildtool/LICENSE
Copyright: Copyright (C) 2022 by Marijn Haverbeke <******@*********.******> and others
Source: https://github.com/marijnh/buildtool.git
Link: https://github.com/marijnh/buildtool.git
-----------
@marijn/find-cluster-break
License: MIT
License File: node_modules/@marijn/find-cluster-break/LICENSE
@@ -3656,6 +3651,13 @@ Copyright: Copyright (c) 2017-present Devon Govett
Source: https://github.com/parcel-bundler/watcher.git
Link: https://github.com/parcel-bundler/watcher.git
-----------
@parcel/watcher-linux-x64-musl
License: MIT
License File: node_modules/@parcel/watcher-linux-x64-musl/LICENSE
Copyright: Copyright (c) 2017-present Devon Govett
Source: https://github.com/parcel-bundler/watcher.git
Link: https://github.com/parcel-bundler/watcher.git
-----------
@parcel/watcher
License: MIT
License File: node_modules/@parcel/watcher/LICENSE
@@ -3825,12 +3827,26 @@ License: MIT
Source: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://www.github.com/DefinitelyTyped/DefinitelyTyped.git
-----------
@types/mocha
@types/linkify-it
License: MIT
License File: node_modules/@types/mocha/LICENSE
License File: node_modules/@types/linkify-it/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mocha
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/linkify-it
-----------
@types/markdown-it
License: MIT
License File: node_modules/@types/markdown-it/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/markdown-it
-----------
@types/mdurl
License: MIT
License File: node_modules/@types/mdurl/LICENSE
Copyright: Copyright (c) Microsoft Corporation.
Source: https://github.com/DefinitelyTyped/DefinitelyTyped.git
Link: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/mdurl
-----------
@types/node
License: MIT

View File

@@ -486,7 +486,7 @@ Link: https://github.com/ramsey/collection.git
ramsey/uuid
License: MIT
License File: vendor/ramsey/uuid/LICENSE
Copyright: Copyright (c) 2012-2023 Ben Ramsey <***@*********.***>
Copyright: Copyright (c) 2012-2025 Ben Ramsey <***@*********.***>
Source: https://github.com/ramsey/uuid.git
Link: https://github.com/ramsey/uuid.git
-----------
@@ -543,13 +543,6 @@ Copyright: Copyright (c) 2024 Nathan Herald, Rohland de Charmoy, Dan Brown
Source: https://codeberg.org/danb/HtmlDiff
Link: https://codeberg.org/danb/HtmlDiff
-----------
ssddanbrown/symfony-mailer
License: MIT
License File: vendor/ssddanbrown/symfony-mailer/LICENSE
Copyright: Copyright (c) 2019-present Fabien Potencier
Source: https://github.com/ssddanbrown/symfony-mailer.git
Link: https://symfony.com
-----------
symfony/clock
License: MIT
License File: vendor/symfony/clock/LICENSE
@@ -620,6 +613,13 @@ Copyright: Copyright (c) 2004-present Fabien Potencier
Source: https://github.com/symfony/http-kernel.git
Link: https://symfony.com
-----------
symfony/mailer
License: MIT
License File: vendor/symfony/mailer/LICENSE
Copyright: Copyright (c) 2019-present Fabien Potencier
Source: https://github.com/symfony/mailer.git
Link: https://symfony.com
-----------
symfony/mime
License: MIT
License File: vendor/symfony/mime/LICENSE

View File

@@ -64,7 +64,7 @@ return [
// Auth
'auth_login' => 'تم تسجيل الدخول',
'auth_register' => 'سجل كمستخدم جديد',
'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة المرور',
'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة السر',
'auth_password_reset_update' => 'إعادة تعيين كلمة مرور المستخدم',
'mfa_setup_method' => 'طريقة المصادقة متعددة العوامل المُهيأة',
'mfa_setup_method_notification' => 'تم إعداد المصادقة متعددة العوامل بنجاح',

View File

@@ -26,7 +26,7 @@ return [
'remember_me' => 'تذكرني',
'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
'create_account' => 'إنشاء حساب',
'already_have_account' => 'لديك حساب بالفعل؟',
'already_have_account' => 'لديك حساب مسبقاً؟',
'dont_have_account' => 'ليس لديك حساب؟',
'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي',
'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي',
@@ -45,7 +45,7 @@ return [
// Password Reset
'reset_password' => 'استعادة كلمة السر',
'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.',
'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة السر.',
'reset_password_send_button' => 'أرسل رابط الاستعادة',
'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة السر إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.',
'reset_password_success' => 'تمت استعادة كلمة السر بنجاح.',
@@ -60,7 +60,7 @@ return [
'email_confirm_action' => 'تأكيد البريد الإلكتروني',
'email_confirm_send_error' => 'تأكيد البريد الإلكتروني مطلوب ولكن النظام لم يستطع إرسال الرسالة. تواصل مع مشرف النظام للتأكد من إعدادات البريد.',
'email_confirm_success' => 'تم تأكيد بريدك الإلكتروني! يمكنك الآن تسجيل الدخول باستخدام عنوان البريد الإلكتروني هذا.',
'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد. الرجاء مراجعة صندوق الوارد',
'email_confirm_resent' => 'تمت إعادة إرسال رسالة التأكيد، الرجاء مراجعة صندوق الوارد.',
'email_confirm_thanks' => 'شكرا للتأكيد!',
'email_confirm_thanks_desc' => 'الرجاء الانتظار لحظة بينما يتم التعامل مع التأكيد الخاص بك. إذا لم يتم إعادة توجيهك بعد 3 ثوان اضغط على الرابط "المتابعة" أدناه للمتابعة.',
@@ -72,24 +72,24 @@ return [
// User Invite
'user_invite_email_subject' => 'تمت دعوتك للانضمام إلى صفحة الحالة الخاصة بـ :app_name!',
'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على %site%.',
'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة مرور الحساب والحصول على الوصول:',
'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على :appName.',
'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة سر الحساب والحصول على الوصول:',
'user_invite_email_action' => 'كلمة سر المستخدم',
'user_invite_page_welcome' => 'مرحبا بكم في :appName!',
'user_invite_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة السر سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.',
'user_invite_page_confirm_button' => 'تأكيد كلمة السر',
'user_invite_success_login' => 'تم تأكيد كلمة السر. يمكنك الآن تسجيل الدخول باستخدام كلمة المرور المحددة للوصول إلى :appName!',
'user_invite_success_login' => 'تم تأكيد كلمة السر. يمكنك الآن تسجيل الدخول باستخدام كلمة السر المحددة للوصول إلى :appName !',
// Multi-factor Authentication
'mfa_setup' => 'إعداد المصادقة متعددة العوامل',
'mfa_setup_desc' => 'إعداد المصادقة متعددة العوامل كطبقة إضافية من الأمان لحساب المستخدم الخاص بك.',
'mfa_setup_configured' => 'تم إعداده مسبقاً',
'mfa_setup_reconfigure' => 'إعادة التكوين',
'mfa_setup_remove_confirmation' => 'هل أنت متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟',
'mfa_setup_action' => 'إعداد (تنصيب)',
'mfa_setup_remove_confirmation' => 'متأكد من أنك تريد إزالة طريقة المصادقة متعددة العوامل هذه؟',
'mfa_setup_action' => 'إعداد',
'mfa_backup_codes_usage_limit_warning' => 'لديك أقل من 5 رموز احتياطية متبقية، الرجاء إنشاء وتخزين مجموعة جديدة قبل نفاد الرموز لتجنب إغلاق حسابك.',
'mfa_option_totp_title' => 'تطبيق الجوال',
'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق جوال يدعم كلمة المرور المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',
'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق جوال يدعم كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',
'mfa_option_backup_codes_title' => 'رموز النسخ الاحتياطي',
'mfa_option_backup_codes_desc' => 'إنشاء مجموعة من رموز النسخ الاحتياطية للاستخدام مرة واحدة و التي سَتُدِخلها عند تسجيل الدخول للتحقق من هويتك. احرص أن تخزينها في مكان آمن.',
'mfa_gen_confirm_and_enable' => 'تأكيد وتمكين',
@@ -98,7 +98,7 @@ return [
'mfa_gen_backup_codes_download' => 'تنزيل الرموز',
'mfa_gen_backup_codes_usage_warning' => 'يمكن استخدام كل رمز مرة واحدة فقط',
'mfa_gen_totp_title' => 'إعداد تطبيق الجوال',
'mfa_gen_totp_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة المرور المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',
'mfa_gen_totp_desc' => 'لاستخدام المصادقة المتعددة ، ستحتاج إلى تطبيق جوال كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',
'mfa_gen_totp_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.',
'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد',
'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:',

View File

@@ -30,6 +30,8 @@ return [
'create' => 'إنشاء',
'update' => 'تحديث',
'edit' => 'تعديل',
'archive' => 'أرشف',
'unarchive' => 'إلغاء الأرشفة',
'sort' => 'سرد',
'move' => 'نقل',
'copy' => 'نسخ',

View File

@@ -48,6 +48,7 @@ return [
'superscript' => 'نص مرتفع',
'subscript' => 'نص منخفض',
'text_color' => 'لون النص',
'highlight_color' => 'لون التمييز',
'custom_color' => 'لون مخصص',
'remove_color' => 'إزالة اللون',
'background_color' => 'لون الخلفية',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(محتوى مستقر)',
'pages_edit_switch_to_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه -WYSIWYG-',
'pages_edit_switch_to_new_wysiwyg' => 'التبديل إلى محرر ما تراه هو ما تحصل عليه الجديد -new WYSIWYG-',
'pages_edit_switch_to_new_wysiwyg_desc' => '(في اختبار ألف)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(في الاختبار التجريبي)',
'pages_edit_set_changelog' => 'تثبيت سجل التعديل',
'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',
'pages_edit_enter_changelog' => 'أدخل سجل التعديل',
@@ -268,6 +268,7 @@ return [
'pages_md_insert_drawing' => 'إدخال رسمة',
'pages_md_show_preview' => 'عرض المعاينة',
'pages_md_sync_scroll' => 'مزامنة معاينة التمرير',
'pages_md_plain_editor' => 'محرر النصوص العادي',
'pages_drawing_unsaved' => 'تم العثور على رسم غير محفوظ',
'pages_drawing_unsaved_confirm' => 'تم العثور على بيانات رسم غير محفوظة من محاولة حفظ رسم سابقة فاشلة. هل ترغب في استعادة هذا الرسم غير المحفوظ ومواصلة تحريره؟',
'pages_not_in_chapter' => 'صفحة ليست في فصل',
@@ -392,8 +393,11 @@ return [
'comment' => 'تعليق',
'comments' => 'تعليقات',
'comment_add' => 'إضافة تعليق',
'comment_none' => 'لا توجد تعليقات لعرضها',
'comment_placeholder' => 'ضع تعليقاً هنا',
'comment_count' => '{0} لا توجد تعليقات|{1} تعليق واحد|{2} تعليقان[3,*] :count تعليقات',
'comment_thread_count' => ':count تعليقات| :count تعليقات',
'comment_archived_count' => ':count مؤرشف',
'comment_archived_threads' => 'المواضيع المؤرشفة',
'comment_save' => 'حفظ التعليق',
'comment_new' => 'تعليق جديد',
'comment_created' => 'تم التعليق :createDiff',
@@ -402,8 +406,14 @@ return [
'comment_deleted_success' => 'تم حذف التعليق',
'comment_created_success' => 'تمت إضافة التعليق',
'comment_updated_success' => 'تم تحديث التعليق',
'comment_archive_success' => 'تم أرشفة التعليق',
'comment_unarchive_success' => 'تعليق غير مؤرشف',
'comment_view' => 'عرض التعليق',
'comment_jump_to_thread' => 'انتقل إلى الموضوع',
'comment_delete_confirm' => 'تأكيد حذف التعليق؟',
'comment_in_reply_to' => 'رداً على :commentId',
'comment_reference' => 'المرجع',
'comment_reference_outdated' => '(قديمة)',
'comment_editor_explain' => 'هذه هي التعليقات المُضافة على هذه الصفحة. يُمكنك إضافة التعليقات وإدارتها عند عرض الصفحة المحفوظة.',
// Revision

View File

@@ -16,7 +16,7 @@ return [
'email_confirmation_expired' => 'صلاحية رابط التأكيد انتهت, تم إرسال رسالة تأكيد جديدة لعنوان البريد الإلكتروني.',
'email_confirmation_awaiting' => 'عنوان البريد الإلكتروني للحساب قيد الاستخدام يحتاج إلى تأكيد',
'ldap_fail_anonymous' => 'فشل الوصول إلى LDAP باستخدام الربط المجهول',
'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و password المعطاة',
'ldap_fail_authed' => 'فشل الوصول إلى LDAP باستخدام dn و كلمة السر المعطاة',
'ldap_extension_not_installed' => 'لم يتم تثبيت إضافة LDAP PHP',
'ldap_cannot_connect' => 'لا يمكن الاتصال بخادم ldap, فشل الاتصال المبدئي',
'saml_already_logged_in' => 'تم تسجيل الدخول بالفعل',

View File

@@ -6,10 +6,10 @@
*/
return [
'password' => 'يجب أن تتكون كلمة المرور من ستة أحرف على الأقل وأن تطابق التأكيد.',
'password' => 'يجب أن تتكون كلمة السر من ستة أحرف على الأقل وأن تطابق التأكيد.',
'user' => "لم يتم العثور على مستخدم بعنوان البريد الإلكتروني المعطى.",
'token' => 'رمز إعادة تعيين كلمة المرور غير صالح لعنوان هذا البريد الإلكتروني.',
'sent' => 'تم إرسال رابط تجديد كلمة المرور إلى بريدكم الإلكتروني!',
'reset' => 'تم تجديد كلمة المرور الخاصة بكم!',
'token' => 'رمز إعادة تعيين كلمة السر غير صالح لعنوان هذا البريد الإلكتروني.',
'sent' => 'تم إرسال رابط تجديد كلمة السر إلى بريدكم الإلكتروني!',
'reset' => 'تم تجديد كلمة السر الخاصة بكم!',
];

View File

@@ -220,12 +220,12 @@ return [
'users_role' => 'أدوار المستخدمين',
'users_role_desc' => 'حدد الأدوار التي سيتم تعيين هذا المستخدم لها. إذا تم تعيين مستخدم لأدوار متعددة ، فسيتم تكديس الأذونات من هذه الأدوار وسيتلقى كل قدرات الأدوار المعينة.',
'users_password' => 'كلمة مرور المستخدم',
'users_password_desc' => 'عيّن كلمة مرور لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.',
'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة المرور الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.',
'users_password_desc' => 'عيّن كلمة سر لتسجيل الدخول إلى التطبيق. يجب ألا تقل عن 8 أحرف.',
'users_send_invite_text' => 'يمكنك اختيار إرسال دعوة بالبريد الإلكتروني إلى هذا المستخدم مما يسمح له بتعيين كلمة السر الخاصة به أو يمكنك تعيين كلمة المرور الخاصة به بنفسك.',
'users_send_invite_option' => 'أرسل بريدًا إلكترونيًا لدعوة المستخدم',
'users_external_auth_id' => 'ربط الحساب بمواقع التواصل',
'users_external_auth_id_desc' => 'عند استخدام نظام مصادقة خارجي (مثل SAML2 أو OIDC أو LDAP)، يكون هذا هو المعرف الذي يربط مستخدم بوكستاك -BookStack- بحساب نظام المصادقة. يمكنك تجاهل هذا الحقل عند استخدام المصادقة الافتراضية عبر البريد الإلكتروني.',
'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة المرور لهذا المستخدم.',
'users_password_warning' => 'قم بملء الحقل أدناه فقط إذا كنت ترغب في تغيير كلمة السر لهذا المستخدم.',
'users_system_public' => 'هذا المستخدم يمثل أي ضيف يقوم بزيارة شيء يخصك. لا يمكن استخدامه لتسجيل الدخول ولكن يتم تعيينه تلقائياً.',
'users_delete' => 'حذف المستخدم',
'users_delete_named' => 'حذف المستخدم :userName',
@@ -348,6 +348,7 @@ return [
'lt' => 'Lietuvių Kalba',
'lv' => 'Latviešu Valoda',
'nb' => 'Norsk (Bokmål)',
'ne' => 'नेपाली',
'nn' => 'Nynorsk',
'nl' => 'Nederlands',
'pl' => 'Polski',

View File

@@ -113,7 +113,7 @@ return [
// Custom validation lines
'custom' => [
'password-confirm' => [
'required_with' => 'يجب تأكيد كلمة المرور',
'required_with' => 'يجب تأكيد كلمة السر',
],
],

View File

@@ -30,6 +30,8 @@ return [
'create' => 'Създаване',
'update' => 'Обновяване',
'edit' => 'Редактиране',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Сортиране',
'move' => 'Преместване',
'copy' => 'Копиране',

View File

@@ -48,6 +48,7 @@ return [
'superscript' => 'Горен индекс',
'subscript' => 'Долен индекс',
'text_color' => 'Цвят на текста',
'highlight_color' => 'Highlight color',
'custom_color' => 'Цвят по избор',
'remove_color' => 'Премахване на цвят',
'background_color' => 'Фонов цвят',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_set_changelog' => 'Задайте регистър на промените',
'pages_edit_enter_changelog_desc' => 'Въведете кратко резюме на промените, които сте създали',
'pages_edit_enter_changelog' => 'Въведи регистър на промените',
@@ -268,6 +268,7 @@ return [
'pages_md_insert_drawing' => 'Вмъкни рисунка',
'pages_md_show_preview' => 'Show preview',
'pages_md_sync_scroll' => 'Sync preview scroll',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
'pages_not_in_chapter' => 'Страницата не принадлежи в никоя глава',
@@ -392,8 +393,11 @@ return [
'comment' => 'Коментирай',
'comments' => 'Коментари',
'comment_add' => 'Добавяне на коментар',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Напишете коментар',
'comment_count' => '{0} Няма коментари|{1} 1 коментар|[2,*] :count коментара',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Запази коментар',
'comment_new' => 'Нов коментар',
'comment_created' => 'коментирано :createDiff',
@@ -402,8 +406,14 @@ return [
'comment_deleted_success' => 'Коментарът е изтрит',
'comment_created_success' => 'Коментарът е добавен',
'comment_updated_success' => 'Коментарът е обновен',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Наистина ли искате да изтриете този коментар?',
'comment_in_reply_to' => 'В отговор на :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
// Revision

View File

@@ -348,6 +348,7 @@ return [
'lt' => 'Lietuvių Kalba',
'lv' => 'Latviešu Valoda',
'nb' => 'Norsk (Bokmål)',
'ne' => 'नेपाली',
'nn' => 'Nynorsk',
'nl' => 'Nederlands',
'pl' => 'Polski',

View File

@@ -30,6 +30,8 @@ return [
'create' => 'তৈরী করুন',
'update' => 'হালনাগাদ করুন',
'edit' => 'সম্পাদন করুন',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'ক্রমান্বয় করুন',
'move' => 'স্থানান্তর করুন',
'copy' => 'অনুলিপি করুন',

View File

@@ -48,6 +48,7 @@ return [
'superscript' => 'Superscript',
'subscript' => 'Subscript',
'text_color' => 'Text color',
'highlight_color' => 'Highlight color',
'custom_color' => 'Custom color',
'remove_color' => 'Remove color',
'background_color' => 'Background color',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
@@ -268,6 +268,7 @@ return [
'pages_md_insert_drawing' => 'Insert Drawing',
'pages_md_show_preview' => 'Show preview',
'pages_md_sync_scroll' => 'Sync preview scroll',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
'pages_not_in_chapter' => 'Page is not in a chapter',
@@ -392,8 +393,11 @@ return [
'comment' => 'Comment',
'comments' => 'Comments',
'comment_add' => 'Add Comment',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Leave a comment here',
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Save Comment',
'comment_new' => 'New Comment',
'comment_created' => 'commented :createDiff',
@@ -402,8 +406,14 @@ return [
'comment_deleted_success' => 'Comment deleted',
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
// Revision

View File

@@ -348,6 +348,7 @@ return [
'lt' => 'Lietuvių Kalba',
'lv' => 'Latviešu Valoda',
'nb' => 'Norsk (Bokmål)',
'ne' => 'नेपाली',
'nn' => 'Nynorsk',
'nl' => 'Nederlands',
'pl' => 'Polski',

View File

@@ -30,6 +30,8 @@ return [
'create' => 'Kreiraj',
'update' => 'Ažuriraj',
'edit' => 'Uredi',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Sortiraj',
'move' => 'Pomjeri',
'copy' => 'Kopiraj',

View File

@@ -48,6 +48,7 @@ return [
'superscript' => 'Superscript',
'subscript' => 'Subscript',
'text_color' => 'Text color',
'highlight_color' => 'Highlight color',
'custom_color' => 'Custom color',
'remove_color' => 'Remove color',
'background_color' => 'Background color',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Stable Content)',
'pages_edit_switch_to_wysiwyg' => 'Switch to WYSIWYG Editor',
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_set_changelog' => 'Set Changelog',
'pages_edit_enter_changelog_desc' => 'Enter a brief description of the changes you\'ve made',
'pages_edit_enter_changelog' => 'Enter Changelog',
@@ -268,6 +268,7 @@ return [
'pages_md_insert_drawing' => 'Insert Drawing',
'pages_md_show_preview' => 'Show preview',
'pages_md_sync_scroll' => 'Sync preview scroll',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => 'Unsaved Drawing Found',
'pages_drawing_unsaved_confirm' => 'Unsaved drawing data was found from a previous failed drawing save attempt. Would you like to restore and continue editing this unsaved drawing?',
'pages_not_in_chapter' => 'Page is not in a chapter',
@@ -392,8 +393,11 @@ return [
'comment' => 'Comment',
'comments' => 'Comments',
'comment_add' => 'Add Comment',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Leave a comment here',
'comment_count' => '{0} No Comments|{1} 1 Comment|[2,*] :count Comments',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Save Comment',
'comment_new' => 'New Comment',
'comment_created' => 'commented :createDiff',
@@ -402,8 +406,14 @@ return [
'comment_deleted_success' => 'Comment deleted',
'comment_created_success' => 'Comment added',
'comment_updated_success' => 'Comment updated',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Are you sure you want to delete this comment?',
'comment_in_reply_to' => 'In reply to :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
// Revision

View File

@@ -348,6 +348,7 @@ return [
'lt' => 'Lietuvių Kalba',
'lv' => 'Latviešu Valoda',
'nb' => 'Norsk (Bokmål)',
'ne' => 'नेपाली',
'nn' => 'Nynorsk',
'nl' => 'Nederlands',
'pl' => 'Polski',

View File

@@ -30,6 +30,8 @@ return [
'create' => 'Crea',
'update' => 'Actualitza',
'edit' => 'Edita',
'archive' => 'Archive',
'unarchive' => 'Un-Archive',
'sort' => 'Ordena',
'move' => 'Mou',
'copy' => 'Copia',

View File

@@ -48,6 +48,7 @@ return [
'superscript' => 'Superíndex',
'subscript' => 'Subíndex',
'text_color' => 'Color del text',
'highlight_color' => 'Highlight color',
'custom_color' => 'Color personalitzat',
'remove_color' => 'Elimina el color',
'background_color' => 'Color de fons',

View File

@@ -248,7 +248,7 @@ return [
'pages_edit_switch_to_markdown_stable' => '(Contingut estable)',
'pages_edit_switch_to_wysiwyg' => 'Canvia a leditor WYSIWYG',
'pages_edit_switch_to_new_wysiwyg' => 'Switch to new WYSIWYG',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Alpha Testing)',
'pages_edit_switch_to_new_wysiwyg_desc' => '(In Beta Testing)',
'pages_edit_set_changelog' => 'Registre de canvis',
'pages_edit_enter_changelog_desc' => 'Introduïu una descripció breu dels canvis que heu fet',
'pages_edit_enter_changelog' => 'Registra un canvi',
@@ -268,6 +268,7 @@ return [
'pages_md_insert_drawing' => 'Insereix un dibuix',
'pages_md_show_preview' => 'Mostra la visualització prèvia',
'pages_md_sync_scroll' => 'Sincronitza el desplaçament de la visualització prèvia',
'pages_md_plain_editor' => 'Plaintext editor',
'pages_drawing_unsaved' => 'Sha trobat un dibuix sense desar',
'pages_drawing_unsaved_confirm' => 'Shan trobat dades dun dibuix dun intent anterior de desar un dibuix. Voleu restaurar aquest dibuix no desat per a reprendren ledició?',
'pages_not_in_chapter' => 'La pàgina no és un capítol',
@@ -392,8 +393,11 @@ return [
'comment' => 'Comentari',
'comments' => 'Comentaris',
'comment_add' => 'Afegeix un comentari',
'comment_none' => 'No comments to display',
'comment_placeholder' => 'Deixa un comentari aquí',
'comment_count' => '{0} No hi ha cap comentari|{1} Hi ha 1 comentari|[2,*] Hi ha :count comentaris',
'comment_thread_count' => ':count Comment Thread|:count Comment Threads',
'comment_archived_count' => ':count Archived',
'comment_archived_threads' => 'Archived Threads',
'comment_save' => 'Desa el comentari',
'comment_new' => 'Crea un comentari',
'comment_created' => 'ha comentat :createDiff',
@@ -402,8 +406,14 @@ return [
'comment_deleted_success' => 'Sha suprimit el comentari',
'comment_created_success' => 'Sha afegit un comentari',
'comment_updated_success' => 'Sha actualitzat un comentari',
'comment_archive_success' => 'Comment archived',
'comment_unarchive_success' => 'Comment un-archived',
'comment_view' => 'View comment',
'comment_jump_to_thread' => 'Jump to thread',
'comment_delete_confirm' => 'Esteu segur que voleu suprimir aquest comentari?',
'comment_in_reply_to' => 'En resposta a :commentId',
'comment_reference' => 'Reference',
'comment_reference_outdated' => '(Outdated)',
'comment_editor_explain' => 'Vet aquí els comentaris que shan fet en aquesta pàgina. Els comentaris es poden fer i gestionar quan es visualitza la pàgina desada.',
// Revision

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