Compare commits

..

208 Commits

Author SHA1 Message Date
Dan Brown
3131050acd Updated version and assets for release v25.07.2 2025-08-28 17:41:48 +01:00
Dan Brown
c0d2874892 Merge branch 'development' into release 2025-08-28 17:39:31 +01:00
Dan Brown
481f356068 Updated translator & dependency attribution before release v25.07.2 2025-08-28 17:39:10 +01:00
Dan Brown
955837c9aa Packages: Upgraded php deps to latest versions 2025-08-28 15:02:26 +01:00
Dan Brown
c6e35c2e7c Merge pull request #5775 from BookStackApp/lexical_aug25
Lexical: August 2025 fixes
2025-08-28 15:00:16 +01:00
Dan Brown
0436ccfebf Updated translations with latest Crowdin changes (#5759) 2025-08-28 14:59:36 +01:00
Dan Brown
f5da31037d Lexical: Fixed details tests
Updated to use new test pattern while there.
2025-08-28 11:17:18 +01:00
Dan Brown
46613f76f6 Lexical: Added backspace handling for details
Allows more reliable removal of details block on backspace at first
child position with the details block.
2025-08-27 14:09:38 +01:00
Dan Brown
519acaf324 Lexical: Added better selection display for collapisble blocks 2025-08-27 12:51:36 +01:00
Dan Brown
849bc4d6c3 Lexical: Improved nested details interaction
- Set to open by default on insert.
- Updated selection handling not to always fully cascade to lowest
  editable child on selection, so parents can be reliably selected.
- Updated mouse handling to treat details panes like the root element,
  inserting within-details where relevant.
2025-08-26 14:45:15 +01:00
Dan Brown
ee994fa2b7 Testing: Addressed deprecation in test helper
Also updated version in phpunit config
2025-08-25 15:01:13 +01:00
Dan Brown
13a79b3f96 Shelves: Addressed book edits removing non-visible books
Tracks the non-visible existing books on change, to retain as part of
the assigned books sync.
Added test to cover.

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

For #5761
2025-08-24 14:02:21 +01:00
Dan Brown
5c481b4282 Testing: Added more deprecation output 2025-08-15 12:42:44 +01:00
Dan Brown
9443682ae4 Maintenance: Addressed a range of deprecations
Updated deps to address deprecations fixed in newer Laravel framework
version.
2025-08-15 12:20:35 +01:00
Dan Brown
0311e3d2d7 Readme: Updated sponsor link
Was leading to a 404.
2025-08-14 16:00:46 +01:00
Dan Brown
5940a91809 Updated version and assets for release v25.07.1 2025-08-11 14:43:51 +01:00
Dan Brown
9a4651badb Merge branch 'development' into release 2025-08-11 14:43:13 +01:00
Dan Brown
a50a256939 ZIP Exports: Fixed reference handling for images
Recent changes could mean missed references for images in non-page
locations. This fixes that, and tries to ensure images are used if we
already have a page-based image as part of the ZIP, otherwise ensure we
have a page as part of the export to attach the image to.
2025-08-11 14:19:48 +01:00
Dan Brown
4830248a1e Release: Updated licenses and translator attribution 2025-08-11 13:41:31 +01:00
Dan Brown
1256b30ad4 Updated translations with latest Crowdin changes (#5740) 2025-08-11 13:38:47 +01:00
Dan Brown
777cca76da Deps: Bumped PHP composer deps again 2025-08-11 13:36:06 +01:00
Dan Brown
a2d13124af Testing: Added mail port to testing env options
Prevents conflict with potential user-set option.
For #5755
2025-08-11 13:33:57 +01:00
Dan Brown
bd966ef99e phpstan: Address a range of level 2 issues 2025-08-09 11:09:50 +01:00
Dan Brown
a6b5733ec2 Deps: Updated PHP packages via composer 2025-08-09 10:12:24 +01:00
Dan Brown
e899066e96 Merge branch 'development' of github.com:BookStackApp/BookStack into development 2025-08-08 17:44:40 +01:00
Dan Brown
f4f2435856 Imports: Fixed errors causing user logout on import run
Fixes #5754
2025-08-08 17:43:58 +01:00
Dan Brown
fca4a0563e Merge pull request #5753 from BookStackApp/a11y_menu_updates
A11y: Improved menu tagging
2025-08-08 17:00:07 +01:00
Dan Brown
0bc9ddd780 A11y: Updated other dropdown menus with correct tagging
Made some form improvements at the same time.
2025-08-07 16:37:18 +01:00
Dan Brown
c66f3b2a37 A11y: Improved tagging of profile menu
- Swapped toggle out to actual button.
- Ensured menu items have proper menu item role.
- Added extra roles/labels where is makes sense.
2025-08-07 14:32:20 +01:00
Dan Brown
7bc0d54af1 Readme: Swapped codeclimate reference for custom phpmetrics 2025-08-05 22:00:55 +01:00
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
Dan Brown
573a2dd22a Updated version and assets for release v25.02.2 2025-04-02 17:32:58 +01:00
Dan Brown
b55cc803d3 Merge branch 'development' into release 2025-04-02 17:31:14 +01:00
Dan Brown
fa566f156a Updated translator & dependency attribution before release v25.02.2 2025-04-02 17:30:43 +01:00
Dan Brown
78a0a2f519 Merge pull request #5558 from BookStackApp/lexical_round3
Lexical Fixes: Round 3
2025-04-02 17:23:38 +01:00
Dan Brown
42cbd6adef Updated translations with latest Crowdin changes (#5537) 2025-04-02 17:19:34 +01:00
Dan Brown
6117349893 Deps: Updated composer packages 2025-04-02 15:30:31 +01:00
Dan Brown
1256320c72 Merge branch 'bernardo-campos/development' into development 2025-04-02 15:18:31 +01:00
Dan Brown
1ba0d26fdd Sort Rules: Updated name comparison to not ignore non-ascii chars
Related to #5550 and #5542
2025-04-02 15:17:17 +01:00
Dan Brown
802f69cf35 Comments: Fixed missing comment timestamps
Due to deleted code during Laravel 11 upgrade.
Added test to cover.
Closes #5555
2025-03-30 17:36:48 +01:00
Dan Brown
bb44334224 Lexical: Added tests to cover recent changes
Also updated list tests to new test process.
2025-03-28 18:29:00 +00:00
Dan Brown
9bfcadd95f Lexical: Improved navigation around images/media
- Added specific handling to move/insert-up/down on arrow press.
- Prevented resize overlay from interrupting image node focus.
2025-03-28 14:30:03 +00:00
Dan Brown
62c8eb3357 Lexical: Made list selections & intendting more reliable
- Added handling to not include parent of top-most list range selection
  so that it's not also changed while not visually part of the
  selection range.
- Fixed issue where list items could be left over after unnesting, due
  to empty checks/removals occuring before all child handling.
- Added node sorting, applied to list items during nest operations so
  that selection range remains reliable.
2025-03-27 17:49:48 +00:00
Dan Brown
c03e44124a Lexical: Fixed task list parsing
Updated list DOM parsing to properly consider task list format set by
other MD/WYSIWYG editors.
2025-03-27 14:56:32 +00:00
Dan Brown
5c6671b3bf Lexical: Fixed issues with content not saving
Found that saving via Ctrl+Enter did not save as logic to load editor
output into form was bypassed, which this fixes by ensuring submit
events are raised during for this shortcut.

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

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

Related to #161
2025-03-11 23:52:01 +00:00
Talstra Ruben SRSNL
da82e70ca3 Add optional OIDC avatar fetching from the “picture” claim 2025-01-20 17:21:46 +01:00
605 changed files with 13345 additions and 6439 deletions

View File

@@ -56,6 +56,7 @@ APP_PROXIES=null
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
# An ipv6 address can be used via the square bracket format ([::1]).
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database

View File

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

View File

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

View File

@@ -438,7 +438,7 @@ javadataherian :: Persian
Ludo-code :: French
hollsten :: Swedish
Ngoc Lan Phung (lanpncz) :: Vietnamese
Worive :: Catalan
Worive :: Catalan; French
Илья Скаба (skabailya) :: Russian
Irjan Olsen (Irch) :: Norwegian Bokmal
Aleksandar Jovanovic (jovanoviczaleksandar) :: Serbian (Cyrillic)
@@ -469,3 +469,39 @@ Raphael Moreno (RaphaelMoreno) :: Portuguese, Brazilian
yn (user99) :: Arabic
Pavel Zlatarov (pzlatarov) :: Bulgarian
ingelres :: French
mabdullah :: Arabic
Skrabák Csaba (kekcsi) :: Hungarian
Evert Meulie (Evert) :: Norwegian Bokmal
Jasper Backer (jasperb) :: Dutch
Alexandar Cavdarovski (ace.200112) :: Swedish
구닥다리TV (yjj8353) :: Korean
Onur Oskay (o.oskay) :: Turkish
Sébastien Merveille (SebastienMerv) :: French
Maxim Kouznetsov (masya.work) :: Hebrew
neodvisnost :: Slovenian
Soubi Agatsuma (bisouya) :: Hebrew
Ilya Shaulov (ishaulov) :: Russian
Konstantin Bobkov (b.konstantv) :: Russian
Ruben Sutter (rubensutter) :: German
jellium :: French
Qxlkdr :: Swedish
Hari (muhhari) :: Indonesian
仙君御 (xjy) :: Chinese Simplified
TapioM :: Finnish
lingb58 :: Chinese Traditional
Angel Pandey (angel-pandey) :: Nepali
Supriya Shrestha (supriyashrestha) :: Nepali
gprabhat :: Nepali
CellCat :: Chinese Simplified
Al Desrahim (aldesrahim) :: Indonesian
ahmad abbaspour (deshneh.dar.diss) :: Persian
Erjon K. (ekr) :: Albanian
LiZerui (iamzrli) :: Chinese Traditional
Ticker (ticker.com) :: Hebrew
CrazyComputer :: Chinese Simplified
Firr (FirrV) :: Russian
João Faro (FaroJoaoFaro) :: Portuguese
Danilo dos Santos Barbosa (bozochegou) :: Portuguese, Brazilian
Chris (furesoft) :: German
Silvia Isern (eiendragon) :: Catalan
Dennis Kron Pedersen (ahjdp) :: Danish

1
.gitignore vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

@@ -20,7 +20,8 @@ class PageUpdateNotificationHandler extends BaseNotificationHandler
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
}
// Get last update from activity
// Get the last update from activity
/** @var ?Activity $lastUpdate */
$lastUpdate = $detail->activity()
->where('type', '=', ActivityType::PAGE_UPDATE)
->where('id', '!=', $activity->id)

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

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

View File

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

@@ -40,12 +40,16 @@ if (env('REDIS_SERVERS', false)) {
// MYSQL
// Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);
if (count($mysql_host_exploded) > 1) {
$mysql_host = $mysql_host_exploded[0];
$mysql_port = intval($mysql_host_exploded[1]);
$mysqlHost = env('DB_HOST', 'localhost');
$mysqlHostExploded = explode(':', $mysqlHost);
$mysqlPort = env('DB_PORT', 3306);
$mysqlHostIpv6 = str_starts_with($mysqlHost, '[');
if ($mysqlHostIpv6 && str_contains($mysqlHost, ']:')) {
$mysqlHost = implode(':', array_slice($mysqlHostExploded, 0, -1));
$mysqlPort = intval(end($mysqlHostExploded));
} else if (!$mysqlHostIpv6 && count($mysqlHostExploded) > 1) {
$mysqlHost = $mysqlHostExploded[0];
$mysqlPort = intval($mysqlHostExploded[1]);
}
return [
@@ -61,12 +65,12 @@ return [
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => $mysql_host,
'host' => $mysqlHost,
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'port' => $mysql_port,
'port' => $mysqlPort,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
// Prefixes are only semi-supported and may be unstable
@@ -88,7 +92,7 @@ return [
'database' => 'bookstack-test',
'username' => env('MYSQL_USER', 'bookstack-test'),
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
'port' => $mysql_port,
'port' => $mysqlPort,
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',

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

@@ -2,6 +2,7 @@
namespace BookStack\Entities\Controllers;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Queries\ChapterQueries;
use BookStack\Entities\Queries\EntityQueries;
@@ -9,12 +10,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'],
@@ -144,7 +144,10 @@ class ChapterApiController extends ApiController
$chapter->load(['tags']);
$chapter->makeVisible('description_html');
$chapter->setAttribute('description_html', $chapter->descriptionHtml());
$chapter->setAttribute('book_slug', $chapter->book()->first()->slug);
/** @var Book $book */
$book = $chapter->book()->first();
$chapter->setAttribute('book_slug', $book->slug);
return $chapter;
}

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

@@ -43,7 +43,6 @@ class PageRevisionController extends Controller
->selectRaw("IF(markdown = '', false, true) as is_markdown")
->with(['page.book', 'createdBy'])
->reorder('id', $listOptions->getOrder())
->reorder('created_at', $listOptions->getOrder())
->paginate(50);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName' => $page->getShortName()]));
@@ -52,6 +51,7 @@ class PageRevisionController extends Controller
'revisions' => $revisions,
'page' => $page,
'listOptions' => $listOptions,
'oldestRevisionId' => $page->revisions()->min('id'),
]);
}

View File

@@ -26,6 +26,7 @@ use BookStack\Users\Models\HasOwner;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
@@ -283,10 +284,14 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
public function getParent(): ?self
{
if ($this instanceof Page) {
return $this->chapter_id ? $this->chapter()->withTrashed()->first() : $this->book()->withTrashed()->first();
/** @var BelongsTo<Chapter|Book, Page> $builder */
$builder = $this->chapter_id ? $this->chapter() : $this->book();
return $builder->withTrashed()->first();
}
if ($this instanceof Chapter) {
return $this->book()->withTrashed()->first();
/** @var BelongsTo<Book, Page> $builder */
$builder = $this->book();
return $builder->withTrashed()->first();
}
return null;
@@ -295,7 +300,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
/**
* Rebuild the permissions for this entity.
*/
public function rebuildPermissions()
public function rebuildPermissions(): void
{
app()->make(JointPermissionBuilder::class)->rebuildForEntity(clone $this);
}
@@ -303,7 +308,7 @@ abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
/**
* Index the current entity for search.
*/
public function indexForSearch()
public function indexForSearch(): void
{
app()->make(SearchIndex::class)->indexEntity(clone $this);
}

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();
}
/**
@@ -54,20 +56,37 @@ class BookshelfRepo
/**
* Update which books are assigned to this shelf by syncing the given book ids.
* Function ensures the books are visible to the current user and existing.
* Function ensures the managed books are visible to the current user and existing,
* and that the user does not alter the assignment of books that are not visible to them.
*/
protected function updateBooks(Bookshelf $shelf, array $bookIds)
protected function updateBooks(Bookshelf $shelf, array $bookIds): void
{
$numericIDs = collect($bookIds)->map(function ($id) {
return intval($id);
});
$syncData = $this->bookQueries->visibleForList()
$existingBookIds = $shelf->books()->pluck('id')->toArray();
$visibleExistingBookIds = $this->bookQueries->visibleForList()
->whereIn('id', $existingBookIds)
->pluck('id')
->toArray();
$nonVisibleExistingBookIds = array_values(array_diff($existingBookIds, $visibleExistingBookIds));
$newIdsToAssign = $this->bookQueries->visibleForList()
->whereIn('id', $bookIds)
->pluck('id')
->mapWithKeys(function ($bookId) use ($numericIDs) {
return [$bookId => ['order' => $numericIDs->search($bookId)]];
});
->toArray();
$maxNewIndex = max($numericIDs->keys()->toArray() ?: [0]);
$syncData = [];
foreach ($newIdsToAssign as $id) {
$syncData[$id] = ['order' => $numericIDs->search($id)];
}
foreach ($nonVisibleExistingBookIds as $index => $id) {
$syncData[$id] = ['order' => $maxNewIndex + ($index + 1)];
}
$shelf->books()->sync($syncData);
}

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

@@ -89,7 +89,7 @@ class ImportController extends Controller
try {
$entity = $this->imports->runImport($import, $parent);
} catch (ZipImportException $exception) {
session()->flush();
session()->forget(['success', 'warning']);
$this->showErrorNotification(trans('errors.import_zip_failed_notification'));
return redirect($import->getUrl())->with('import_errors', $exception->errors);
}

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,12 +71,12 @@ 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-');
$zip = new ZipArchive();
$opened = $zip->open($zipFile, ZipArchive::CREATE);
$opened = $zip->open($zipFile, ZipArchive::OVERWRITE);
if ($opened !== true) {
throw new ZipExportException('Failed to create zip file for export.');
}

View File

@@ -17,17 +17,17 @@ use BookStack\Uploads\Image;
class ZipExportReferences
{
/** @var ZipExportPage[] */
/** @var array<int, ZipExportPage> */
protected array $pages = [];
/** @var ZipExportChapter[] */
/** @var array<int, ZipExportChapter> */
protected array $chapters = [];
/** @var ZipExportBook[] */
/** @var array<int, ZipExportBook> */
protected array $books = [];
/** @var ZipExportAttachment[] */
/** @var array<int, ZipExportAttachment> */
protected array $attachments = [];
/** @var ZipExportImage[] */
/** @var array<int, ZipExportImage> */
protected array $images = [];
public function __construct(
@@ -134,11 +134,12 @@ class ZipExportReferences
// Find and include images if in visibility
$page = $model->getPage();
if ($page && userCan('view', $page)) {
$pageExportModel = $this->pages[$page->id] ?? ($exportModel instanceof ZipExportPage ? $exportModel : null);
if (isset($this->images[$model->id]) || ($page && $pageExportModel && userCan('view', $page))) {
if (!isset($this->images[$model->id])) {
$exportImage = ZipExportImage::fromModel($model, $files);
$this->images[$model->id] = $exportImage;
$exportModel->images[] = $exportImage;
$pageExportModel->images[] = $exportImage;
}
return "[[bsexport:image:{$model->id}]]";
}

View File

@@ -29,7 +29,10 @@ class ZipImportReferences
/** @var Image[] */
protected array $images = [];
/** @var array<string, Model> */
/**
* Mapping keyed by "type:old-reference-id" with values being the new imported equivalent model.
* @var array<string, Model>
*/
protected array $referenceMap = [];
/** @var array<int, ZipExportPage> */
@@ -108,6 +111,22 @@ class ZipImportReferences
return null;
}
protected function replaceDrawingIdReferences(string $content): string
{
$referenceRegex = '/\sdrawio-diagram=[\'"](\d+)[\'"]/';
$result = preg_replace_callback($referenceRegex, function ($matches) {
$key = 'image:' . $matches[1];
$model = $this->referenceMap[$key] ?? null;
if ($model instanceof Image && $model->type === 'drawio') {
return ' drawio-diagram="' . $model->id . '"';
}
return $matches[0];
}, $content);
return $result ?: $content;
}
public function replaceReferences(): void
{
foreach ($this->books as $book) {
@@ -134,7 +153,9 @@ class ZipImportReferences
$exportPage = $this->zipExportPageMap[$page->id];
$contentType = $exportPage->markdown ? 'markdown' : 'html';
$content = $exportPage->markdown ?: ($exportPage->html ?: '');
$parsed = $this->parser->parseReferences($content, $this->handleReference(...));
$parsed = $this->replaceDrawingIdReferences($parsed);
$this->pageRepo->setContentFromInput($page, [
$contentType => $parsed,

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

@@ -2,6 +2,7 @@
namespace BookStack\Sorting;
use voku\helper\ASCII;
use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Entity;
@@ -13,12 +14,12 @@ class SortSetOperationComparisons
{
public static function nameAsc(Entity $a, Entity $b): int
{
return strtolower($a->name) <=> strtolower($b->name);
return strtolower(ASCII::to_transliterate($a->name, null)) <=> strtolower(ASCII::to_transliterate($b->name, null));
}
public static function nameDesc(Entity $a, Entity $b): int
{
return strtolower($b->name) <=> strtolower($a->name);
return strtolower(ASCII::to_transliterate($b->name, null)) <=> strtolower(ASCII::to_transliterate($a->name, null));
}
public static function nameNumericAsc(Entity $a, Entity $b): int

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

@@ -7,6 +7,8 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $created_by
* @property int $updated_by
* @property ?User $createdBy
* @property ?User $updatedBy
*/
trait HasCreatorAndUpdater
{

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",

1635
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

@@ -2,6 +2,7 @@
namespace Database\Factories\Entities\Models;
use BookStack\Entities\Tools\PageEditorType;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@@ -29,6 +30,7 @@ class PageFactory extends Factory
'html' => $html,
'text' => strip_tags($html),
'revision_count' => 1,
'editor' => 'wysiwyg',
];
}
}

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'),
};

1
dev/checksums/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
!.gitignore

1
dev/checksums/vendor Normal file
View File

@@ -0,0 +1 @@
fa162564940d9a81e4dd0b20ae8775d32f2ea5c615e33ebcc8adf035d958c352

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

@@ -467,7 +467,7 @@ License: MIT
License File: vendor/psy/psysh/LICENSE
Copyright: Copyright (c) 2012-2023 Justin Hileman
Source: https://github.com/bobthecow/psysh.git
Link: http://psysh.org
Link: https://psysh.org
-----------
ralouphie/getallheaders
License: MIT
@@ -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,12 +64,12 @@ return [
// Auth
'auth_login' => 'تم تسجيل الدخول',
'auth_register' => 'سجل كمستخدم جديد',
'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة المرور',
'auth_password_reset_request' => 'طلب رابط جديد لإعادة تعيين كلمة السر',
'auth_password_reset_update' => 'إعادة تعيين كلمة مرور المستخدم',
'mfa_setup_method' => 'طريقة MFA المكونة',
'mfa_setup_method_notification' => 'تم تكوين طريقة متعددة العوامل بنجاح',
'mfa_remove_method' => 'إزالة طريقة MFA',
'mfa_remove_method_notification' => 'تمت إزالة طريقة متعددة العوامل بنجاح',
'mfa_setup_method' => 'طريقة المصادقة متعددة العوامل المُهيأة',
'mfa_setup_method_notification' => 'تم إعداد المصادقة متعددة العوامل بنجاح',
'mfa_remove_method' => 'إزالة طريقة المصادقة متعددة العوامل',
'mfa_remove_method_notification' => 'تمت إزالة المصادقة متعددة العوامل بنجاح',
// Settings
'settings_update' => 'تحديث الإعدادات',
@@ -77,36 +77,36 @@ return [
'maintenance_action_run' => 'إجراء الصيانة',
// Webhooks
'webhook_create' => 'تم إنشاء webhook',
'webhook_create_notification' => 'تم إنشاء Webhook بنجاح',
'webhook_update' => 'تم تحديث webhook',
'webhook_update_notification' => 'تم تحديث Webhook بنجاح',
'webhook_delete' => 'حذف webhook',
'webhook_delete_notification' => 'تم حذف Webhook بنجاح',
'webhook_create' => 'تم إنشاء خطاف ويب',
'webhook_create_notification' => 'تم إنشاء خطاف ويب بنجاح',
'webhook_update' => 'تم تحديث خطاف الويب',
'webhook_update_notification' => 'تم تحديث خطاف الويب بنجاح',
'webhook_delete' => 'حذف خطاف ويب',
'webhook_delete_notification' => 'تم حذف خطاف الويب بنجاح',
// Imports
'import_create' => 'created import',
'import_create_notification' => 'Import successfully uploaded',
'import_run' => 'updated import',
'import_run_notification' => 'Content successfully imported',
'import_delete' => 'deleted import',
'import_delete_notification' => 'Import successfully deleted',
'import_create' => 'تم إنشاء الاستيراد',
'import_create_notification' => 'تم رفع الاستيراد بنجاح',
'import_run' => 'تم تحديث الاستيراد',
'import_run_notification' => 'تم استيراد المحتوى بنجاح',
'import_delete' => 'تم حذف الاستيراد',
'import_delete_notification' => 'تم الاستيراد بنجاح',
// Users
'user_create' => 'إنشاء مستخدم',
'user_create_notification' => 'تم انشاء الحساب',
'user_create_notification' => 'تم إنشاء الحساب',
'user_update' => 'المستخدم المحدث',
'user_update_notification' => 'تم تحديث المستخدم بنجاح',
'user_delete' => 'المستخدم المحذوف',
'user_delete_notification' => 'تم إزالة المستخدم بنجاح',
// API Tokens
'api_token_create' => 'created API token',
'api_token_create_notification' => 'تم إنشاء رمز الـ API بنجاح',
'api_token_update' => 'updated API token',
'api_token_update_notification' => 'تم تحديث رمز الـ API بنجاح',
'api_token_delete' => 'deleted API token',
'api_token_delete_notification' => 'تم حذف رمز الـ API بنجاح',
'api_token_create' => 'تم إنشاء رمز واجهة برمجة التطبيقات -API-',
'api_token_create_notification' => 'تم إنشاء واجهة برمجة التطبيقات -API- بنجاح',
'api_token_update' => 'رمز واجهة برمجة التطبيقات المحدث',
'api_token_update_notification' => 'تم تحديث رمز واجهة برمجة التطبيقات -API- بنجاح',
'api_token_delete' => 'رمز واجهة برمجة التطبيقات المحذوف',
'api_token_delete_notification' => 'تم حذف رمز واجهة برمجة التطبيقات -API- بنجاح',
// Roles
'role_create' => 'إنشاء صَلاحِيَة',
@@ -128,13 +128,13 @@ return [
'comment_delete' => 'تعليق محذوف',
// Sort Rules
'sort_rule_create' => 'created sort rule',
'sort_rule_create_notification' => 'Sort rule successfully created',
'sort_rule_update' => 'updated sort rule',
'sort_rule_update_notification' => 'Sort rule successfully updated',
'sort_rule_delete' => 'deleted sort rule',
'sort_rule_delete_notification' => 'Sort rule successfully deleted',
'sort_rule_create' => 'تم إنشاء قاعدة الفرز',
'sort_rule_create_notification' => 'تم إنشاء قاعدة الفرز بنجاح',
'sort_rule_update' => 'تم تحديث قاعدة الفرز',
'sort_rule_update_notification' => 'تم تحديث قاعدة الفرز بنجاح',
'sort_rule_delete' => 'تم حذف قاعدة الفرز',
'sort_rule_delete_notification' => 'تم حذف قاعدة الفرز بنجاح',
// Other
'permissions_update' => 'تحديث الأذونات',
'permissions_update' => 'تحديث الصلاحيات',
];

View File

@@ -7,26 +7,26 @@
return [
'failed' => 'البيانات المعطاة لا توافق سجلاتنا.',
'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds seconds.',
'throttle' => 'تجاوزت الحد الأقصى من المحاولات. الرجاء المحاولة مرة أخرى بعد :seconds ثانية/ثواني.',
// Login & Register
'sign_up' => 'إنشاء حساب',
'log_in' => 'تسجيل الدخول',
'log_in_with' => 'تسجيل الدخول باستخدام :socialDriver',
'sign_up_with' => 'إنشاء حساب باستخدام :socialDriver',
'logout' => 'تسجيل الخروج',
'logout' => 'الخروج',
'name' => 'الاسم',
'username' => 'اسم المستخدم',
'email' => 'البريد الإلكتروني',
'password' => 'كلمة المرور',
'password_confirm' => 'تأكيد كلمة المرور',
'password_hint' => 'يجب أن تحتوي كلمة المرور على 8 خانات على الأقل',
'forgot_password' => 'نسيت كلمة المرور؟',
'password' => 'كلمة السر',
'password_confirm' => 'تأكيد كلمة السر',
'password_hint' => 'يجب أن تحتوي كلمة السر على 8 خانات على الأقل',
'forgot_password' => 'نسيت كلمة السر؟',
'remember_me' => 'تذكرني',
'ldap_email_hint' => 'الرجاء إدخال عنوان بريد إلكتروني لاستخدامه مع الحساب.',
'create_account' => 'إنشاء حساب',
'already_have_account' => 'لديك حساب بالفعل؟',
'already_have_account' => 'لديك حساب مسبقاً؟',
'dont_have_account' => 'ليس لديك حساب؟',
'social_login' => 'تسجيل الدخول باستخدام حسابات التواصل الاجتماعي',
'social_registration' => 'إنشاء حساب باستخدام حسابات التواصل الاجتماعي',
@@ -44,14 +44,14 @@ return [
'auto_init_start_link' => 'المتابعة مع المصادقة',
// Password Reset
'reset_password' => 'استعادة كلمة المرور',
'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة المرور.',
'reset_password' => 'استعادة كلمة السر',
'reset_password_send_instructions' => 'أدخل بريدك الإلكتروني بالأسفل وسيتم إرسال رسالة برابط لاستعادة كلمة السر.',
'reset_password_send_button' => 'أرسل رابط الاستعادة',
'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة المرور إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.',
'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.',
'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName',
'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.',
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم، فلا حاجة لاتخاذ أية خطوات.',
'reset_password_sent' => 'سيتم إرسال رابط إعادة تعيين كلمة السر إلى عنوان البريد الإلكتروني هذا إذا كان موجودًا في النظام.',
'reset_password_success' => 'تمت استعادة كلمة السر بنجاح.',
'email_reset_subject' => 'استعد كلمة السر الخاصة بتطبيق :appName',
'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة السر الخاصة بحسابكم.',
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة السر من قبلكم، فلا حاجة لاتخاذ أية خطوات.',
// Email Confirmation
'email_confirm_subject' => 'تأكيد بريدكم الإلكتروني لتطبيق :appName',
@@ -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,46 +72,46 @@ 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_page_text' => 'لإكمال حسابك والحصول على حق الوصول تحتاج إلى تعيين كلمة السر سيتم استخدامها لتسجيل الدخول إلى :appName في الزيارات المستقبلية.',
'user_invite_page_confirm_button' => 'تأكيد كلمة السر',
'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_backup_codes_usage_limit_warning' => 'You have less than 5 backup codes remaining, Please generate and store a new set before you run out of codes to prevent being locked out of your account.',
'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 Authenticer.',
'mfa_option_totp_desc' => 'لاستخدام المصادقة المتعددة العوامل، ستحتاج إلى تطبيق جوال يدعم كلمة السر المؤقته -TOTP- مثل جوجل أوثنتيكاتور -Google Authenticator- أو أوثي -Authy- أو مايكروسوفت أوثنتيكاتور -Microsoft Authenticator-.',
'mfa_option_backup_codes_title' => 'رموز النسخ الاحتياطي',
'mfa_option_backup_codes_desc' => 'Generates a set of one-time-use backup codes which you\'ll enter on login to verify your identity. Make sure to store these in a safe & secure place.',
'mfa_gen_confirm_and_enable' => 'Confirm and Enable',
'mfa_gen_backup_codes_title' => 'Backup Codes Setup',
'mfa_gen_backup_codes_desc' => 'Store the below list of codes in a safe place. When accessing the system you\'ll be able to use one of the codes as a second authentication mechanism.',
'mfa_gen_backup_codes_download' => 'Download Codes',
'mfa_gen_backup_codes_usage_warning' => 'Each code can only be used once',
'mfa_gen_totp_title' => 'Mobile App Setup',
'mfa_gen_totp_desc' => 'To use multi-factor authentication you\'ll need a mobile application that supports TOTP such as Google Authenticator, Authy or Microsoft Authenticator.',
'mfa_gen_totp_scan' => 'Scan the QR code below using your preferred authentication app to get started.',
'mfa_gen_totp_verify_setup' => 'Verify Setup',
'mfa_gen_totp_verify_setup_desc' => 'Verify that all is working by entering a code, generated within your authentication app, in the input box below:',
'mfa_gen_totp_provide_code_here' => 'Provide your app generated code here',
'mfa_verify_access' => 'Verify Access',
'mfa_verify_access_desc' => 'Your user account requires you to confirm your identity via an additional level of verification before you\'re granted access. Verify using one of your configured methods to continue.',
'mfa_verify_no_methods' => 'No Methods Configured',
'mfa_verify_no_methods_desc' => 'No multi-factor authentication methods could be found for your account. You\'ll need to set up at least one method before you gain access.',
'mfa_verify_use_totp' => 'Verify using a mobile app',
'mfa_verify_use_backup_codes' => 'Verify using a backup code',
'mfa_verify_backup_code' => 'Backup Code',
'mfa_verify_backup_code_desc' => 'Enter one of your remaining backup codes below:',
'mfa_verify_backup_code_enter_here' => 'Enter backup code here',
'mfa_verify_totp_desc' => 'Enter the code, generated using your mobile app, below:',
'mfa_setup_login_notification' => 'Multi-factor method configured, Please now login again using the configured method.',
'mfa_option_backup_codes_desc' => 'إنشاء مجموعة من رموز النسخ الاحتياطية للاستخدام مرة واحدة و التي سَتُدِخلها عند تسجيل الدخول للتحقق من هويتك. احرص أن تخزينها في مكان آمن.',
'mfa_gen_confirm_and_enable' => 'تأكيد وتمكين',
'mfa_gen_backup_codes_title' => 'إعداد رموز النسخ الاحتياطي',
'mfa_gen_backup_codes_desc' => 'خَزِن قائمة الرموز أدناه في مكان آمن. عند الوصول إلى النظام، ستتمكن من استخدام أحد الرموز كآلية مصادقة ثانية.',
'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_scan' => 'امسح رمز الاستجابة السريعة -QR- أدناه باستخدام تطبيق المصادقة المفضل لديك للبدء.',
'mfa_gen_totp_verify_setup' => 'التحقق من الإعداد',
'mfa_gen_totp_verify_setup_desc' => 'تحقق أن كل شيء يعمل عن طريق إدخال رمز تم إنشاؤه داخل تطبيق المصادقة الخاص بك في مربع الإدخال أدناه:',
'mfa_gen_totp_provide_code_here' => 'أدخل الرمز الذي تم إنشاؤه للتطبيق الخاص بك هنا',
'mfa_verify_access' => 'التحقق من الوصول',
'mfa_verify_access_desc' => 'يتطلب حساب المستخدم الخاص بك تأكيد هويتك عن طريق مستوى إضافي من التحقق قبل منحك حق الوصول. تحقق استخدام إحدى الطرق التي إعدادها للمتابعة.',
'mfa_verify_no_methods' => 'لا توجد طرق معدة',
'mfa_verify_no_methods_desc' => 'لم يتم العثور على طرق مصادقة متعددة العوامل لحسابك. ستحتاج إلى إعداد طريقة واحدة على الأقل قبل أن تتمكن من الوصول.',
'mfa_verify_use_totp' => 'التحقق باستخدام تطبيق الجوال',
'mfa_verify_use_backup_codes' => 'التحقق باستخدام رمز النسخ الاحتياطي',
'mfa_verify_backup_code' => 'الرموز الاحتياطية',
'mfa_verify_backup_code_desc' => 'أدخل أحد الرموز الاحتياطية المتبقية أدناه:',
'mfa_verify_backup_code_enter_here' => 'أدخل الرمز الاحتياطي هنا',
'mfa_verify_totp_desc' => 'أدخل الرمز الذي تم إنشاؤه باستخدام تطبيق الجوال الخاص بك، أدناه:',
'mfa_setup_login_notification' => 'تم إعداد طريقة الدخول متعددة العوامل، يرجى الآن تسجيل الدخول مرة أخرى باستخدام الطريقة التي تم إعدادها.',
];

View File

@@ -20,7 +20,7 @@ return [
'description' => 'الوصف',
'role' => 'الدور',
'cover_image' => 'صورة الغلاف',
'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440x250 بكسل، على الرغم من أنه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.',
'cover_image_description' => 'يجب أن يكون حجم هذه الصورة تقريبًا 440 في 250 بكسل، مع أنّه سيتم تحجيمها وقصها بشكل مرن لتناسب واجهة المستخدم في سيناريوهات مختلفة حسب الحاجة، لذا فإن الأبعاد الفعلية للعرض ستختلف.',
// Actions
'actions' => 'إجراءات',
@@ -30,6 +30,8 @@ return [
'create' => 'إنشاء',
'update' => 'تحديث',
'edit' => 'تعديل',
'archive' => 'أرشف',
'unarchive' => 'إلغاء الأرشفة',
'sort' => 'سرد',
'move' => 'نقل',
'copy' => 'نسخ',
@@ -48,8 +50,8 @@ return [
'unfavourite' => 'إزالة من المفضلة',
'next' => 'التالي',
'previous' => 'السابق',
'filter_active' => 'الفلاتر المفعلة:',
'filter_clear' => 'مسح الفلاتر',
'filter_active' => 'التصفية المفعلة:',
'filter_clear' => 'مسح التصفية',
'download' => 'تنزيل',
'open_in_tab' => 'فتح في علامة تبويب',
'open' => 'فتح',
@@ -109,5 +111,5 @@ return [
'terms_of_service' => 'اتفاقية شروط الخدمة',
// OpenSearch
'opensearch_description' => 'Search :appName',
'opensearch_description' => 'البحث عن :appName',
];

View File

@@ -6,36 +6,36 @@ return [
// Image Manager
'image_select' => 'تحديد صورة',
'image_list' => 'Image List',
'image_details' => 'Image Details',
'image_upload' => 'Upload Image',
'image_intro' => 'Here you can select and manage images that have been previously uploaded to the system.',
'image_intro_upload' => 'Upload a new image by dragging an image file into this window, or by using the "Upload Image" button above.',
'image_list' => 'قائمة الصور',
'image_details' => 'تفاصيل الصورة',
'image_upload' => 'تحميل صورة',
'image_intro' => 'هنا يمكنك تحديد وإدارة الصور التي تم تحميلها مسبقًا إلى النظام.',
'image_intro_upload' => 'تحميل صورة جديدة عن طريق سحب الصورة إلى هذه النافذة، أو باستخدام زر "تحميل صورة" أعلاه.',
'image_all' => 'الكل',
'image_all_title' => 'عرض جميع الصور',
'image_book_title' => 'عرض الصور المرفوعة لهذا الكتاب',
'image_page_title' => 'عرض الصور المرفوعة لهذه الصفحة',
'image_search_hint' => 'البحث باستخدام اسم الصورة',
'image_uploaded' => 'وقت الرفع :uploadedDate',
'image_uploaded_by' => 'Uploaded by :userName',
'image_uploaded_to' => 'Uploaded to :pageLink',
'image_updated' => 'Updated :updateDate',
'image_uploaded_by' => 'تم تحميلها من قبل :userName',
'image_uploaded_to' => 'تم رفعها إلى :pageLink',
'image_updated' => 'تم تحديثها :updatedate',
'image_load_more' => 'المزيد',
'image_image_name' => 'اسم الصورة',
'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',
'image_select_image' => 'تحديد الصورة',
'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
'image_dropzone_drop' => 'Drop images here to upload',
'image_dropzone_drop' => 'إسقاط صورة أو اضغط هنا للرفع',
'images_deleted' => 'تم حذف الصور',
'image_preview' => 'معاينة الصور',
'image_upload_success' => 'تم رفع الصورة بنجاح',
'image_update_success' => 'تم تحديث تفاصيل الصورة بنجاح',
'image_delete_success' => 'تم حذف الصورة بنجاح',
'image_replace' => 'Replace Image',
'image_replace_success' => 'Image file successfully updated',
'image_rebuild_thumbs' => 'Regenerate Size Variations',
'image_rebuild_thumbs_success' => 'Image size variations successfully rebuilt!',
'image_replace' => 'استبدال صورة',
'image_replace_success' => 'تم تحديث الصورة بنجاح',
'image_rebuild_thumbs' => 'تجديد تغيرات الحجم',
'image_rebuild_thumbs_success' => 'تم إعادة بناء تغيرات حجم الصورة بنجاح!',
// Code Editor
'code_editor' => 'تعديل الشفرة',

View File

@@ -13,7 +13,7 @@ return [
'cancel' => 'إلغاء',
'save' => 'حفظ',
'close' => 'إغلاق',
'apply' => 'Apply',
'apply' => 'تطبيق',
'undo' => 'تراجع',
'redo' => 'إعادة التنفيذ',
'left' => 'يسار',
@@ -25,7 +25,7 @@ return [
'width' => 'العرض',
'height' => 'الارتفاع',
'More' => 'المزيد',
'select' => 'Select...',
'select' => 'إختار...',
// Toolbar
'formats' => 'التنسيقات',
@@ -48,79 +48,80 @@ return [
'superscript' => 'نص مرتفع',
'subscript' => 'نص منخفض',
'text_color' => 'لون النص',
'highlight_color' => 'لون التمييز',
'custom_color' => 'لون مخصص',
'remove_color' => 'إزالة اللون',
'background_color' => 'لون الخلفية',
'align_left' => 'محاذاة لليسار',
'align_center' => 'محاذاة بالمنتصف',
'align_right' => 'مُحاذاة لليمين',
'align_justify' => 'Justify',
'align_justify' => 'المحاذاة',
'list_bullet' => 'قائمة نقاط',
'list_numbered' => 'قائمة مرقمة',
'list_task' => 'Task list',
'list_task' => 'قائمة المهام',
'indent_increase' => 'زيادة البادئة',
'indent_decrease' => 'إنقاص البادئة',
'table' => 'جدول',
'insert_image' => 'ادراج صورة',
'insert_image_title' => 'Insert/Edit Image',
'insert_link' => 'Insert/edit link',
'insert_link_title' => 'Insert/Edit Link',
'insert_horizontal_line' => 'Insert horizontal line',
'insert_code_block' => 'Insert code block',
'edit_code_block' => 'Edit code block',
'insert_drawing' => 'Insert/edit drawing',
'drawing_manager' => 'Drawing manager',
'insert_media' => 'Insert/edit media',
'insert_media_title' => 'Insert/Edit Media',
'clear_formatting' => 'Clear formatting',
'source_code' => 'Source code',
'source_code_title' => 'Source Code',
'fullscreen' => 'Fullscreen',
'image_options' => 'Image options',
'insert_image_title' => 'إضافة/تحرير الصورة',
'insert_link' => 'إضافة/تعديل الرابط',
'insert_link_title' => 'إضافة/تحرير الرابط',
'insert_horizontal_line' => 'إضافة خط أفقي',
'insert_code_block' => 'إضافة مربع رموز برمجية',
'edit_code_block' => 'تعديل مربع الرموز البرمجية',
'insert_drawing' => 'إضافة/تعديل الرسم',
'drawing_manager' => 'إدارة الرسم',
'insert_media' => 'إضافة/تحرير الوسائط',
'insert_media_title' => 'إضافة/تحرير الوسائط',
'clear_formatting' => 'مسح التنسيق',
'source_code' => 'الرمز البرمجي',
'source_code_title' => 'الرمز البرمجي',
'fullscreen' => 'شاشة كاملة',
'image_options' => 'خيارات الصورة',
// Tables
'table_properties' => 'Table properties',
'table_properties_title' => 'Table Properties',
'delete_table' => 'Delete table',
'table_clear_formatting' => 'Clear table formatting',
'resize_to_contents' => 'Resize to contents',
'row_header' => 'Row header',
'insert_row_before' => 'Insert row before',
'insert_row_after' => 'Insert row after',
'delete_row' => 'Delete row',
'insert_column_before' => 'Insert column before',
'insert_column_after' => 'Insert column after',
'delete_column' => 'Delete column',
'table_cell' => 'Cell',
'table_row' => 'Row',
'table_column' => 'Column',
'cell_properties' => 'Cell properties',
'cell_properties_title' => 'Cell Properties',
'cell_type' => 'Cell type',
'cell_type_cell' => 'Cell',
'cell_scope' => 'Scope',
'cell_type_header' => 'Header cell',
'merge_cells' => 'Merge cells',
'split_cell' => 'Split cell',
'table_row_group' => 'Row Group',
'table_column_group' => 'Column Group',
'horizontal_align' => 'Horizontal align',
'vertical_align' => 'Vertical align',
'border_width' => 'Border width',
'border_style' => 'Border style',
'border_color' => 'Border color',
'row_properties' => 'Row properties',
'row_properties_title' => 'Row Properties',
'cut_row' => 'Cut row',
'copy_row' => 'Copy row',
'paste_row_before' => 'Paste row before',
'paste_row_after' => 'Paste row after',
'row_type' => 'Row type',
'row_type_header' => 'Header',
'row_type_body' => 'Body',
'row_type_footer' => 'Footer',
'alignment' => 'Alignment',
'cut_column' => 'Cut column',
'table_properties' => 'خصائص الجدول',
'table_properties_title' => 'خصائص الجدول',
'delete_table' => 'حذف الجدول',
'table_clear_formatting' => 'مسح تنسيق الجدول',
'resize_to_contents' => 'تغيير الحجم إلى المحتوى',
'row_header' => 'رأس الصف',
'insert_row_before' => 'إضافة صف قبل',
'insert_row_after' => 'إضافة صف بعد',
'delete_row' => 'حذف الصف',
'insert_column_before' => 'إدراج عمود قبل',
'insert_column_after' => 'إدراج عمود بعد',
'delete_column' => 'حذف عمود',
'table_cell' => 'خلية',
'table_row' => 'صف',
'table_column' => 'عمود',
'cell_properties' => 'خصائص الخلية',
'cell_properties_title' => 'خصائص الخلية',
'cell_type' => 'نوع الخلية',
'cell_type_cell' => 'الخلية',
'cell_scope' => 'النِطَاق',
'cell_type_header' => 'عنوان الخلية',
'merge_cells' => 'دمج الخلايا',
'split_cell' => 'خلية منقسمة',
'table_row_group' => 'مجموعة الصفوف',
'table_column_group' => 'مجموعة الأعمدة',
'horizontal_align' => 'محاذاة أفقية',
'vertical_align' => 'محاذاة عمودية',
'border_width' => 'عرض الحدود',
'border_style' => 'نمط الحدود',
'border_color' => 'لون الحدود',
'row_properties' => 'خصائص الصف',
'row_properties_title' => 'خصائص الصف',
'cut_row' => 'فص الصف',
'copy_row' => 'نسخ الصف',
'paste_row_before' => 'لصق الصف قبل',
'paste_row_after' => 'لصق الصف بعد',
'row_type' => 'نوع الصف',
'row_type_header' => 'العنوان',
'row_type_body' => 'المحتوى ',
'row_type_footer' => 'تذييل',
'alignment' => 'المحاذاة',
'cut_column' => 'قص العمود',
'copy_column' => 'نسخ العمود',
'paste_column_before' => 'لصق عمود قبل',
'paste_column_after' => 'لصق عمود بعد',
@@ -128,54 +129,54 @@ return [
'cell_spacing' => 'تباعد الخلايا',
'caption' => 'الوصف',
'show_caption' => 'إظهار الوصف',
'constrain' => 'Constrain proportions',
'cell_border_solid' => 'Solid',
'cell_border_dotted' => 'Dotted',
'cell_border_dashed' => 'Dashed',
'cell_border_double' => 'Double',
'cell_border_groove' => 'Groove',
'cell_border_ridge' => 'Ridge',
'cell_border_inset' => 'Inset',
'cell_border_outset' => 'Outset',
'cell_border_none' => 'None',
'cell_border_hidden' => 'Hidden',
'constrain' => 'تقييد النسب',
'cell_border_solid' => 'لون كامل',
'cell_border_dotted' => 'مُنَقط',
'cell_border_dashed' => 'متقطع',
'cell_border_double' => 'مزدوج',
'cell_border_groove' => 'أخدود',
'cell_border_ridge' => 'الحافَة',
'cell_border_inset' => 'الداخلية',
'cell_border_outset' => 'الخارجية',
'cell_border_none' => 'لا شَيْء',
'cell_border_hidden' => 'مخفي',
// Images, links, details/summary & embed
'source' => 'Source',
'alt_desc' => 'Alternative description',
'embed' => 'Embed',
'paste_embed' => 'Paste your embed code below:',
'url' => 'URL',
'text_to_display' => 'Text to display',
'title' => 'Title',
'browse_links' => 'Browse links',
'open_link' => 'Open link',
'open_link_in' => 'Open link in...',
'open_link_current' => 'Current window',
'open_link_new' => 'New window',
'remove_link' => 'Remove link',
'insert_collapsible' => 'Insert collapsible block',
'collapsible_unwrap' => 'Unwrap',
'edit_label' => 'Edit label',
'toggle_open_closed' => 'Toggle open/closed',
'collapsible_edit' => 'Edit collapsible block',
'toggle_label' => 'Toggle label',
'source' => 'المصدر',
'alt_desc' => 'وصف بديل',
'embed' => 'تضمين',
'paste_embed' => 'قم بلصق الرموز المصدرية المضمنة الخاص بك أدناه:',
'url' => 'الرابط',
'text_to_display' => 'النص المراد عرضه',
'title' => 'العنوان',
'browse_links' => 'تصفح الروابط',
'open_link' => 'افتح الرابط',
'open_link_in' => 'افتح الرابط في...',
'open_link_current' => 'النافذة الحالية',
'open_link_new' => 'نافذة جديدة',
'remove_link' => 'إزالة الرابط',
'insert_collapsible' => 'أدخل كتلة قابلة للطي',
'collapsible_unwrap' => 'بسط',
'edit_label' => 'عدل الوصف',
'toggle_open_closed' => 'التبديل بين الفتح والإغلاق',
'collapsible_edit' => 'تحرير الكتلة القابلة للطي',
'toggle_label' => 'تبديل التسمية',
// About view
'about' => 'About the editor',
'about_title' => 'About the WYSIWYG Editor',
'editor_license' => 'Editor License & Copyright',
'editor_lexical_license' => 'This editor is built as a fork of :lexicalLink which is distributed under the MIT license.',
'editor_lexical_license_link' => 'Full license details can be found here.',
'editor_tiny_license' => 'This editor is built using :tinyLink which is provided under the MIT license.',
'editor_tiny_license_link' => 'The copyright and license details of TinyMCE can be found here.',
'save_continue' => 'Save Page & Continue',
'callouts_cycle' => '(Keep pressing to toggle through types)',
'link_selector' => 'Link to content',
'shortcuts' => 'Shortcuts',
'shortcut' => 'Shortcut',
'shortcuts_intro' => 'The following shortcuts are available in the editor:',
'windows_linux' => '(Windows/Linux)',
'mac' => '(Mac)',
'description' => 'Description',
'about' => 'عن المحرر',
'about_title' => 'حول محرر ما تراه هو ما تحصل عليه -WYSIWYG-',
'editor_license' => 'رخصة المحرر وحقوق التأليف والنشر',
'editor_lexical_license' => 'تم إنشاء هذا المحرر باعتباره فرعًا لـ :lexicalLink الذي يتم توزيعه بموجب ترخيص معهد ماساتشوستس للتقانة -MIT-.',
'editor_lexical_license_link' => 'يمكنك العثور على تفاصيل الترخيص الكاملة هنا.',
'editor_tiny_license' => 'تم إنشاء هذا المحرر باستخدام :tinyLink والذي يتم توفيره بموجب ترخيص معهد ماساتشوستس للتقانة -MIT-.',
'editor_tiny_license_link' => 'يمكن الاطلاع هنا على تفاصيل حقوق التأليف والنشر والترخيص الخاصة بتاینی‌ام‌سی‌ای -TinyMCE-.',
'save_continue' => 'حفظ الصفحة ومتابعة',
'callouts_cycle' => '(استمر في الضغط للتبديل بين الأنواع)',
'link_selector' => 'رابط للمحتوى',
'shortcuts' => 'الاختصارات',
'shortcut' => 'الاختصار',
'shortcuts_intro' => 'الاختصارات التالية متاحة في المحرر:',
'windows_linux' => '(ويندوز/لينكس)',
'mac' => '(ماك)',
'description' => 'الوصف',
];

View File

@@ -22,15 +22,15 @@ return [
'meta_created_name' => 'أنشئ :timeLength بواسطة :user',
'meta_updated' => 'مُحدث :timeLength',
'meta_updated_name' => 'مُحدث :timeLength بواسطة :user',
'meta_owned_name' => 'Owned by :user',
'meta_reference_count' => 'Referenced by :count item|Referenced by :count items',
'meta_owned_name' => 'مملوكة لـ:user',
'meta_reference_count' => 'مشار إليه :count مرة|مشار إليه :count مرة',
'entity_select' => 'اختيار الكيان',
'entity_select_lack_permission' => 'You don\'t have the required permissions to select this item',
'entity_select_lack_permission' => 'ليس لديك الصلاحيات المطلوبة لتحديد هذا العنصر',
'images' => 'صور',
'my_recent_drafts' => 'مسوداتي الحديثة',
'my_recently_viewed' => 'ما عرضته مؤخراً',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'my_most_viewed_favourites' => 'مفضلاتي الأكثر مشاهدة',
'my_favourites' => 'مفضلاتي',
'no_pages_viewed' => 'لم تستعرض أي صفحات',
'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',
'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',
@@ -38,43 +38,47 @@ return [
'export_html' => 'صفحة ويب',
'export_pdf' => 'ملف PDF',
'export_text' => 'ملف نص عادي',
'export_md' => 'Markdown File',
'export_zip' => 'Portable ZIP',
'default_template' => 'Default Page Template',
'default_template_explain' => 'Assign a page template that will be used as the default content for all pages created within this item. Keep in mind this will only be used if the page creator has view access to the chosen template page.',
'default_template_select' => 'Select a template page',
'import' => 'Import',
'import_validate' => 'Validate Import',
'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.',
'import_zip_select' => 'Select ZIP file to upload',
'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',
'import_pending' => 'Pending Imports',
'import_pending_none' => 'No imports have been started.',
'import_continue' => 'Continue Import',
'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',
'import_details' => 'Import Details',
'import_run' => 'Run Import',
'import_size' => ':size Import ZIP Size',
'import_uploaded_at' => 'Uploaded :relativeTime',
'import_uploaded_by' => 'Uploaded by',
'import_location' => 'Import Location',
'import_location_desc' => 'Select a target location for your imported content. You\'ll need the relevant permissions to create within the location you choose.',
'import_delete_confirm' => 'Are you sure you want to delete this import?',
'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',
'import_errors' => 'Import Errors',
'import_errors_desc' => 'The follow errors occurred during the import attempt:',
'export_md' => 'ملف ماركداون -Markdown-',
'export_zip' => 'ملف مضغوط -ZIP-',
'default_template' => 'قالب الصفحة الافتراضية',
'default_template_explain' => 'قم بتعيين قالب صفحة سيتم استخدامه كمحتوى افتراضي لجميع الصفحات التي تم إنشاؤها ضمن هذا العنصر. ضع في اعتبارك أن هذا لن يتم استخدامه إلا إذا كان لدى منشئ الصفحة حق الوصول إلى صفحة القالب المختارة.',
'default_template_select' => 'حدد صفحة القالب',
'import' => 'استيراد',
'import_validate' => 'التحقق من صحة الاستيراد',
'import_desc' => 'استيراد الكتب والفصول والصفحات باستخدام تصدير مِلَفّ مضغوط ZIP محمول من نفس النظام أو نظام مختلف. حدد مِلَفّ ZIP للمتابعة. بعد تحميل المِلَفّ والتحقق من صحته، ستتمكن من إعداد وتأكيد الاستيراد في العرض التالي.',
'import_zip_select' => 'حدد مِلَفّ مضغوط بصيغة ZIP للتحميل',
'import_zip_validation_errors' => 'تم اكتشاف أخطاء في أثناء التحقق من صحة المِلَفّ المضغوط ZIP المقدم:',
'import_pending' => 'الاستيرادات المعلقة',
'import_pending_none' => 'لم يتم البَدْء في أي عملية استيراد.',
'import_continue' => 'متابعة الاستيراد',
'import_continue_desc' => 'راجع المحتوى الذي يجب استيراده من المِلَفّ المضغوط ZIP الذي تم تحميله. عندما يكون جاهزًا، تشتغل عملية الاستيراد لإضافة محتوياته إلى هذا النظام. سيتم إزالة مِلَفّ الاستيراد الذي تم تحميله تلقائيًا عند الاستيراد الناجح.',
'import_details' => 'تفاصيل الاستيراد',
'import_run' => 'تشغيل الاستيراد',
'import_size' => 'حجم الاستيراد :size ',
'import_uploaded_at' => 'تم تحميلة في :relativeTime',
'import_uploaded_by' => 'رُفِع بواسطة',
'import_location' => 'موقع الاستيراد',
'import_location_desc' => 'حدد موقعًا مستهدفًا للمحتوى المستورد. ستحتاج إلى الصلاحيات ذات الصلة لإنشاء المحتوى داخل الموقع الذي تختاره.',
'import_delete_confirm' => 'متيقِّن من أنك تريد حذف الاستيراد؟',
'import_delete_desc' => 'سيؤدي هذا إلى حذف مِلَفّ الاستيراد المضغوط ZIP، ولا يمكن التراجع عنه.',
'import_errors' => 'أخطاء الاستيراد',
'import_errors_desc' => 'حدثت الأخطاء التالية خلال محاولة الاستيراد:',
'breadcrumb_siblings_for_page' => 'Navigate siblings for page',
'breadcrumb_siblings_for_chapter' => 'Navigate siblings for chapter',
'breadcrumb_siblings_for_book' => 'Navigate siblings for book',
'breadcrumb_siblings_for_bookshelf' => 'Navigate siblings for shelf',
// Permissions and restrictions
'permissions' => 'الأذونات',
'permissions_desc' => 'Set permissions here to override the default permissions provided by user roles.',
'permissions_book_cascade' => 'Permissions set on books will automatically cascade to child chapters and pages, unless they have their own permissions defined.',
'permissions_chapter_cascade' => 'Permissions set on chapters will automatically cascade to child pages, unless they have their own permissions defined.',
'permissions_desc' => 'تعيين الصلاحيات هنا لتجاوز الصلاحيات الافتراضية التي توفرها أدوار المستخدم.',
'permissions_book_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها للكتب تلقائيًا إلى الفصول والصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',
'permissions_chapter_cascade' => 'سيتم نقل الصلاحيات التي تم تعيينها على الفصول تلقائيًا إلى الصفحات الفرعية، ما لم تكن لديها صلاحيات خاصة بها محددة.',
'permissions_save' => 'حفظ الأذونات',
'permissions_owner' => 'Owner',
'permissions_role_everyone_else' => 'Everyone Else',
'permissions_role_everyone_else_desc' => 'Set permissions for all roles not specifically overridden.',
'permissions_role_override' => 'Override permissions for role',
'permissions_inherit_defaults' => 'Inherit defaults',
'permissions_owner' => 'المالك',
'permissions_role_everyone_else' => 'الآخرين',
'permissions_role_everyone_else_desc' => 'تعيين الصلاحيات لجميع الأدوار التي لم يتم تجاوزها على وجه التحديد.',
'permissions_role_override' => 'تجاوز الصلاحيات للدور',
'permissions_inherit_defaults' => 'وراثة الإعدادات الافتراضية',
// Search
'search_results' => 'نتائج البحث',
@@ -94,7 +98,7 @@ return [
'search_permissions_set' => 'حزمة الأذونات',
'search_created_by_me' => 'أنشئت بواسطتي',
'search_updated_by_me' => 'حُدثت بواسطتي',
'search_owned_by_me' => 'Owned by me',
'search_owned_by_me' => 'مملوكة لي',
'search_date_options' => 'خيارات التاريخ',
'search_updated_before' => 'حدثت قبل',
'search_updated_after' => 'حدثت بعد',
@@ -117,24 +121,24 @@ return [
'shelves_save' => 'حفظ الرف',
'shelves_books' => 'كتب على هذا الرف',
'shelves_add_books' => 'إضافة كتب لهذا الرف',
'shelves_drag_books' => 'Drag books below to add them to this shelf',
'shelves_drag_books' => 'اسحب الكتب الموجودة بالأسفل لإضافتها إلى هذا الرف',
'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
'shelves_edit_named' => 'Edit Shelf :name',
'shelves_edit' => 'Edit Shelf',
'shelves_delete' => 'Delete Shelf',
'shelves_delete_named' => 'Delete Shelf :name',
'shelves_delete_explain' => "This will delete the shelf with the name ':name'. Contained books will not be deleted.",
'shelves_delete_confirmation' => 'Are you sure you want to delete this shelf?',
'shelves_permissions' => 'Shelf Permissions',
'shelves_permissions_updated' => 'Shelf Permissions Updated',
'shelves_permissions_active' => 'Shelf Permissions Active',
'shelves_permissions_cascade_warning' => 'Permissions on shelves do not automatically cascade to contained books. This is because a book can exist on multiple shelves. Permissions can however be copied down to child books using the option found below.',
'shelves_permissions_create' => 'Shelf create permissions are only used for copying permissions to child books using the action below. They do not control the ability to create books.',
'shelves_edit_named' => 'تعديل الرف :name',
'shelves_edit' => 'تعديل الرف',
'shelves_delete' => 'حذف الرف',
'shelves_delete_named' => 'حذف الرف :name',
'shelves_delete_explain' => "سيؤدي هذا إلى حذف الرف الذي يحمل الاسم ':name'. لن يتم حذف الكتب المضمنة بداخله.",
'shelves_delete_confirmation' => 'هل أنت متأكد أنك تريد حذف هذا الرف؟',
'shelves_permissions' => 'صلاحيات الرف',
'shelves_permissions_updated' => 'تم تحديث صلاحيات الرف',
'shelves_permissions_active' => 'صلاحيات الرف نشطة',
'shelves_permissions_cascade_warning' => 'لا يتم نقل الصلاحيات الموجودة على الأرفف تلقائيًا إلى الكتب الموجودة في كل رف. وذلك لأن الكتاب يمكن أن يوجد على أرفف متعددة. ومع ذلك، يمكن نسخ الصلاحيات إلى الكتب الفرعية باستخدام الخِيار الموجود أدناه.',
'shelves_permissions_create' => 'تُستخدم صلاحيات إنشاء الرفوف فقط لنسخ الصلاحيات إلى الكتب الفرعية باستخدام الإجراء أدناه. ولا تتحكم في القدرة على إنشاء الكتب.',
'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
'shelves_copy_permissions' => 'نسخ الأذونات',
'shelves_copy_permissions_explain' => 'This will apply the current permission settings of this shelf to all books contained within. Before activating, ensure any changes to the permissions of this shelf have been saved.',
'shelves_copy_permission_success' => 'Shelf permissions copied to :count books',
'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الصلاحيات الحالية لهذا الرف على جميع الكتب الموجودة بداخله. قبل التنشيط، تأكد من حفظ أي تغييرات على صلاحيات هذا الرف.',
'shelves_copy_permission_success' => 'تم نسخ صلاحيات الرف إلى :count كتاب/كتب',
// Books
'book' => 'كتاب',
@@ -166,9 +170,9 @@ return [
'books_search_this' => 'البحث في هذا الكتاب',
'books_navigation' => 'تصفح الكتاب',
'books_sort' => 'فرز محتويات الكتاب',
'books_sort_desc' => 'Move chapters and pages within a book to reorganise its contents. Other books can be added which allows easy moving of chapters and pages between books. Optionally an auto sort rule can be set to automatically sort this book\'s contents upon changes.',
'books_sort_auto_sort' => 'Auto Sort Option',
'books_sort_auto_sort_active' => 'Auto Sort Active: :sortName',
'books_sort_desc' => 'نقل الفصول والصفحات داخل الكتاب لإعادة تنظيم محتوياته. يمكن إضافة كتب أخرى مما يسمح بنقل الفصول والصفحات بسهولة بين الكتب. اختياريًا، يمكن تعيين قاعدة فرز تلقائي لفرز محتويات هذا الكتاب تلقائيًا عند حدوث تغييرات.',
'books_sort_auto_sort' => 'خِيار الفرز التلقائي',
'books_sort_auto_sort_active' => 'الفرز التلقائي الشَغَّال: :sortName',
'books_sort_named' => 'فرز كتاب :bookName',
'books_sort_name' => 'ترتيب حسب الإسم',
'books_sort_created' => 'ترتيب حسب تاريخ الإنشاء',
@@ -177,19 +181,19 @@ return [
'books_sort_chapters_last' => 'الفصول الأخيرة',
'books_sort_show_other' => 'عرض كتب أخرى',
'books_sort_save' => 'حفظ الترتيب الجديد',
'books_sort_show_other_desc' => 'Add other books here to include them in the sort operation, and allow easy cross-book reorganisation.',
'books_sort_move_up' => 'Move Up',
'books_sort_move_down' => 'Move Down',
'books_sort_move_prev_book' => 'Move to Previous Book',
'books_sort_move_next_book' => 'Move to Next Book',
'books_sort_move_prev_chapter' => 'Move Into Previous Chapter',
'books_sort_move_next_chapter' => 'Move Into Next Chapter',
'books_sort_move_book_start' => 'Move to Start of Book',
'books_sort_move_book_end' => 'Move to End of Book',
'books_sort_move_before_chapter' => 'Move to Before Chapter',
'books_sort_move_after_chapter' => 'Move to After Chapter',
'books_copy' => 'Copy Book',
'books_copy_success' => 'Book successfully copied',
'books_sort_show_other_desc' => 'أضف كتبًا أخرى هنا لتضمينها في عملية الفرز، والسماح بإعادة تنظيم الكتب بسهولة.',
'books_sort_move_up' => 'حرك للأعلى',
'books_sort_move_down' => 'حرك للأسفل',
'books_sort_move_prev_book' => 'نقل للكتاب السابق',
'books_sort_move_next_book' => 'نقل للكتاب التالي',
'books_sort_move_prev_chapter' => 'نقل إلى الفصل السابق',
'books_sort_move_next_chapter' => 'نقل إلى الفصل التالي',
'books_sort_move_book_start' => 'نقل إلى بداية الكتاب',
'books_sort_move_book_end' => 'نقل إلى نهاية الكتاب',
'books_sort_move_before_chapter' => 'نقل إلى الفصل السابق',
'books_sort_move_after_chapter' => 'نقل إلى الفصل التالي',
'books_copy' => 'نسخة الكتاب',
'books_copy_success' => 'تم نسخ الكتاب بنجاح',
// Chapters
'chapter' => 'فصل',
@@ -200,21 +204,21 @@ return [
'chapters_create' => 'إنشاء فصل جديد',
'chapters_delete' => 'حذف الفصل',
'chapters_delete_named' => 'حذف فصل :chapterName',
'chapters_delete_explain' => 'This will delete the chapter with the name \':chapterName\'. All pages that exist within this chapter will also be deleted.',
'chapters_delete_explain' => 'سيؤدي هذا إلى حذف الفصل الذي يحمل الاسم \':chapterName\'. كما سيتم حذف جميع الصفحات الموجودة داخل هذا الفصل.',
'chapters_delete_confirm' => 'تأكيد حذف الفصل؟',
'chapters_edit' => 'تعديل الفصل',
'chapters_edit_named' => 'تعديل فصل :chapterName',
'chapters_save' => 'حفظ الفصل',
'chapters_move' => 'نقل الفصل',
'chapters_move_named' => 'نقل فصل :chapterName',
'chapters_copy' => 'Copy Chapter',
'chapters_copy_success' => 'Chapter successfully copied',
'chapters_copy' => 'نسخ الفصل',
'chapters_copy_success' => 'تم نسخ الفصل بنجاح',
'chapters_permissions' => 'أذونات الفصل',
'chapters_empty' => 'لا توجد أي صفحات في هذا الفصل حالياً',
'chapters_permissions_active' => 'أذونات الفصل مفعلة',
'chapters_permissions_success' => 'تم تحديث أذونات الفصل',
'chapters_search_this' => 'البحث في هذا الفصل',
'chapter_sort_book' => 'Sort Book',
'chapter_sort_book' => 'فرز الكتاب',
// Pages
'page' => 'صفحة',
@@ -230,7 +234,7 @@ return [
'pages_delete_draft' => 'حذف المسودة',
'pages_delete_success' => 'تم حذف الصفحة',
'pages_delete_draft_success' => 'تم حذف المسودة',
'pages_delete_warning_template' => 'This page is in active use as a book or chapter default page template. These books or chapters will no longer have a default page template assigned after this page is deleted.',
'pages_delete_warning_template' => 'هذه الصفحة قيد الاستخدام كقالب افتراضي لصفحات الكتب أو الفصول. لن يكون لهذه الكتب أو الفصول قالب افتراضي بعد حذفها.',
'pages_delete_confirm' => 'تأكيد حذف الصفحة؟',
'pages_delete_draft_confirm' => 'تأكيد حذف المسودة؟',
'pages_editing_named' => ':pageName قيد التعديل',
@@ -241,23 +245,23 @@ return [
'pages_editing_page' => 'الصفحة قيد التعديل',
'pages_edit_draft_save_at' => 'تم خفظ المسودة في ',
'pages_edit_delete_draft' => 'حذف المسودة',
'pages_edit_delete_draft_confirm' => 'Are you sure you want to delete your draft page changes? All of your changes, since the last full save, will be lost and the editor will be updated with the latest page non-draft save state.',
'pages_edit_delete_draft_confirm' => 'متيقِّن من رغبتك في حذف تغييرات صفحة المُسَوَّدَة؟ ستُفقد جميع تغييراتك، منذ آخر حفظ كامل، وسيتم تحديث المحرر بأحدث حالة حفظ للصفحة (غير مسودة).',
'pages_edit_discard_draft' => 'التخلص من المسودة',
'pages_edit_switch_to_markdown' => 'Switch to Markdown Editor',
'pages_edit_switch_to_markdown_clean' => '(Clean Content)',
'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_markdown' => 'التبديل إلى محرر ماركداون -Markdown-',
'pages_edit_switch_to_markdown_clean' => '(محتوى نظيف)',
'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_set_changelog' => 'تثبيت سجل التعديل',
'pages_edit_enter_changelog_desc' => 'ضع وصف مختصر للتعديلات التي تمت',
'pages_edit_enter_changelog' => 'أدخل سجل التعديل',
'pages_editor_switch_title' => 'Switch Editor',
'pages_editor_switch_are_you_sure' => 'Are you sure you want to change the editor for this page?',
'pages_editor_switch_consider_following' => 'Consider the following when changing editors:',
'pages_editor_switch_consideration_a' => 'Once saved, the new editor option will be used by any future editors, including those that may not be able to change editor type themselves.',
'pages_editor_switch_consideration_b' => 'This can potentially lead to a loss of detail and syntax in certain circumstances.',
'pages_editor_switch_consideration_c' => 'Tag or changelog changes, made since last save, won\'t persist across this change.',
'pages_editor_switch_title' => 'تبديل المحرر',
'pages_editor_switch_are_you_sure' => 'متيقِّن أنك تريد تغيير المحرر لهذه الصفحة؟',
'pages_editor_switch_consider_following' => 'عند تغيير المحررين، ضع في اعتبارك ما يلي:',
'pages_editor_switch_consideration_a' => 'بمجرد الحفظ، سيتم استخدام خِيار المحرر الجديد بواسطة أي محررين مستقبليين، بما في ذلك أولئك الذين قد لا يتمكنون من تغيير نوع المحرر بأنفسهم.',
'pages_editor_switch_consideration_b' => 'من الممكن أن يؤدي هذا إلى فقدان التفاصيل والنحو في ظروف معينة.',
'pages_editor_switch_consideration_c' => 'لن تستمر تغييرات العلامة أو سجل التغييرات، التي تم إجراؤها منذ الحفظ الأخير، عبر هذا التغيير.',
'pages_save' => 'حفظ الصفحة',
'pages_title' => 'عنوان الصفحة',
'pages_name' => 'اسم الصفحة',
@@ -266,10 +270,11 @@ return [
'pages_md_insert_image' => 'إدخال صورة',
'pages_md_insert_link' => 'إدراج ارتباط الكيان',
'pages_md_insert_drawing' => 'إدخال رسمة',
'pages_md_show_preview' => 'Show preview',
'pages_md_sync_scroll' => 'Sync preview scroll',
'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_md_show_preview' => 'عرض المعاينة',
'pages_md_sync_scroll' => 'مزامنة معاينة التمرير',
'pages_md_plain_editor' => 'محرر النصوص العادي',
'pages_drawing_unsaved' => 'تم العثور على رسم غير محفوظ',
'pages_drawing_unsaved_confirm' => 'تم العثور على بيانات رسم غير محفوظة من محاولة حفظ رسم سابقة فاشلة. هل ترغب في استعادة هذا الرسم غير المحفوظ ومواصلة تحريره؟',
'pages_not_in_chapter' => 'صفحة ليست في فصل',
'pages_move' => 'نقل الصفحة',
'pages_copy' => 'نسخ الصفحة',
@@ -279,17 +284,17 @@ return [
'pages_permissions_success' => 'تم تحديث أذونات الصفحة',
'pages_revision' => 'مراجعة',
'pages_revisions' => 'مراجعات الصفحة',
'pages_revisions_desc' => 'Listed below are all the past revisions of this page. You can look back upon, compare, and restore old page versions if permissions allow. The full history of the page may not be fully reflected here since, depending on system configuration, old revisions could be auto-deleted.',
'pages_revisions_desc' => 'تجد أدناه جميع الإصدارات السابقة لهذه الصفحة. يمكنك الاطلاع عليها ومقارنتها واستعادة الإصدارات القديمة إذا سمحت الصلاحيات بذلك. قد لا يظهر تاريخ الصفحة بالكامل هنا، إذ قد تُحذف الإصدارات القديمة تلقائيًا، وذلك حسب إعدادات النظام.',
'pages_revisions_named' => 'مراجعات صفحة :pageName',
'pages_revision_named' => 'مراجعة صفحة :pageName',
'pages_revision_restored_from' => 'Restored from #:id; :summary',
'pages_revision_restored_from' => 'تم الاستعادة من #:id; :summary',
'pages_revisions_created_by' => 'أنشئ بواسطة',
'pages_revisions_date' => 'تاريخ المراجعة',
'pages_revisions_number' => '#',
'pages_revisions_sort_number' => 'Revision Number',
'pages_revisions_sort_number' => 'رَقْم المراجعة',
'pages_revisions_numbered' => 'مراجعة #:id',
'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
'pages_revisions_editor' => 'Editor Type',
'pages_revisions_editor' => 'نوع المحرر',
'pages_revisions_changelog' => 'سجل التعديل',
'pages_revisions_changes' => 'التعديلات',
'pages_revisions_current' => 'النسخة الحالية',
@@ -297,20 +302,20 @@ return [
'pages_revisions_restore' => 'استرجاع',
'pages_revisions_none' => 'لا توجد مراجعات لهذه الصفحة',
'pages_copy_link' => 'نسخ الرابط',
'pages_edit_content_link' => 'Jump to section in editor',
'pages_pointer_enter_mode' => 'Enter section select mode',
'pages_pointer_label' => 'Page Section Options',
'pages_pointer_permalink' => 'Page Section Permalink',
'pages_pointer_include_tag' => 'Page Section Include Tag',
'pages_pointer_toggle_link' => 'Permalink mode, Press to show include tag',
'pages_pointer_toggle_include' => 'Include tag mode, Press to show permalink',
'pages_edit_content_link' => 'انتقل إلى القسم في المحرر',
'pages_pointer_enter_mode' => 'أدخل وضع اختيار القسم',
'pages_pointer_label' => 'خيارات قسم الصفحة',
'pages_pointer_permalink' => 'رابط دائم لقسم الصفحة',
'pages_pointer_include_tag' => 'قسم الصفحة يتضمن العلامة',
'pages_pointer_toggle_link' => 'وضع الرابط الدائم، اضغط لإظهار علامة التضمين',
'pages_pointer_toggle_include' => 'تضمين وضع العلامة، اضغط لإظهار الرابط الدائم',
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
'pages_initial_revision' => 'نشر مبدئي',
'pages_references_update_revision' => 'System auto-update of internal links',
'pages_references_update_revision' => 'التحديث التلقائي للنظام للروابط الداخلية',
'pages_initial_name' => 'صفحة جديدة',
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
'pages_draft_page_changed_since_creation' => 'This page has been updated since this draft was created. It is recommended that you discard this draft or take care not to overwrite any page changes.',
'pages_draft_page_changed_since_creation' => 'تم تحديث هذه الصفحة منذ إنشاء هذه المُسَوَّدَة. يُنصح بتجاهل هذه المُسَوَّدَة أو الحرص على عدم استبدال أي تغييرات في الصفحة.',
'pages_draft_edit_active' => [
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
'start_b' => ':userName بدأ بتعديل هذه الصفحة',
@@ -318,44 +323,44 @@ return [
'time_b' => 'في آخر :minCount دقيقة/دقائق',
'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',
],
'pages_draft_discarded' => 'Draft discarded! The editor has been updated with the current page content',
'pages_draft_deleted' => 'Draft deleted! The editor has been updated with the current page content',
'pages_draft_discarded' => 'تم رفض المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',
'pages_draft_deleted' => 'تم حذف المُسَوَّدَة! تم تحديث المحرر بمحتوى الصفحة الحالي.',
'pages_specific' => 'صفحة محددة',
'pages_is_template' => 'قالب الصفحة',
// Editor Sidebar
'toggle_sidebar' => 'Toggle Sidebar',
'toggle_sidebar' => 'تبديل الشريط الجانبي',
'page_tags' => 'وسوم الصفحة',
'chapter_tags' => 'وسوم الفصل',
'book_tags' => 'وسوم الكتاب',
'shelf_tags' => 'علامات الرف',
'tag' => 'وسم',
'tags' => 'وسوم',
'tags_index_desc' => 'Tags can be applied to content within the system to apply a flexible form of categorization. Tags can have both a key and value, with the value being optional. Once applied, content can then be queried using the tag name and value.',
'tags_index_desc' => 'يمكن تطبيق الوسوم على المحتوى داخل النظام لتطبيق تصنيف مرن. يمكن أن تحتوي الوسوم على مفتاح وقيمة، مع العلم أن القيمة اختيارية. بعد تطبيقها، يمكن الاستعلام عن المحتوى باستخدام اسم الوسم وقيمته.',
'tag_name' => 'اسم العلامة',
'tag_value' => 'قيمة الوسم (اختياري)',
'tags_explain' => "إضافة الوسوم تساعد بترتيب وتقسيم المحتوى. \n من الممكن وضع قيمة لكل وسم لترتيب أفضل وأدق.",
'tags_add' => 'إضافة وسم آخر',
'tags_remove' => 'إزالة هذه العلامة',
'tags_usages' => 'Total tag usages',
'tags_assigned_pages' => 'Assigned to Pages',
'tags_assigned_chapters' => 'Assigned to Chapters',
'tags_assigned_books' => 'Assigned to Books',
'tags_assigned_shelves' => 'Assigned to Shelves',
'tags_x_unique_values' => ':count unique values',
'tags_all_values' => 'All values',
'tags_view_tags' => 'View Tags',
'tags_view_existing_tags' => 'View existing tags',
'tags_list_empty_hint' => 'Tags can be assigned via the page editor sidebar or while editing the details of a book, chapter or shelf.',
'tags_usages' => 'إجمالي استخدامات العلامة',
'tags_assigned_pages' => 'مُخصصة للصفحات',
'tags_assigned_chapters' => 'مُخصصة للفصول',
'tags_assigned_books' => 'مُخصص للكتب',
'tags_assigned_shelves' => 'مُخصصة للأرفف',
'tags_x_unique_values' => 'قيم الفريدة :count',
'tags_all_values' => 'جميع القيم',
'tags_view_tags' => 'عرض العلامات',
'tags_view_existing_tags' => 'عرض العلامات الموجودة',
'tags_list_empty_hint' => 'يمكن تعيين العلامات بواسطة الشريط الجانبي لمحرر الصفحة أو خلال تحرير تفاصيل الكتاب أو الفصل أو الرف.',
'attachments' => 'المرفقات',
'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',
'attachments_upload' => 'رفع ملف',
'attachments_link' => 'إرفاق رابط',
'attachments_upload_drop' => 'Alternatively you can drag and drop a file here to upload it as an attachment.',
'attachments_upload_drop' => 'وبدلاً من ذلك، يمكنك سحب المِلَفّ وإفلاته هنا لتحميله كمرفق.',
'attachments_set_link' => 'تحديد الرابط',
'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
'attachments_dropzone' => 'Drop files here to upload',
'attachments_dropzone' => 'قم بإسقاط الملفات هنا للتحميل',
'attachments_no_files' => 'لم تُرفع أي ملفات',
'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
'attachments_link_name' => 'اسم الرابط',
@@ -392,19 +397,28 @@ 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',
'comment_updated' => 'تم التحديث :updateDiff بواسطة :username',
'comment_updated_indicator' => 'Updated',
'comment_updated_indicator' => 'تم التحديث',
'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_editor_explain' => 'Here are the comments that have been left on this page. Comments can be added & managed when viewing the saved page.',
'comment_reference' => 'المرجع',
'comment_reference_outdated' => '(قديمة)',
'comment_editor_explain' => 'هذه هي التعليقات المُضافة على هذه الصفحة. يُمكنك إضافة التعليقات وإدارتها عند عرض الصفحة المحفوظة.',
// Revision
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟',
@@ -412,51 +426,51 @@ return [
'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.',
// Copy view
'copy_consider' => 'Please consider the below when copying content.',
'copy_consider_permissions' => 'Custom permission settings will not be copied.',
'copy_consider_owner' => 'You will become the owner of all copied content.',
'copy_consider_images' => 'Page image files will not be duplicated & the original images will retain their relation to the page they were originally uploaded to.',
'copy_consider_attachments' => 'Page attachments will not be copied.',
'copy_consider_access' => 'A change of location, owner or permissions may result in this content being accessible to those previously without access.',
'copy_consider' => 'يرجى مراعاة ما يلي عند نسخ المحتوى.',
'copy_consider_permissions' => 'لن يتم نسخ إعدادات الصلاحيات المخصصة.',
'copy_consider_owner' => 'سوف تصبح مالكًا لجميع المحتوى المنسوخ.',
'copy_consider_images' => 'لن يتم تكرار ملفات صور الصفحة وستحتفظ الصور الأصلية بعلاقتها بالصفحة التي تم تحميلها إليها في الأصل.',
'copy_consider_attachments' => 'لن يتم نسخ مرفقات الصفحة.',
'copy_consider_access' => 'قد يؤدي تغيير الموقع أو المالك أو الصلاحيات إلى إمكانية وصول الأشخاص الذين لم يتمكنوا من الوصول إلى هذا المحتوى سابقًا.',
// Conversions
'convert_to_shelf' => 'Convert to Shelf',
'convert_to_shelf_contents_desc' => 'You can convert this book to a new shelf with the same contents. Chapters contained within this book will be converted to new books. If this book contains any pages, that are not in a chapter, this book will be renamed and contain such pages, and this book will become part of the new shelf.',
'convert_to_shelf_permissions_desc' => 'Any permissions set on this book will be copied to the new shelf and to all new child books that don\'t have their own permissions enforced. Note that permissions on shelves do not auto-cascade to content within, as they do for books.',
'convert_book' => 'Convert Book',
'convert_book_confirm' => 'Are you sure you want to convert this book?',
'convert_undo_warning' => 'This cannot be as easily undone.',
'convert_to_book' => 'Convert to Book',
'convert_to_book_desc' => 'You can convert this chapter to a new book with the same contents. Any permissions set on this chapter will be copied to the new book but any inherited permissions, from the parent book, will not be copied which could lead to a change of access control.',
'convert_chapter' => 'Convert Chapter',
'convert_chapter_confirm' => 'Are you sure you want to convert this chapter?',
'convert_to_shelf' => 'تحويل إلى رف',
'convert_to_shelf_contents_desc' => 'يمكنك تحويل هذا الكتاب إلى رف جديد بنفس المحتويات. سيتم تحويل الفصول الموجودة فيه إلى كتب جديدة. إذا احتوى هذا الكتاب على أي صفحات غير موجودة في أي فصل، فسيتم إعادة تسمية الكتاب وإضافة هذه الصفحات إليه، وسيصبح جزءًا من الرف الجديد.',
'convert_to_shelf_permissions_desc' => 'سيتم نسخ أي صلاحيات مُحددة لهذا الكتاب إلى الرف الجديد وإلى جميع الكتب الفرعية الجديدة التي لم تُطبّق عليها صلاحيات خاصة بها. يُرجى العلم بأن الصلاحيات على الرفوف لا تنتقل تلقائيًا إلى المحتوى داخلها، كما هو الحال مع الكتب.',
'convert_book' => 'تحويل الكتاب',
'convert_book_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الكتاب؟',
'convert_undo_warning' => 'لا يمكن التراجع عن هذا الأمر بسهولة.',
'convert_to_book' => 'تحويله إلى كتاب',
'convert_to_book_desc' => 'يمكنك تحويل هذا الفصل إلى كتاب جديد بنفس المحتوى. سيتم نسخ أي صلاحيات مُعيّنة لهذا الفصل إلى الكتاب الجديد، ولكن لن يتم نسخ أي صلاحيات موروثة من الكتاب الأصلي، مما قد يؤدي إلى تغيير في التحكم في الوصول.',
'convert_chapter' => 'تحويل الفصل',
'convert_chapter_confirm' => 'هل أنت متيقِّن أنك تريد تحويل هذا الفصل؟',
// References
'references' => 'References',
'references_none' => 'There are no tracked references to this item.',
'references_to_desc' => 'Listed below is all the known content in the system that links to this item.',
'references' => 'مراجع',
'references_none' => 'لا توجد مراجع متعقبة لهذا العنصر.',
'references_to_desc' => 'تجد أدناه كل المحتوى المعروف في النظام المرتبط بهذا العنصر.',
// Watch Options
'watch' => 'Watch',
'watch_title_default' => 'Default Preferences',
'watch_desc_default' => 'Revert watching to just your default notification preferences.',
'watch_title_ignore' => 'Ignore',
'watch_desc_ignore' => 'Ignore all notifications, including those from user-level preferences.',
'watch_title_new' => 'New Pages',
'watch_desc_new' => 'Notify when any new page is created within this item.',
'watch_title_updates' => 'All Page Updates',
'watch_desc_updates' => 'Notify upon all new pages and page changes.',
'watch_desc_updates_page' => 'Notify upon all page changes.',
'watch_title_comments' => 'All Page Updates & Comments',
'watch_desc_comments' => 'Notify upon all new pages, page changes and new comments.',
'watch_desc_comments_page' => 'Notify upon page changes and new comments.',
'watch_change_default' => 'Change default notification preferences',
'watch_detail_ignore' => 'Ignoring notifications',
'watch_detail_new' => 'Watching for new pages',
'watch_detail_updates' => 'Watching new pages and updates',
'watch_detail_comments' => 'Watching new pages, updates & comments',
'watch_detail_parent_book' => 'Watching via parent book',
'watch_detail_parent_book_ignore' => 'Ignoring via parent book',
'watch_detail_parent_chapter' => 'Watching via parent chapter',
'watch_detail_parent_chapter_ignore' => 'Ignoring via parent chapter',
'watch' => 'شاهد',
'watch_title_default' => 'التفضيلات الافتراضية',
'watch_desc_default' => 'استعادة المشاهدة إلى تفضيلات الإشعارات الافتراضية فقط.',
'watch_title_ignore' => 'تجاهل',
'watch_desc_ignore' => 'تجاهل كافة الإشعارات، بما في ذلك تلك الواردة من تفضيلات مستوى المستخدم.',
'watch_title_new' => 'صفحات جديدة',
'watch_desc_new' => 'إعلام عند إنشاء أي صفحة جديدة ضمن هذا العنصر.',
'watch_title_updates' => 'جميع تحديثات الصفحة',
'watch_desc_updates' => 'إشعار بجميع الصفحات الجديدة والتغييرات في الصفحات.',
'watch_desc_updates_page' => 'إشعار عند حدوث أي تغييرات في الصفحة.',
'watch_title_comments' => 'جميع تحديثات الصفحة والتعليقات',
'watch_desc_comments' => 'إشعار بجميع الصفحات الجديدة، وتغييرات الصفحات والتعليقات الجديدة.',
'watch_desc_comments_page' => 'إشعار عند حدوث تغييرات في الصفحة أو تعليقات جديدة.',
'watch_change_default' => 'تغيير تفضيلات الإشعارات الافتراضية',
'watch_detail_ignore' => 'تجاهل الإشعارات',
'watch_detail_new' => 'ترقب الصفحات الجديدة',
'watch_detail_updates' => 'مشاهدة الصفحات الجديدة والتحديثات',
'watch_detail_comments' => 'مشاهدة الصفحات الجديدة والتحديثات والتعليقات',
'watch_detail_parent_book' => 'المشاهدة عبر الكتاب الرئيس',
'watch_detail_parent_book_ignore' => 'التجاهل عبر الكتاب الرئيس',
'watch_detail_parent_chapter' => 'المشاهدة عبر الفصل الرئيس',
'watch_detail_parent_chapter_ignore' => 'التجاهل عبر الفصل الرئيس',
];

View File

@@ -10,13 +10,13 @@ return [
// Auth
'error_user_exists_different_creds' => 'يوجد مستخدم ببيانات مختلفة مسجل بالنظام للبريد الإلكتروني :email.',
'auth_pre_register_theme_prevention' => 'User account could not be registered for the provided details',
'auth_pre_register_theme_prevention' => 'لم يتمكن حساب المستخدم من التسجيل للحصول على التفاصيل المقدمة',
'email_already_confirmed' => 'تم تأكيد البريد الإلكتروني من قبل, الرجاء محاولة تسجيل الدخول.',
'email_confirmation_invalid' => 'رابط التأكيد غير صحيح أو قد تم استخدامه من قبل, الرجاء محاولة التسجيل من جديد.',
'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' => 'تم تسجيل الدخول بالفعل',
@@ -37,7 +37,7 @@ return [
'social_driver_not_found' => 'لم يتم العثور على السوشيال درايفر "Social driver"',
'social_driver_not_configured' => 'لم يتم تهيئة إعدادات حسابك الاجتماعي بشكل صحيح.',
'invite_token_expired' => 'انتهت صلاحية رابط هذه الدعوة. يمكنك بدلاً من ذلك محاولة إعادة تعيين كلمة مرور حسابك.',
'login_user_not_found' => 'A user for this action could not be found.',
'login_user_not_found' => 'لم يتم العثور على مستخدم لهذا الإجراء.',
// System
'path_not_writable' => 'لا يمكن الرفع إلى مسار :filePath. الرجاء التأكد من قابلية الكتابة إلى الخادم.',
@@ -78,7 +78,7 @@ return [
// Users
'users_cannot_delete_only_admin' => 'لا يمكن حذف المشرف الوحيد',
'users_cannot_delete_guest' => 'لا يمكن حذف المستخدم الضيف',
'users_could_not_send_invite' => 'Could not create user since invite email failed to send',
'users_could_not_send_invite' => 'لم يتم إنشاء المستخدم بسبب فشل إرسال بريد الدعوة',
// Roles
'role_cannot_be_edited' => 'لا يمكن تعديل هذا الدور',
@@ -106,16 +106,16 @@ return [
'back_soon' => 'سيعود للعمل قريباً.',
// Import
'import_zip_cant_read' => 'Could not read ZIP file.',
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
'import_validation_failed' => 'Import ZIP failed to validate with errors:',
'import_zip_failed_notification' => 'Failed to import ZIP file.',
'import_perms_books' => 'You are lacking the required permissions to create books.',
'import_perms_chapters' => 'You are lacking the required permissions to create chapters.',
'import_perms_pages' => 'You are lacking the required permissions to create pages.',
'import_perms_images' => 'You are lacking the required permissions to create images.',
'import_perms_attachments' => 'You are lacking the required permission to create attachments.',
'import_zip_cant_read' => 'لم أتمكن من قراءة المِلَفّ المضغوط -ZIP-.',
'import_zip_cant_decode_data' => 'لم نتمكن من العثور على محتوى المِلَفّ المضغوط data.json وفك تشفيره.',
'import_zip_no_data' => 'لا تتضمن بيانات المِلَفّ المضغوط أي محتوى متوقع للكتاب أو الفصل أو الصفحة.',
'import_validation_failed' => 'فشل التحقق من صحة استيراد المِلَفّ المضغوط بسبب الأخطاء التالية:',
'import_zip_failed_notification' => 'فشل استيراد المِلَفّ المضغوط.',
'import_perms_books' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الكتب.',
'import_perms_chapters' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الفصول.',
'import_perms_pages' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصفحات.',
'import_perms_images' => 'أنت تفتقر إلى الصلاحيات المطلوبة لإنشاء الصور.',
'import_perms_attachments' => 'أنت تفتقر إلى الصَّلاحِيَة المطلوب لإنشاء المرفقات.',
// API errors
'api_no_authorization_found' => 'لم يتم العثور على رمز ترخيص مميز في الطلب',

View File

@@ -4,24 +4,24 @@
*/
return [
'new_comment_subject' => 'New comment on page: :pageName',
'new_comment_intro' => 'A user has commented on a page in :appName:',
'new_page_subject' => 'New page: :pageName',
'new_page_intro' => 'A new page has been created in :appName:',
'updated_page_subject' => 'Updated page: :pageName',
'updated_page_intro' => 'A page has been updated in :appName:',
'updated_page_debounce' => 'To prevent a mass of notifications, for a while you won\'t be sent notifications for further edits to this page by the same editor.',
'new_comment_subject' => 'تعليق جديد على الصفحة: :pageName',
'new_comment_intro' => 'قام أحد المستخدمين بالتعليق على صفحة في :appName:',
'new_page_subject' => 'صفحة جديدة: :pageName',
'new_page_intro' => 'تم إنشاء صفحة جديدة في :appName:',
'updated_page_subject' => 'تم تحديث الصفحة: :pageName',
'updated_page_intro' => 'تم تحديث الصفحة في :appName:',
'updated_page_debounce' => 'لمنع تلقي عدد كبير من الإشعارات، لن يتم إرسال إشعارات إليك لفترة من الوقت لإجراء المزيد من التعديلات على هذه الصفحة بواسطة نفس المحرر.',
'detail_page_name' => 'Page Name:',
'detail_page_path' => 'Page Path:',
'detail_commenter' => 'Commenter:',
'detail_comment' => 'Comment:',
'detail_created_by' => 'Created By:',
'detail_updated_by' => 'Updated By:',
'detail_page_name' => 'اسم الصفحة:',
'detail_page_path' => 'مسار الصفحة:',
'detail_commenter' => 'المُعَلِق:',
'detail_comment' => 'التعليق:',
'detail_created_by' => 'أنشئ من طرف:',
'detail_updated_by' => 'تم التحديث بواسطة:',
'action_view_comment' => 'View Comment',
'action_view_page' => 'View Page',
'action_view_comment' => 'عرض التعليق',
'action_view_page' => 'عرض الصفحة',
'footer_reason' => 'This notification was sent to you because :link cover this type of activity for this item.',
'footer_reason_link' => 'your notification preferences',
'footer_reason' => 'لقد تم إرسال هذا الإشعار إليك لأن :link يغطي هذا النوع من النشاط لهذا العنصر.',
'footer_reason_link' => 'إعدادات الإشعارات الخاصة بك',
];

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