Compare commits

...

130 Commits

Author SHA1 Message Date
Dan Brown
7792cb3915 Updated version and assets for release v21.05.2 2021-06-13 14:26:34 +01:00
Dan Brown
be26253a18 Merge branch 'master' into release 2021-06-13 14:25:39 +01:00
Dan Brown
3d5899d28c Fixed issue with using old non-existing reference in controller
Also done a little code cleanup.
2021-06-13 14:16:09 +01:00
Dan Brown
917d7428d6 Updated composer.lock 2021-06-13 14:06:56 +01:00
Dan Brown
bcc01bd8ff New Crowdin updates (#2790)
* New translations common.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations auth.php (Chinese Simplified)

* New translations auth.php (Chinese Simplified)

* New translations errors.php (Indonesian)

* New translations entities.php (Indonesian)

* New translations errors.php (Indonesian)

* New translations settings.php (Indonesian)

* New translations validation.php (Indonesian)

* New translations settings.php (Spanish, Argentina)
2021-06-13 14:04:23 +01:00
Dan Brown
2c34a99248 Merge pull request #2791 from BookStackApp/attachments_open_in_browser
Attachment serving without forced download
2021-06-13 14:03:08 +01:00
Dan Brown
789d17ab3f Updated platform deps and development version number 2021-06-13 13:57:29 +01:00
Dan Brown
58117bcf2d Extracted not found text into its own simple blade file
Related/intended for #2796
2021-06-13 13:53:59 +01:00
Dan Brown
b5caaa73b7 Fixed content parsing break with line html comment
Fixes issues thrown in custom HMTL head & page content filtering when
the content is comprised of only a single HTML comment.
Adds tests to cover.

For #2804
2021-06-13 12:53:04 +01:00
Dan Brown
7997300f96 Added front-end toggle and testing of inline attachments 2021-06-06 13:55:56 +01:00
Dan Brown
888f435651 Added back-end attachments-in-browser support
A query string will cause attachments to be provided inline
with an appropriate mime type.
Remaining actions:
- Tests
- Front-end functionality
- Config option?
2021-06-06 00:51:06 +01:00
Dan Brown
1bdd1f8189 Updated version for release v21.05.1 2021-06-04 23:09:42 +01:00
Dan Brown
fa62c79b17 Merge branch 'master' into release 2021-06-04 23:08:59 +01:00
Dan Brown
a8471b2c66 Updated translator attribution before release v21.05.1 2021-06-04 23:08:43 +01:00
Dan Brown
0627efe5e9 Updated base64 image extraction to use url instead of path
To ensure it works with all storage types and follows the format of
manually uploaded image content
2021-06-04 22:59:31 +01:00
Dan Brown
af7d62799c New Crowdin updates (#2787)
* New translations common.php (German)

* New translations common.php (Dutch)
2021-06-04 22:50:48 +01:00
Dan Brown
bb00c331e4 Ordered entity permission roles by display name
Closes #2782
2021-06-04 22:36:30 +01:00
Dan Brown
807f92b693 Updated homepage action button colors for consistency
Were previously inconsistent with other homepage buttons for non-default
homepage options.
2021-06-04 22:28:38 +01:00
Dan Brown
c5d31ea7b2 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-06-04 22:21:06 +01:00
Dan Brown
ef1bde8bb1 Fixed wrong styles for homepage favourites
When using a non-default homepage option.

Fixes #2783
2021-06-04 22:20:11 +01:00
Dan Brown
8897945609 Roll-out and re-fix of croation via crowdin (#2785)
* New translations auth.php (Croatian)

* New translations activities.php (Croatian)

* New translations activities.php (German Informal)

* New translations common.php (Croatian)

* New translations passwords.php (Croatian)

* New translations settings.php (Czech)

* New translations settings.php (Spanish)

* New translations settings.php (Catalan)

* New translations settings.php (Arabic)

* New translations settings.php (French)

* New translations pagination.php (Croatian)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Hebrew)

* New translations validation.php (Korean)

* New translations validation.php (Croatian)

* New translations settings.php (Hungarian)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Japanese)

* New translations settings.php (Korean)

* New translations settings.php (Dutch)

* New translations settings.php (Polish)

* New translations settings.php (Portuguese)

* New translations settings.php (Russian)

* New translations settings.php (Slovak)

* New translations settings.php (Slovenian)

* New translations settings.php (Turkish)

* New translations settings.php (Ukrainian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Indonesian)

* New translations settings.php (Persian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Croatian)

* New translations settings.php (Latvian)

* New translations settings.php (Bosnian)

* New translations settings.php (Norwegian Bokmal)

* New translations settings.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (Italian)

* New translations settings.php (Swedish)

* New translations settings.php (Bulgarian)

* New translations errors.php (German Informal)

* New translations errors.php (Croatian)

* New translations components.php (Croatian)

* New translations entities.php (Croatian)

* New translations pagination.php (Croatian)

* New translations entities.php (Croatian)

* New translations components.php (Croatian)

* New translations errors.php (Croatian)

* New translations settings.php (Croatian)

* New translations validation.php (Croatian)

* New translations passwords.php (Croatian)

* New translations auth.php (Croatian)

* New translations common.php (Croatian)

* New translations activities.php (Croatian)
2021-06-02 22:15:58 +01:00
Dan Brown
9382d647d7 Merge branch 'ffranchina-master' 2021-06-02 21:57:23 +01:00
Dan Brown
0d17d18d07 New Crowdin updates (#2777)
* New translations common.php (Latvian)

* New translations entities.php (Latvian)

* New translations activities.php (Italian)

* New translations common.php (Italian)

* New translations entities.php (Italian)

* New translations errors.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations activities.php (Spanish, Argentina)

* New translations common.php (Spanish, Argentina)

* New translations common.php (French)

* New translations common.php (Swedish)

* New translations activities.php (Swedish)

* New translations common.php (Swedish)

* New translations entities.php (Swedish)

* New translations errors.php (Swedish)

* New translations settings.php (Swedish)

* New translations validation.php (Bulgarian)

* New translations validation.php (Bulgarian)

* New translations common.php (Bulgarian)

* New translations validation.php (Bulgarian)

* New translations settings.php (Bulgarian)

* New translations activities.php (Indonesian)

* New translations settings.php (Bulgarian)

* New translations common.php (Bulgarian)

* New translations entities.php (Bulgarian)

* New translations activities.php (Turkish)

* New translations settings.php (Bulgarian)

* New translations components.php (Bulgarian)

* New translations activities.php (Russian)

* New translations common.php (Russian)

* New translations entities.php (Russian)

* New translations common.php (Russian)

* New translations entities.php (Russian)
2021-06-02 21:56:53 +01:00
Dan Brown
24eef03fb9 Added croatian to required arrays/lists 2021-06-02 21:55:30 +01:00
Dan Brown
e51352e1a4 Added back in commas, reset settings language array
Related to #2784
2021-06-02 21:50:38 +01:00
Dan Brown
2dfb1ae3ee Merge branch 'master' of https://github.com/ffranchina/BookStack into ffranchina-master 2021-06-02 21:44:39 +01:00
Dan Brown
39928e1c63 Reviewed base64 image upload support
- Added test cases to cover.
- Altered parsing logic to be a little less reliant on regex.
- Added new iamge repo method for creating from data.
- Added extension validation and additional type support.
- Done some cleanup of common operations within PageContent.
- Added message to API docs/method to mention image usage.

For #2700 and #2631.
2021-06-02 21:34:34 +01:00
Dan Brown
40ca50e44f Merge branch 'master' of https://github.com/awarre/BookStack into awarre-master 2021-06-02 20:25:20 +01:00
Francesco Franchina
fc7b8c49fb Adding Croatian translation files 2021-06-02 17:32:31 +02:00
Dan Brown
d7d8fa1e5b Updated version and assets for release v21.05 2021-05-30 16:17:56 +01:00
Dan Brown
18562f1e10 Merge branch 'master' into release 2021-05-30 16:17:44 +01:00
Dan Brown
fdabafffda Added thumbnail attribute to complete .env 2021-05-30 15:22:58 +01:00
Dan Brown
e2df15fe20 Updated translators before next release 2021-05-30 15:11:59 +01:00
Dan Brown
54bac17ef0 New Crowdin updates (#2764) 2021-05-30 15:10:11 +01:00
Dan Brown
7634ac4e12 Updated test to align with export date format change 2021-05-30 13:23:51 +01:00
Dan Brown
c4f5ab12cf Aligned export and revision shown date format
As raised in #2771
2021-05-30 00:02:32 +01:00
Dan Brown
57a063cdfb Updated nav tests to look for shortened item names 2021-05-29 23:46:33 +01:00
Dan Brown
1fa90e4f12 Converted another couple of tests from browserkit 2021-05-29 23:42:21 +01:00
Dan Brown
d62cdd58d3 Upgraded php and npm deps
- Sass upgrade had some breaking changes where division was used
hence updated for newer sass version support.
2021-05-29 13:08:28 +01:00
Dan Brown
ed6ec341df Added testing to cover next/previous navigation
For #2511
2021-05-29 12:49:10 +01:00
Dan Brown
0cfff6ab6f Reviewed and refactored next/previous navigation button implementation
- Updated styling to include item name.
- Extracted used text to translations.
- Updated the design to better suit the surrounding blocks.
- Removed newly added model/repo methods.
- Moved core logic out of controller and instead into a "NextPreviousContentLocator"
helper with re-uses the output from the book-tree generation.
- Also added the system to chapters.

For #2511
2021-05-29 12:39:41 +01:00
Dan Brown
7ca66c5d5e Merge branch 'prev-next-button' of https://github.com/shubhamosmosys/BookStack into shubhamosmosys-prev-next-button 2021-05-26 22:13:19 +01:00
Dan Brown
9cbea1eb08 Updated drawing upload error to shown/handle server limit errors
Closes #2740
2021-05-26 18:23:27 +01:00
Dan Brown
1a2d374f24 Revert "Added app logo to outgoing emails"
This reverts commit e32929029b.
2021-05-26 17:13:59 +01:00
Dan Brown
e32929029b Added app logo to outgoing emails
Required changing the header bar of the email to be solid color to match
the configuration of the main app header since otherwise colors may not
work together.

Closes #2577
2021-05-26 17:11:03 +01:00
Dan Brown
eb76e882c5 Added deletion of revisions on page delete
Added testing to cover.
Closes #2668
2021-05-26 16:40:56 +01:00
Dan Brown
d326417edc Added name input autofocus on shelves, books and chapters
Closes #1956
2021-05-26 15:25:23 +01:00
Dan Brown
a3a8fef6b2 Made users header interface more adaptable
Search input was stacking on create button on default desktop view
due when viewing in russian due to combined width exceeding container.
Made into normal flexbox instead.

Closes #2147
2021-05-26 15:20:35 +01:00
Dan Brown
0c16334426 Merge branch 'master' of github.com:BookStackApp/BookStack 2021-05-25 00:06:13 +01:00
Dan Brown
600f8cd142 Added origin verification to postMessage usage.
Closes #2769
2021-05-25 00:05:20 +01:00
Dan Brown
5c8c85a0ff Merge pull request #2768 from CorruptComputer/RSPEC-5148-Fixes
[sec] Fixes a few minor vulnerabilies when using target="_blank" on links (RSPEC-5148)
2021-05-24 22:11:49 +01:00
Nickolas Gupton
7a6f21648a Fixes minor vulnerability when using target="_blank" on links (RSPEC-5148) 2021-05-24 16:17:08 -04:00
Dan Brown
df0e03cd07 Reviewed PR to add import user avatars va LDAP
- Reduced options to single new configuration paramter instead of two.
- Moved more logic into UserAvatars class.
- Updated LDAP avatar import to also run on login when no image is
  currently set.
- Added thumbnail fetching to search requests.
- Added testing to cover.

Related to PR #2320, and issue #1161
2021-05-24 18:54:08 +01:00
Dan Brown
85db812fea Merge branch 'master' of https://github.com/jasonhoule/BookStack into jasonhoule-master 2021-05-24 17:06:50 +01:00
Dan Brown
fb5b5e138d Updated existing tag tests away from browserkit testing 2021-05-24 16:16:58 +01:00
Dan Brown
3eaf03a7ac Reviewed tag in seach work
- Refactored some tag code bits while reviewing.
- Updated tag design in search listing to be more subtle.
- Moved tags out of entity-list-item-basic template and instead moved
  them into entity-list-item, below the existing content.
- Tweaked existing tag colors a little.
- Changed tag icon to be more tag-like.
- Added tag-on-search test case.

Review of #2487, Related to #2462
2021-05-24 16:12:09 +01:00
Dan Brown
5420f3451c Merge branch 'show-tags' of https://github.com/burnoutberni/BookStack into burnoutberni-show-tags 2021-05-24 15:12:45 +01:00
Dan Brown
7d94da10fb Merge branch 'v21.04.x' 2021-05-24 13:08:51 +01:00
Dan Brown
86090a694f Updated version and assets for release v21.04.6 2021-05-24 13:06:03 +01:00
Dan Brown
1ee8287c73 Merge branch 'v21.04.x' into release 2021-05-24 13:05:34 +01:00
Dan Brown
c7322a71f7 Added theme add social driver redirect configuration callback
Allows someone using the theme system to configure the social driver
before a redirect action occurs, by passing a callback as an additional
param to the theme 'addSocialDriver' method.
2021-05-24 12:55:45 +01:00
Dan Brown
2c3523f6a1 Updated image permission setting logic
To ensure thhat the visibility is still set on local storage options
since the previous recent changes could cause problems where in
scenarios where the server user could not read images uploaded by the
php process user.

Closes #2758
2021-05-24 12:09:28 +01:00
Dan Brown
dd6076049c Merge pull request #2748 from BookStackApp/favourite_system
Favourite System
2021-05-23 14:45:42 +01:00
Dan Brown
ba8ba5c634 Added testing to favourite system
- Also removed some old view service references.
- Updated TopFavourites query to be based on favourites table and join
  in the views instead of the other way around, so that favourites still
show even if they have no views.
2021-05-23 14:34:36 +01:00
Dan Brown
c2069f37cc Added deletion of favourites on entity/user delete 2021-05-23 13:41:56 +01:00
Dan Brown
1e0aa7ee2c Added favourites page with link from header and home 2021-05-23 13:34:08 +01:00
Dan Brown
27942f5ce8 Deleted redundant complex relationmultimodel query class 2021-05-22 14:07:57 +01:00
Dan Brown
d0ff79ea60 Revamped some complex queries, added favourites to home
- Removed old view system and started use of new query classes instead.
- Finished off RelationMultiModelQuery but found it was less efficient
than x-many queries due to the amount of tables being scanned.
Adding now for history but will delete as not used.
- Updated recently viewed to use same query system as popular items
  rather than running and joining x-entities queries.
- Added "Most Viewed Faviourites" listing to homepages.
2021-05-22 14:05:28 +01:00
Dan Brown
3de02566bf Started building system for cross-model queries 2021-05-19 23:37:23 +01:00
Dan Brown
93fd869ba3 Started refactoring of view service
Phasing out the view service from being a generic 'service' class,
moving the core create/delete methods into the model.
The idea is that the existing query work will need to interlink
with the favourite system so maybe we have a (or many composable)
query building classes rather than mixing query building and
create/delete work as per the old service.
2021-05-16 10:49:37 +01:00
Dan Brown
3ca149137e Added faviourtes to other entity types 2021-05-16 10:26:28 +01:00
Dan Brown
db9aa41096 Started writing testing for favourites 2021-05-16 01:07:20 +01:00
Dan Brown
bf8e7f3393 Started addition of favourite system 2021-05-16 00:29:56 +01:00
Dan Brown
8eb98cd591 Updated version and assets for release v21.04.5 2021-05-15 17:56:29 +01:00
Dan Brown
0f9ba21b05 Merge branch 'v21.04.x' into release 2021-05-15 17:56:03 +01:00
Dan Brown
7a059a5e90 Updated translator attribution before release v21.04.5 2021-05-15 17:54:57 +01:00
Dan Brown
e5fc104aff New Crowdin updates (#2737)
* New translations errors.php (Italian)

* New translations errors.php (Slovak)

* New translations errors.php (Norwegian Bokmal)

* New translations errors.php (Bosnian)

* New translations errors.php (Latvian)

* New translations errors.php (Spanish, Argentina)

* New translations errors.php (Persian)

* New translations errors.php (Indonesian)

* New translations errors.php (Portuguese, Brazilian)

* New translations errors.php (Vietnamese)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (Ukrainian)

* New translations errors.php (Turkish)

* New translations errors.php (Swedish)

* New translations errors.php (Slovenian)

* New translations errors.php (Russian)

* New translations errors.php (French)

* New translations errors.php (Portuguese)

* New translations errors.php (Polish)

* New translations errors.php (Dutch)

* New translations errors.php (Korean)

* New translations errors.php (Japanese)

* New translations errors.php (Hungarian)

* New translations errors.php (Hebrew)

* New translations errors.php (German)

* New translations errors.php (Danish)

* New translations errors.php (Czech)

* New translations errors.php (Catalan)

* New translations errors.php (Bulgarian)

* New translations errors.php (Arabic)

* New translations errors.php (Spanish)

* New translations errors.php (German Informal)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (French)

* New translations common.php (French)

* New translations errors.php (Spanish, Argentina)

* New translations common.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations activities.php (Arabic)

* New translations auth.php (Arabic)

* New translations entities.php (Arabic)

* New translations auth.php (Arabic)

* New translations components.php (Arabic)

* New translations entities.php (Arabic)

* New translations errors.php (Russian)

* New translations common.php (Portuguese)

* New translations errors.php (Portuguese)
2021-05-15 17:50:02 +01:00
Dan Brown
d0ed165630 Merge pull request #2735 from dopyrory3/table_column_fix
Fix table width styling on pages rendered in markdown
2021-05-15 17:48:27 +01:00
Dan Brown
68ef6a842f Fixed issue thrown upon empty markdown content save
Closes #2741
2021-05-15 17:33:53 +01:00
Dan Brown
c1f070a136 Handle acl set of images differently for s3 and s3-like
Related to #2739
2021-05-15 17:25:51 +01:00
Dan Brown
c2cc1ec5e5 Adjusted dompdf font path to writable folder
Related to #2746
2021-05-15 12:19:36 +01:00
Rory Maher
386925ad8e Apply column fix to all tables 2021-05-10 12:11:28 +01:00
Rory Maher
243c1db408 Revert "Fix table width style"
This reverts commit b010d2663d.
2021-05-10 12:10:02 +01:00
Dan Brown
834f8e7046 Updated version and assets for release v21.04.4 2021-05-09 14:46:05 +01:00
Dan Brown
32e3399334 Merge branch 'master' into release 2021-05-09 14:45:36 +01:00
Dan Brown
9e7bcacf8c Moved NotifyException render work from handler to exception
As continued from last commit.
2021-05-08 19:00:09 +01:00
Dan Brown
7be7d7d1e7 Updated not-found image path handling to have better ux
Added test to cover.
Started refactoring some of the app error handling in
the process of this.

Fixes #2696
2021-05-08 18:49:58 +01:00
Dan Brown
04c1d0e071 Updated translators before v21.04.4 release 2021-05-08 17:56:35 +01:00
Dan Brown
ab62e0f75b Merge pull request #2716 from Jokuna/master
Update Korean translation
2021-05-08 17:53:02 +01:00
Dan Brown
d85f99c87c New Crowdin updates (#2719)
* New translations entities.php (Dutch)

* New translations components.php (Italian)

* New translations entities.php (Italian)

* New translations entities.php (Italian)

* New translations errors.php (Italian)

* New translations passwords.php (Italian)

* New translations settings.php (Italian)

* New translations validation.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Indonesian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Italian)

* New translations common.php (Portuguese)

* New translations common.php (Arabic)

* New translations common.php (Arabic)

* New translations entities.php (Arabic)

* New translations entities.php (Arabic)

* New translations settings.php (Italian)
2021-05-08 17:52:32 +01:00
Dan Brown
c42b6aece9 Updated composer deps again and run npm audit fix 2021-05-08 17:50:28 +01:00
Dan Brown
7f8f3080c5 Removed php8-only 'mixed' type from test method 2021-05-08 13:23:28 +01:00
Dan Brown
9cf4191079 Reviewed and updated SAML2 authncontext option
Added tests to cover.
Changed default to align with existing default.
Added env option parsing.
For #1998
2021-05-08 13:07:25 +01:00
Dan Brown
b8e2d75014 Merge branch 'ivir-authncontext' of https://github.com/ivir/BookStack into ivir-ivir-authncontext 2021-05-08 12:13:27 +01:00
Dan Brown
f522f16526 Fixed SAML login button alignment 2021-05-08 11:49:18 +01:00
Rory Maher
b010d2663d Fix table width style
Tables generated by the markdown renderer don't honour the max-width property without applying word-break styling to the td elements
2021-05-06 13:23:38 +01:00
Dan Brown
a083ceaf44 Fixed item export with deleted creator/updated
Added test to cover.
Fixes #2733
2021-05-05 22:52:08 +01:00
Dan Brown
95798a2eba Standardised export views with base layout, Reduced included export styles
Related to #2666
2021-05-04 23:15:05 +01:00
Dan Brown
43b6633183 Filtered scripts in custom HTML head for exports
Since it appeared to cause problems in some scenarios.
Related to #2490
2021-05-03 23:59:52 +01:00
Dan Brown
c50ac022a8 Updated composer deps 2021-05-03 22:32:19 +01:00
Dan Brown
a3d36237e2 Fixed white borders on layout tabs on ios
Closes #2728
2021-05-03 22:28:25 +01:00
Jokuna
a2be61f26d [Fix] app_footer_links_desc 2021-04-29 15:06:58 +09:00
Jokuna
79f5b579d7 [Fix] maint_delete_images_only_in_revisions better 2021-04-29 14:49:46 +09:00
Jokuna
66ecee1e26 [Fix] maint_delete_images_only_in_revisions 2021-04-29 13:54:24 +09:00
Jokuna
02e86ea18f [Fix] app_footer_links_desc 2021-04-29 13:44:50 +09:00
Jokuna
723dbe1da7 [Fix] korean 2021-04-29 13:43:10 +09:00
Jokuna
65fe89441f fix pages_revisions_resotred_from 2021-04-29 13:39:11 +09:00
Jokuna
2093122ac5 Korean translation
resources/lang/ko/settings.php
2021-04-29 12:58:53 +09:00
Jokuna
ab584c93bc Korean translations
activities.php
common.php
entities.php
validation.php
2021-04-29 00:11:01 +09:00
Dan Brown
2d8698a218 Updated version and assets for release v21.04.3 2021-04-27 22:01:37 +01:00
Dan Brown
454fb883a2 Merge branch 'master' into release 2021-04-27 22:01:15 +01:00
Dan Brown
fc504a3d2c Updated translator attribution before release v21.04.3 2021-04-27 22:00:51 +01:00
Dan Brown
dd805503fb New Crowdin updates (#2695)
* New translations settings.php (Japanese)

* New translations settings.php (Japanese)

* New translations common.php (Latvian)

* New translations common.php (Russian)

* New translations settings.php (Dutch)

* New translations common.php (Dutch)

* New translations settings.php (Dutch)

* New translations entities.php (Dutch)

* New translations validation.php (Dutch)

* New translations activities.php (Dutch)

* New translations common.php (German)

* New translations common.php (Dutch)

* New translations common.php (German Informal)

* New translations activities.php (Dutch)

* New translations entities.php (German)

* New translations settings.php (German)

* New translations auth.php (Dutch)

* New translations components.php (Dutch)

* New translations common.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (German Informal)

* New translations common.php (Catalan)

* New translations common.php (Catalan)

* New translations passwords.php (Catalan)

* New translations validation.php (Catalan)

* New translations validation.php (Catalan)

* New translations auth.php (Catalan)

* New translations common.php (Italian)

* New translations activities.php (Italian)

* New translations common.php (Italian)
2021-04-27 21:58:09 +01:00
Dan Brown
f24336f77a Updated mobile content tabs to respect dark mode 2021-04-27 21:55:33 +01:00
Dan Brown
aa6a752e38 Implemented custom select controls because apple hates web developers
They'd rather keep pushing their 2007 era strange form control styles
even though they're horribly outdated, ugly and hard to style. The
only way to override is a full nuking of the default styles, which means
we have to then implement the frigging arrow icon using hacks which would
then conflict with all other sensible browsers so we have to nuke their
styles aswell to ensure some stupid backgroud hack is used everywhere.

I bet apple don't even use their shite default control styles and nuke
them also, Lets see. Yup, First thing I see on the top of their homepage
is a locale select dropdown custom built from about 10 HTML elements. FML

For #2709
2021-04-27 21:36:08 +01:00
Dan Brown
83b576eb19 Prevented "Recently Viewed" homepage list showing non-user-viewed items
Triggered when the user has no/limited views. Added a test to cover.
Closes #2703
2021-04-27 21:05:01 +01:00
Dan Brown
c4e31a0d5e Updated hard-coded string lengths for indexed columns
Since this is what's causing issues for people during migration due to max
key lengths.
Related to #2710.
2021-04-27 20:53:22 +01:00
Dan Brown
f8cdd6e80d Reduced calls for s3-based uploads
Combined the public ACL update into the put operation.
2021-04-27 20:36:42 +01:00
awarre
f8b5a0fd50 Add base64 image support 2021-04-20 23:41:21 +00:00
Dan Brown
6f4a6ab8ea Updated version for release v21.04.2 2021-04-20 22:37:05 +01:00
Dan Brown
9c4b6f36f1 Merge branch 'master' into release 2021-04-20 22:36:35 +01:00
Dan Brown
140aed3586 Updated translator attribution before release v21.04.2 2021-04-20 22:36:21 +01:00
Dan Brown
cf87b78636 New Crowdin updates (#2691)
* New translations common.php (Spanish)

* New translations common.php (Danish)

* New translations auth.php (Danish)

* New translations components.php (Danish)

* New translations entities.php (Danish)

* New translations entities.php (Danish)

* New translations settings.php (Danish)

* New translations common.php (Chinese Simplified)

* New translations auth.php (Chinese Simplified)

* New translations settings.php (Danish)

* New translations settings.php (Danish)

* New translations activities.php (Danish)

* New translations validation.php (Danish)

* New translations common.php (Danish)

* New translations auth.php (Danish)

* New translations activities.php (Danish)
2021-04-20 22:14:37 +01:00
Dan Brown
ec827da5a5 Updated public view test case to be more reliable
Was failing due to either common name or view share being
sticky across requests.
2021-04-20 22:14:01 +01:00
Dan Brown
20528a2442 Fixed error thrown when owner existed but the creator did not
Added test to cover.
For #2687
2021-04-20 21:04:38 +01:00
Shubham Tiwari
99c42033b1 Add prev and next button to navigate through different pages 2021-01-27 10:15:28 +05:30
Bernhard Hayden
aad2ee675c Show tags of all search results 2021-01-15 15:52:03 +01:00
Jason Houle
a192b600fc Missed a variable when updating LdapService. 2020-10-12 12:47:36 -04:00
Jason Houle
b714652e10 Import thumbnail photos when LDAP users are created. 2020-10-12 12:33:55 -04:00
Jan Mareš
034478409e Add support Windows Authentication via SAML 2020-04-03 14:05:07 +02:00
355 changed files with 6113 additions and 1893 deletions

View File

@@ -200,6 +200,7 @@ LDAP_TLS_INSECURE=false
LDAP_ID_ATTRIBUTE=uid
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_THUMBNAIL_ATTRIBUTE=null
LDAP_FOLLOW_REFERRALS=true
LDAP_DUMP_USER_DETAILS=false
@@ -222,6 +223,7 @@ SAML2_IDP_x509=null
SAML2_ONELOGIN_OVERRIDES=null
SAML2_DUMP_USER_DETAILS=false
SAML2_AUTOLOAD_METADATA=false
SAML2_IDP_AUTHNCONTEXT=true
# SAML group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/saml2-auth/

View File

@@ -54,6 +54,7 @@ Name :: Languages
@benediktvolke :: German
@Baptistou :: French
@arcoai :: Spanish
@Jokuna :: Korean
cipi1965 :: Italian
Mykola Ronik (Mantikor) :: Ukrainian
furkanoyk :: Turkish
@@ -156,3 +157,13 @@ nikservik :: Ukrainian; Russian; Polish
HenrijsS :: Latvian
Pascal R-B (pborgner) :: German
Boris (Ginfred) :: Russian
Jonas Anker Rasmussen (jonasanker) :: Danish
Gerwin de Keijzer (gdekeijzer) :: Dutch; German; German Informal
kometchtech :: Japanese
Auri (Atalonica) :: Catalan
Francesco Franchina (ffranchina) :: Italian
Aimrane Kds (aimrane.kds) :: Arabic
whenwesober :: Indonesian
Rem (remkovdhoef) :: Dutch
syn7ax69 :: Bulgarian; Turkish
Blaade :: French

17
app/Actions/Favourite.php Normal file
View File

@@ -0,0 +1,17 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Favourite extends Model
{
protected $fillable = ['user_id'];
/**
* Get the related model that can be favourited.
*/
public function favouritable(): MorphTo
{
return $this->morphTo();
}
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Actions;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
class Tag extends Model
{
@@ -9,10 +10,25 @@ class Tag extends Model
/**
* Get the entity that this tag belongs to
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function entity()
public function entity(): MorphTo
{
return $this->morphTo('entity');
}
/**
* Get a full URL to start a tag name search for this tag name.
*/
public function nameUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%5D');
}
/**
* Get a full URL to start a tag name and value search for this tag's values.
*/
public function valueUrl(): string
{
return url('/search?term=%5B' . urlencode($this->name) .'%3D' . urlencode($this->value) . '%5D');
}
}

View File

@@ -1,7 +1,19 @@
<?php namespace BookStack\Actions;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use Illuminate\Database\Eloquent\Relations\MorphTo;
/**
* Class View
* Views are stored per-item per-person within the database.
* They can be used to find popular items or recently viewed items
* at a per-person level. They do not record every view instance as an
* activity. Only the latest and original view times could be recognised.
*
* @property int $views
* @property int $user_id
*/
class View extends Model
{
@@ -9,10 +21,37 @@ class View extends Model
/**
* Get all owning viewable models.
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function viewable()
public function viewable(): MorphTo
{
return $this->morphTo();
}
/**
* Increment the current user's view count for the given viewable model.
*/
public static function incrementFor(Viewable $viewable): int
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return 0;
}
/** @var View $view */
$view = $viewable->views()->firstOrNew([
'user_id' => $user->id,
], ['views' => 0]);
$view->forceFill(['views' => $view->views + 1])->save();
return $view->views;
}
/**
* Clear all views from the system.
*/
public static function clearAll()
{
static::query()->truncate();
}
}

View File

@@ -1,116 +0,0 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\EntityProvider;
use DB;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param View $view
* @param PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
* Add a view to the given entity.
* @param \BookStack\Entities\Models\Entity $entity
* @return int
*/
public function add(Entity $entity)
{
$user = user();
if ($user === null || $user->isDefault()) {
return 0;
}
$view = $entity->views()->where('user_id', '=', $user->id)->first();
// Add view if model exists
if ($view) {
$view->increment('views');
return $view->views;
}
// Otherwise create new view count
$entity->views()->save($this->view->newInstance([
'user_id' => $user->id,
'views' => 1
]));
return 1;
}
/**
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return Collection
*/
public function getPopular(int $count = 10, int $page = 0, array $filterModels = null, string $action = 'view')
{
$skipCount = $count * $page;
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view->newQuery(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')
->skip($skipCount)
->take($count)
->get()
->pluck('viewable')
->filter();
}
/**
* Get all recently viewed entities for the current user.
*/
public function getUserRecentlyViewed(int $count = 10, int $page = 1)
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$all = collect();
/** @var Entity $instance */
foreach ($this->entityProvider->all() as $name => $instance) {
$items = $instance::visible()->withLastView()
->orderBy('last_viewed_at', 'desc')
->skip($count * ($page - 1))
->take($count)
->get();
$all = $all->concat($items);
}
return $all->sortByDesc('last_viewed_at')->slice(0, $count);
}
/**
* Reset all view counts by deleting all views.
*/
public function resetAll()
{
$this->view->truncate();
}
}

View File

@@ -90,6 +90,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
$this->ldapService->syncGroups($user, $username);
}
// Attach avatar if non-existent
if (is_null($user->avatar)) {
$this->ldapService->saveAndAttachAvatar($user, $userDetails);
}
$this->login($user, $remember);
return true;
}
@@ -115,6 +120,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
'password' => Str::random(32),
];
return $this->registrationService->registerUser($details, null, false);
$user = $this->registrationService->registerUser($details, null, false);
$this->ldapService->saveAndAttachAvatar($user, $ldapUserDetails);
return $user;
}
}

View File

@@ -3,7 +3,9 @@
use BookStack\Auth\User;
use BookStack\Exceptions\JsonDebugException;
use BookStack\Exceptions\LdapException;
use BookStack\Uploads\UserAvatars;
use ErrorException;
use Illuminate\Support\Facades\Log;
/**
* Class LdapService
@@ -14,15 +16,17 @@ class LdapService extends ExternalAuthService
protected $ldap;
protected $ldapConnection;
protected $userAvatars;
protected $config;
protected $enabled;
/**
* LdapService constructor.
*/
public function __construct(Ldap $ldap)
public function __construct(Ldap $ldap, UserAvatars $userAvatars)
{
$this->ldap = $ldap;
$this->userAvatars = $userAvatars;
$this->config = config('services.ldap');
$this->enabled = config('auth.method') === 'ldap';
}
@@ -76,10 +80,13 @@ class LdapService extends ExternalAuthService
$idAttr = $this->config['id_attribute'];
$emailAttr = $this->config['email_attribute'];
$displayNameAttr = $this->config['display_name_attribute'];
$thumbnailAttr = $this->config['thumbnail_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'dn', $idAttr, $emailAttr, $displayNameAttr]);
$user = $this->getUserWithAttributes($userName, array_filter([
'cn', 'dn', $idAttr, $emailAttr, $displayNameAttr, $thumbnailAttr,
]));
if ($user === null) {
if (is_null($user)) {
return null;
}
@@ -89,6 +96,7 @@ class LdapService extends ExternalAuthService
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
'avatar'=> $thumbnailAttr ? $this->getUserResponseProperty($user, $thumbnailAttr, null) : null,
];
if ($this->config['dump_user_details']) {
@@ -350,4 +358,22 @@ class LdapService extends ExternalAuthService
$userLdapGroups = $this->getUserGroups($username);
$this->syncWithGroups($user, $userLdapGroups);
}
/**
* Save and attach an avatar image, if found in the ldap details, and attach
* to the given user model.
*/
public function saveAndAttachAvatar(User $user, array $ldapUserDetails): void
{
if (is_null(config('services.ldap.thumbnail_attribute')) || is_null($ldapUserDetails['avatar'])) {
return;
}
try {
$imageData = $ldapUserDetails['avatar'];
$this->userAvatars->assignToUserFromExistingData($user, $imageData, 'jpg');
} catch (\Exception $exception) {
Log::info("Failed to use avatar image from LDAP data for user id {$user->id}");
}
}
}

View File

@@ -19,10 +19,37 @@ use Symfony\Component\HttpFoundation\RedirectResponse;
class SocialAuthService
{
/**
* The core socialite library used.
* @var Socialite
*/
protected $socialite;
protected $socialAccount;
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
/**
* The default built-in social drivers we support.
* @var string[]
*/
protected $validSocialDrivers = [
'google',
'github',
'facebook',
'slack',
'twitter',
'azure',
'okta',
'gitlab',
'twitch',
'discord'
];
/**
* Callbacks to run when configuring a social driver
* for an initial redirect action.
* Array is keyed by social driver name.
* Callbacks are passed an instance of the driver.
* @var array<string, callable>
*/
protected $configureForRedirectCallbacks = [];
/**
* SocialAuthService constructor.
@@ -39,7 +66,7 @@ class SocialAuthService
public function startLogIn(string $socialDriver): RedirectResponse
{
$driver = $this->validateDriver($socialDriver);
return $this->getSocialDriver($driver)->redirect();
return $this->getDriverForRedirect($driver)->redirect();
}
/**
@@ -49,7 +76,7 @@ class SocialAuthService
public function startRegister(string $socialDriver): RedirectResponse
{
$driver = $this->validateDriver($socialDriver);
return $this->getSocialDriver($driver)->redirect();
return $this->getDriverForRedirect($driver)->redirect();
}
/**
@@ -227,7 +254,7 @@ class SocialAuthService
/**
* Provide redirect options per service for the Laravel Socialite driver
*/
public function getSocialDriver(string $driverName): Provider
protected function getDriverForRedirect(string $driverName): Provider
{
$driver = $this->socialite->driver($driverName);
@@ -238,6 +265,10 @@ class SocialAuthService
$driver->with(['resource' => 'https://graph.windows.net']);
}
if (isset($this->configureForRedirectCallbacks[$driverName])) {
$this->configureForRedirectCallbacks[$driverName]($driver);
}
return $driver;
}
@@ -248,12 +279,19 @@ class SocialAuthService
* within the `Config/services.php` file.
* Handler should be a Class@method handler to the SocialiteWasCalled event.
*/
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
{
public function addSocialDriver(
string $driverName,
array $config,
string $socialiteHandler,
callable $configureForRedirect = null
) {
$this->validSocialDrivers[] = $driverName;
config()->set('services.' . $driverName, $config);
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
if (!is_null($configureForRedirect)) {
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
}
}
}

View File

@@ -580,14 +580,15 @@ class PermissionService
/**
* Filter items that have entities set as a polymorphic relation.
* @param Builder|\Illuminate\Database\Query\Builder $query
*/
public function filterRestrictedEntityRelations(Builder $query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view'): Builder
public function filterRestrictedEntityRelations($query, string $tableName, string $entityIdColumn, string $entityTypeColumn, string $action = 'view')
{
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
$q = $query->where(function ($query) use ($tableDetails, $action) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $action) {
$permissionQuery->select('id')->from('joint_permissions')
$permissionQuery->select(['role_id'])->from('joint_permissions')
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
->whereRaw('joint_permissions.entity_type=' . $tableDetails['tableName'] . '.' . $tableDetails['entityTypeColumn'])
->where('action', '=', $action)

View File

@@ -92,7 +92,7 @@ class Role extends Model implements Loggable
}
/**
* Get all visible roles
* Get all visible roles.
*/
public static function visible(): Collection
{
@@ -104,7 +104,10 @@ class Role extends Model implements Loggable
*/
public static function restrictable(): Collection
{
return static::query()->where('system_name', '!=', 'admin')->get();
return static::query()
->where('system_name', '!=', 'admin')
->orderBy('display_name', 'asc')
->get();
}
/**

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Auth;
use BookStack\Actions\Favourite;
use BookStack\Api\ApiToken;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Interfaces\Loggable;
@@ -240,6 +241,14 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
return $this->hasMany(ApiToken::class);
}
/**
* Get the favourite instances for this user.
*/
public function favourites(): HasMany
{
return $this->hasMany(Favourite::class);
}
/**
* Get the last activity time for this user.
*/

View File

@@ -8,13 +8,11 @@ use BookStack\Entities\Models\Chapter;
use BookStack\Entities\Models\Page;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use BookStack\Uploads\UserAvatars;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Pagination\LengthAwarePaginator;
use Images;
use Log;
class UserRepo
@@ -184,16 +182,11 @@ class UserRepo
{
$user->socialAccounts()->delete();
$user->apiTokens()->delete();
$user->favourites()->delete();
$user->delete();
// Delete user profile images
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
$this->userAvatar->destroyAllForUser($user);
if (!empty($newOwnerId)) {
$newOwner = User::query()->find($newOwnerId);

View File

@@ -56,7 +56,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
'locales' => ['en', 'ar', 'bg', 'bs', 'ca', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hr', 'hu', 'id', 'it', 'ja', 'ko', 'lv', 'nl', 'nb', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
// Application Fallback Locale
'fallback_locale' => 'en',
@@ -184,11 +184,8 @@ return [
// Custom BookStack
'Activity' => BookStack\Facades\Activity::class,
'Views' => BookStack\Facades\Views::class,
'Images' => BookStack\Facades\Images::class,
'Permissions' => BookStack\Facades\Permissions::class,
'Theme' => BookStack\Facades\Theme::class,
],
// Proxy configuration

View File

@@ -38,7 +38,7 @@ return [
* Times-Roman, Times-Bold, Times-BoldItalic, Times-Italic,
* Symbol, ZapfDingbats.
*/
"DOMPDF_FONT_DIR" => app_path('vendor/dompdf/dompdf/lib/fonts/'), //storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
"DOMPDF_FONT_DIR" => storage_path('fonts/'), // advised by dompdf (https://github.com/dompdf/dompdf/pull/782)
/**
* The location of the DOMPDF font cache directory
@@ -219,7 +219,7 @@ return [
*
* @var bool
*/
"DOMPDF_ENABLE_JAVASCRIPT" => true,
"DOMPDF_ENABLE_JAVASCRIPT" => false,
/**
* Enable remote file access

View File

@@ -1,5 +1,7 @@
<?php
$SAML2_IDP_AUTHNCONTEXT = env('SAML2_IDP_AUTHNCONTEXT', true);
return [
// Display name, shown to users, for SAML2 option
@@ -139,6 +141,14 @@ return [
// )
// ),
],
'security' => [
// SAML2 Authn context
// When set to false no AuthContext will be sent in the AuthNRequest,
// When set to true (Default) you will get an AuthContext 'exact' 'urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport'.
// Multiple forced values can be passed via a space separated array, For example:
// SAML2_IDP_AUTHNCONTEXT="urn:federation:authentication:windows urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport"
'requestedAuthnContext' => is_string($SAML2_IDP_AUTHNCONTEXT) ? explode(' ', $SAML2_IDP_AUTHNCONTEXT) : $SAML2_IDP_AUTHNCONTEXT,
],
],
];

View File

@@ -133,6 +133,7 @@ return [
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS', false),
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
'start_tls' => env('LDAP_START_TLS', false),
'thumbnail_attribute' => env('LDAP_THUMBNAIL_ATTRIBUTE', null),
],
];

View File

@@ -2,6 +2,7 @@
namespace BookStack\Console\Commands;
use BookStack\Actions\View;
use Illuminate\Console\Command;
class ClearViews extends Command
@@ -36,7 +37,7 @@ class ClearViews extends Command
*/
public function handle()
{
\Views::resetAll();
View::clearAll();
$this->comment('Views cleared');
}
}

View File

@@ -2,6 +2,7 @@
use BookStack\Actions\Activity;
use BookStack\Actions\Comment;
use BookStack\Actions\Favourite;
use BookStack\Actions\Tag;
use BookStack\Actions\View;
use BookStack\Auth\Permissions\EntityPermission;
@@ -9,7 +10,9 @@ use BookStack\Auth\Permissions\JointPermission;
use BookStack\Entities\Tools\SearchIndex;
use BookStack\Entities\Tools\SlugGenerator;
use BookStack\Facades\Permissions;
use BookStack\Interfaces\Favouritable;
use BookStack\Interfaces\Sluggable;
use BookStack\Interfaces\Viewable;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use BookStack\Traits\HasOwner;
@@ -38,7 +41,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
* @method static Builder withLastView()
* @method static Builder withViewCount()
*/
abstract class Entity extends Model implements Sluggable
abstract class Entity extends Model implements Sluggable, Favouritable, Viewable
{
use SoftDeletes;
use HasCreatorAndUpdater;
@@ -297,4 +300,22 @@ abstract class Entity extends Model implements Sluggable
$this->slug = app(SlugGenerator::class)->generate($this);
return $this->slug;
}
/**
* @inheritdoc
*/
public function favourites(): MorphMany
{
return $this->morphMany(Favourite::class, 'favouritable');
}
/**
* Check if the entity is a favourite of the current user.
*/
public function isFavourite(): bool
{
return $this->favourites()
->where('user_id', '=', user()->id)
->exists();
}
}

View File

@@ -75,11 +75,23 @@ class Page extends BookChild
/**
* Get the associated page revisions, ordered by created date.
* @return mixed
* Only provides actual saved page revision instances, Not drafts.
*/
public function revisions()
public function revisions(): HasMany
{
return $this->hasMany(PageRevision::class)->where('type', '=', 'version')->orderBy('created_at', 'desc')->orderBy('id', 'desc');
return $this->allRevisions()
->where('type', '=', 'version')
->orderBy('created_at', 'desc')
->orderBy('id', 'desc');
}
/**
* Get all revision instances assigned to this page.
* Includes all types of revisions.
*/
public function allRevisions(): HasMany
{
return $this->hasMany(PageRevision::class);
}
/**

View File

@@ -0,0 +1,17 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\EntityProvider;
abstract class EntityQuery
{
protected function permissionService(): PermissionService
{
return app()->make(PermissionService::class);
}
protected function entityProvider(): EntityProvider
{
return app()->make(EntityProvider::class);
}
}

View File

@@ -0,0 +1,29 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Facades\DB;
class Popular extends EntityQuery
{
public function run(int $count, int $page, array $filterModels = null, string $action = 'view')
{
$query = $this->permissionService()
->filterRestrictedEntityRelations(View::query(), 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider()->getMorphClasses($filterModels));
}
return $query->with('viewable')
->skip($count * ($page - 1))
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@@ -0,0 +1,32 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\View;
use Illuminate\Support\Collection;
class RecentlyViewed extends EntityQuery
{
public function run(int $count, int $page): Collection
{
$user = user();
if ($user === null || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()->filterRestrictedEntityRelations(
View::query(),
'views',
'viewable_id',
'viewable_type',
'view'
)
->orderBy('views.updated_at', 'desc')
->where('user_id', '=', user()->id);
return $query->with('viewable')
->skip(($page - 1) * $count)
->take($count)
->get()
->pluck('viewable')
->filter();
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace BookStack\Entities\Queries;
use BookStack\Actions\Favourite;
use Illuminate\Database\Query\JoinClause;
class TopFavourites extends EntityQuery
{
public function run(int $count, int $skip = 0)
{
$user = user();
if (is_null($user) || $user->isDefault()) {
return collect();
}
$query = $this->permissionService()
->filterRestrictedEntityRelations(Favourite::query(), 'favourites', 'favouritable_id', 'favouritable_type', 'view')
->select('favourites.*')
->leftJoin('views', function (JoinClause $join) {
$join->on('favourites.favouritable_id', '=', 'views.viewable_id');
$join->on('favourites.favouritable_type', '=', 'views.viewable_type');
$join->where('views.user_id', '=', user()->id);
})
->orderBy('views.views', 'desc')
->where('favourites.user_id', '=', user()->id);
return $query->with('favouritable')
->skip($skip)
->take($count)
->get()
->pluck('favouritable')
->filter();
}
}

View File

@@ -212,7 +212,7 @@ class PageRepo
if (!empty($input['markdown'] ?? '')) {
$pageContent->setNewMarkdown($input['markdown']);
} else {
$pageContent->setNewHTML($input['html']);
$pageContent->setNewHTML($input['html'] ?? '');
}
}

View File

@@ -0,0 +1,69 @@
<?php namespace BookStack\Entities\Tools;
use BookStack\Entities\Models\BookChild;
use BookStack\Entities\Models\Entity;
use Illuminate\Support\Collection;
/**
* Finds the next or previous content of a book element (page or chapter).
*/
class NextPreviousContentLocator
{
protected $relativeBookItem;
protected $flatTree;
protected $currentIndex = null;
/**
* NextPreviousContentLocator constructor.
*/
public function __construct(BookChild $relativeBookItem, Collection $bookTree)
{
$this->relativeBookItem = $relativeBookItem;
$this->flatTree = $this->treeToFlatOrderedCollection($bookTree);
$this->currentIndex = $this->getCurrentIndex();
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getNext(): ?Entity
{
return $this->flatTree->get($this->currentIndex + 1);
}
/**
* Get the next logical entity within the book hierarchy.
*/
public function getPrevious(): ?Entity
{
return $this->flatTree->get($this->currentIndex - 1);
}
/**
* Get the index of the current relative item.
*/
protected function getCurrentIndex(): ?int
{
$index = $this->flatTree->search(function (Entity $entity) {
return get_class($entity) === get_class($this->relativeBookItem)
&& $entity->id === $this->relativeBookItem->id;
});
return $index === false ? null : $index;
}
/**
* Convert a book tree collection to a flattened version
* where all items follow the expected order of user flow.
*/
protected function treeToFlatOrderedCollection(Collection $bookTree): Collection
{
$flatOrdered = collect();
/** @var Entity $item */
foreach ($bookTree->all() as $item) {
$flatOrdered->push($item);
$childPages = $item->visible_pages ?? [];
$flatOrdered = $flatOrdered->concat($childPages);
}
return $flatOrdered;
}
}

View File

@@ -2,11 +2,15 @@
use BookStack\Entities\Models\Page;
use BookStack\Entities\Tools\Markdown\CustomStrikeThroughExtension;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Facades\Theme;
use BookStack\Theming\ThemeEvents;
use BookStack\Util\HtmlContentFilter;
use BookStack\Uploads\ImageRepo;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
use Illuminate\Support\Str;
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Table\TableExtension;
@@ -30,6 +34,7 @@ class PageContent
*/
public function setNewHTML(string $html)
{
$html = $this->extractBase64Images($this->page, $html);
$this->page->html = $this->formatHtml($html);
$this->page->text = $this->toPlainText();
$this->page->markdown = '';
@@ -60,19 +65,65 @@ class PageContent
return $converter->convertToHtml($markdown);
}
/**
* Convert all base64 image data to saved images
*/
public function extractBase64Images(Page $page, string $htmlText): string
{
if (empty($htmlText) || strpos($htmlText, 'data:image') === false) {
return $htmlText;
}
$doc = $this->loadDocumentFromHtml($htmlText);
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
$xPath = new DOMXPath($doc);
$imageRepo = app()->make(ImageRepo::class);
$allowedExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp'];
// Get all img elements with image data blobs
$imageNodes = $xPath->query('//img[contains(@src, \'data:image\')]');
foreach ($imageNodes as $imageNode) {
$imageSrc = $imageNode->getAttribute('src');
[$dataDefinition, $base64ImageData] = explode(',', $imageSrc, 2);
$extension = strtolower(preg_split('/[\/;]/', $dataDefinition)[1] ?? 'png');
// Validate extension
if (!in_array($extension, $allowedExtensions)) {
$imageNode->setAttribute('src', '');
continue;
}
// Save image from data with a random name
$imageName = 'embedded-image-' . Str::random(8) . '.' . $extension;
try {
$image = $imageRepo->saveNewFromData($imageName, base64_decode($base64ImageData), 'gallery', $page->id);
$imageNode->setAttribute('src', $image->url);
} catch (ImageUploadException $exception) {
$imageNode->setAttribute('src', '');
}
}
// Generate inner html as a string
$html = '';
foreach ($childNodes as $childNode) {
$html .= $doc->saveHTML($childNode);
}
return $html;
}
/**
* Formats a page's html to be tagged correctly within the system.
*/
protected function formatHtml(string $htmlText): string
{
if ($htmlText == '') {
if (empty($htmlText)) {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($htmlText);
$container = $doc->documentElement;
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
@@ -111,7 +162,7 @@ class PageContent
protected function updateLinks(DOMXPath $xpath, string $old, string $new)
{
$old = str_replace('"', '', $old);
$matchingLinks = $xpath->query('//body//*//*[@href="'.$old.'"]');
$matchingLinks = $xpath->query('//body//*//*[@href="' . $old . '"]');
foreach ($matchingLinks as $domElem) {
$domElem->setAttribute('href', $new);
}
@@ -164,12 +215,12 @@ class PageContent
/**
* Render the page for viewing
*/
public function render(bool $blankIncludes = false) : string
public function render(bool $blankIncludes = false): string
{
$content = $this->page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
$content = HtmlContentFilter::removeScripts($content);
}
if ($blankIncludes) {
@@ -190,9 +241,7 @@ class PageContent
return [];
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlContent, 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($htmlContent);
$xPath = new DOMXPath($doc);
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
@@ -232,7 +281,7 @@ class PageContent
/**
* Remove any page include tags within the given HTML.
*/
protected function blankPageIncludes(string $html) : string
protected function blankPageIncludes(string $html): string
{
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
}
@@ -240,7 +289,7 @@ class PageContent
/**
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
*/
protected function parsePageIncludes(string $html) : string
protected function parsePageIncludes(string $html): string
{
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
@@ -283,9 +332,7 @@ class PageContent
protected function fetchSectionOfPage(Page $page, string $sectionId): string
{
$topLevelTags = ['table', 'ul', 'ol'];
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML(mb_convert_encoding('<body>'.$page->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$doc = $this->loadDocumentFromHtml($page->html);
// Search included content for the id given and blank out if not exists.
$matchingElem = $doc->getElementById($sectionId);
@@ -310,63 +357,14 @@ class PageContent
}
/**
* Escape script tags within HTML content.
* Create and load a DOMDocument from the given html content.
*/
protected function escapeScripts(string $html) : string
protected function loadDocumentFromHtml(string $html): DOMDocument
{
if (empty($html)) {
return $html;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$html = '<body>' . $html . '</body>';
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove clickable links to JavaScript URI
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
foreach ($badLinks as $badLink) {
$badLink->parentNode->removeChild($badLink);
}
// Remove forms with calls to JavaScript URI
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
foreach ($badForms as $badForm) {
$badForm->parentNode->removeChild($badForm);
}
// Remove meta tag to prevent external redirects
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
foreach ($metaTags as $metaTag) {
$metaTag->parentNode->removeChild($metaTag);
}
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
foreach ($badIframes as $badIframe) {
$badIframe->parentNode->removeChild($badIframe);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
return $doc;
}
}

View File

@@ -151,6 +151,7 @@ class TrashCan
protected function destroyPage(Page $page): int
{
$this->destroyCommonRelations($page);
$page->allRevisions()->delete();
// Delete Attached Files
$attachmentService = app(AttachmentService::class);
@@ -317,6 +318,7 @@ class TrashCan
$entity->jointPermissions()->delete();
$entity->searchTerms()->delete();
$entity->deletions()->delete();
$entity->favourites()->delete();
if ($entity instanceof HasCoverImage && $entity->cover) {
$imageService = app()->make(ImageService::class);

View File

@@ -9,7 +9,6 @@ use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class Handler extends ExceptionHandler
{
@@ -58,29 +57,6 @@ class Handler extends ExceptionHandler
return $this->renderApiException($e);
}
// Handle notify exceptions which will redirect to the
// specified location then show a notification message.
if ($this->isExceptionType($e, NotifyException::class)) {
$message = $this->getOriginalMessage($e);
if (!empty($message)) {
session()->flash('error', $message);
}
return redirect($e->redirectLocation);
}
// Handle pretty exceptions which will show a friendly application-fitting page
// Which will include the basic message to point the user roughly to the cause.
if ($this->isExceptionType($e, PrettyException::class) && !config('app.debug')) {
$message = $this->getOriginalMessage($e);
$code = ($e->getCode() === 0) ? 500 : $e->getCode();
return response()->view('errors/' . $code, ['message' => $message], $code);
}
// Handle 404 errors with a loaded session to enable showing user-specific information
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
return \Route::respondWithRoute('fallback');
}
return parent::render($request, $e);
}
@@ -119,30 +95,6 @@ class Handler extends ExceptionHandler
return new JsonResponse($responseData, $code, $headers);
}
/**
* Check the exception chain to compare against the original exception type.
*/
protected function isExceptionType(Exception $e, string $type): bool
{
do {
if (is_a($e, $type)) {
return true;
}
} while ($e = $e->getPrevious());
return false;
}
/**
* Get original exception message.
*/
protected function getOriginalMessage(Exception $e): string
{
do {
$message = $e->getMessage();
} while ($e = $e->getPrevious());
return $message;
}
/**
* Convert an authentication exception into an unauthenticated response.
*

View File

@@ -1,8 +1,10 @@
<?php namespace BookStack\Exceptions;
class NotifyException extends \Exception
{
use Exception;
use Illuminate\Contracts\Support\Responsable;
class NotifyException extends Exception implements Responsable
{
public $message;
public $redirectLocation;
@@ -15,4 +17,19 @@ class NotifyException extends \Exception
$this->redirectLocation = $redirectLocation;
parent::__construct();
}
/**
* Send the response for this type of exception.
* @inheritdoc
*/
public function toResponse($request)
{
$message = $this->getMessage();
if (!empty($message)) {
session()->flash('error', $message);
}
return redirect($this->redirectLocation);
}
}

View File

@@ -1,6 +1,43 @@
<?php namespace BookStack\Exceptions;
class PrettyException extends \Exception
{
use Exception;
use Illuminate\Contracts\Support\Responsable;
class PrettyException extends Exception implements Responsable
{
/**
* @var ?string
*/
protected $subtitle = null;
/**
* @var ?string
*/
protected $details = null;
/**
* Render a response for when this exception occurs.
* @inheritdoc
*/
public function toResponse($request)
{
$code = ($this->getCode() === 0) ? 500 : $this->getCode();
return response()->view('errors.' . $code, [
'message' => $this->getMessage(),
'subtitle' => $this->subtitle,
'details' => $this->details,
], $code);
}
public function setSubtitle(string $subtitle): self
{
$this->subtitle = $subtitle;
return $this;
}
public function setDetails(string $details): self
{
$this->details = $details;
return $this;
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Images extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'images';
}
}

View File

@@ -1,16 +0,0 @@
<?php namespace BookStack\Facades;
use Illuminate\Support\Facades\Facade;
class Views extends Facade
{
/**
* Get the registered name of the component.
*
* @return string
*/
protected static function getFacadeAccessor()
{
return 'views';
}
}

View File

@@ -60,6 +60,8 @@ class PageApiController extends ApiController
*
* Any HTML content provided should be kept to a single-block depth of plain HTML
* elements to remain compatible with the BookStack front-end and editors.
* Any images included via base64 data URIs will be extracted and saved as gallery
* images against the page during upload.
*/
public function create(Request $request)
{

View File

@@ -14,16 +14,14 @@ use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
{
protected $attachmentService;
protected $attachment;
protected $pageRepo;
/**
* AttachmentController constructor.
*/
public function __construct(AttachmentService $attachmentService, Attachment $attachment, PageRepo $pageRepo)
public function __construct(AttachmentService $attachmentService, PageRepo $pageRepo)
{
$this->attachmentService = $attachmentService;
$this->attachment = $attachment;
$this->pageRepo = $pageRepo;
}
@@ -67,7 +65,7 @@ class AttachmentController extends Controller
'file' => 'required|file'
]);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
@@ -89,7 +87,7 @@ class AttachmentController extends Controller
*/
public function getUpdateForm(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
@@ -104,8 +102,8 @@ class AttachmentController extends Controller
*/
public function update(Request $request, string $attachmentId)
{
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
@@ -160,7 +158,7 @@ class AttachmentController extends Controller
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
$this->attachmentService->saveNewFromLink($attachmentName, $link, intval($pageId));
return view('attachments.manager-link-form', [
'pageId' => $pageId,
@@ -202,9 +200,10 @@ class AttachmentController extends Controller
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function get(string $attachmentId)
public function get(Request $request, string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
try {
$page = $this->pageRepo->getById($attachment->uploaded_to);
} catch (NotFoundException $exception) {
@@ -217,8 +216,13 @@ class AttachmentController extends Controller
return redirect($attachment->path);
}
$fileName = $attachment->getFileName();
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
if ($request->get('open') === 'true') {
return $this->inlineDownloadResponse($attachmentContents, $fileName);
}
return $this->downloadResponse($attachmentContents, $fileName);
}
/**
@@ -227,7 +231,8 @@ class AttachmentController extends Controller
*/
public function delete(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
/** @var Attachment $attachment */
$attachment = Attachment::query()->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);
$this->attachmentService->deleteFile($attachment);
return response()->json(['message' => trans('entities.attachments_deleted')]);

View File

@@ -12,6 +12,7 @@ use BookStack\Http\Controllers\Controller;
use BookStack\Theming\ThemeEvents;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class LoginController extends Controller
{
@@ -198,4 +199,19 @@ class LoginController extends Controller
return redirect('/login');
}
/**
* Get the failed login response instance.
*
* @param \Illuminate\Http\Request $request
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Illuminate\Validation\ValidationException
*/
protected function sendFailedLoginResponse(Request $request)
{
throw ValidationException::withMessages([
$this->username() => [trans('auth.failed')],
])->redirectTo('/login');
}
}

View File

@@ -2,6 +2,7 @@
use Activity;
use BookStack\Actions\ActivityType;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Models\Bookshelf;
use BookStack\Entities\Tools\PermissionsUpdater;
@@ -11,7 +12,6 @@ use BookStack\Exceptions\ImageUploadException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class BookController extends Controller
{
@@ -112,7 +112,7 @@ class BookController extends Controller
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->visible()->get();
Views::add($book);
View::incrementFor($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use Activity;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Entities\Tools\ShelfContext;
@@ -109,7 +110,7 @@ class BookshelfController extends Controller
->values()
->all();
Views::add($shelf);
View::incrementFor($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$view = setting()->getForCurrentUser('bookshelf_view_type');

View File

@@ -1,15 +1,16 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\MoveOperationException;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class ChapterController extends Controller
{
@@ -64,7 +65,8 @@ class ChapterController extends Controller
$sidebarTree = (new BookContents($chapter->book))->getTree();
$pages = $chapter->getVisiblePages();
Views::add($chapter);
$nextPreviousLocator = new NextPreviousContentLocator($chapter, $sidebarTree);
View::incrementFor($chapter);
$this->setPageTitle($chapter->getShortName());
return view('chapters.show', [
@@ -72,7 +74,9 @@ class ChapterController extends Controller
'chapter' => $chapter,
'current' => $chapter,
'sidebarTree' => $sidebarTree,
'pages' => $pages
'pages' => $pages,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}

View File

@@ -6,6 +6,7 @@ use BookStack\Facades\Activity;
use BookStack\Interfaces\Loggable;
use BookStack\HasCreatorAndUpdater;
use BookStack\Model;
use finfo;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException;
@@ -121,6 +122,20 @@ abstract class Controller extends BaseController
]);
}
/**
* Create a file download response that provides the file with a content-type
* correct for the file, in a way so the browser can show the content in browser.
*/
protected function inlineDownloadResponse(string $content, string $fileName): Response
{
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime = $finfo->buffer($content) ?: 'application/octet-stream';
return response()->make($content, 200, [
'Content-Type' => $mime,
'Content-Disposition' => 'inline; filename="' . $fileName . '"'
]);
}
/**
* Show a positive, successful notification to the user on next view load.
*/

View File

@@ -0,0 +1,95 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Entities\Models\Entity;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Interfaces\Favouritable;
use BookStack\Model;
use Illuminate\Http\Request;
class FavouriteController extends Controller
{
/**
* Show a listing of all favourite items for the current user.
*/
public function index(Request $request)
{
$viewCount = 20;
$page = intval($request->get('page', 1));
$favourites = (new TopFavourites)->run($viewCount + 1, (($page - 1) * $viewCount));
$hasMoreLink = ($favourites->count() > $viewCount) ? url("/favourites?page=" . ($page+1)) : null;
return view('common.detailed-listing-with-more', [
'title' => trans('entities.my_favourites'),
'entities' => $favourites->slice(0, $viewCount),
'hasMoreLink' => $hasMoreLink,
]);
}
/**
* Add a new item as a favourite.
*/
public function add(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->firstOrCreate([
'user_id' => user()->id,
]);
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* Remove an item as a favourite.
*/
public function remove(Request $request)
{
$favouritable = $this->getValidatedModelFromRequest($request);
$favouritable->favourites()->where([
'user_id' => user()->id,
])->delete();
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
'name' => $favouritable->name,
]));
return redirect()->back();
}
/**
* @throws \Illuminate\Validation\ValidationException
* @throws \Exception
*/
protected function getValidatedModelFromRequest(Request $request): Favouritable
{
$modelInfo = $this->validate($request, [
'type' => 'required|string',
'id' => 'required|integer',
]);
if (!class_exists($modelInfo['type'])) {
throw new \Exception('Model not found');
}
/** @var Model $model */
$model = new $modelInfo['type'];
if (! $model instanceof Favouritable) {
throw new \Exception('Model not favouritable');
}
$modelInstance = $model->newQuery()
->where('id', '=', $modelInfo['id'])
->first(['id', 'name']);
$inaccessibleEntity = ($modelInstance instanceof Entity && !userCan('view', $modelInstance));
if (is_null($modelInstance) || $inaccessibleEntity) {
throw new \Exception('Model instance not found');
}
return $modelInstance;
}
}

View File

@@ -2,11 +2,12 @@
use Activity;
use BookStack\Entities\Models\Book;
use BookStack\Entities\Queries\RecentlyViewed;
use BookStack\Entities\Queries\TopFavourites;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\BookRepo;
use BookStack\Entities\Repos\BookshelfRepo;
use Illuminate\Http\Response;
use Views;
class HomeController extends Controller
@@ -32,12 +33,13 @@ class HomeController extends Controller
$recentFactor = count($draftPages) > 0 ? 0.5 : 1;
$recents = $this->isSignedIn() ?
Views::getUserRecentlyViewed(12*$recentFactor, 1)
(new RecentlyViewed)->run(12*$recentFactor, 1)
: Book::visible()->orderBy('created_at', 'desc')->take(12 * $recentFactor)->get();
$favourites = (new TopFavourites)->run(6);
$recentlyUpdatedPages = Page::visible()->with('book')
->where('draft', false)
->orderBy('updated_at', 'desc')
->take(12)
->take($favourites->count() > 0 ? 6 : 12)
->get();
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
@@ -51,6 +53,7 @@ class HomeController extends Controller
'recents' => $recents,
'recentlyUpdatedPages' => $recentlyUpdatedPages,
'draftPages' => $draftPages,
'favourites' => $favourites,
];
// Add required list ordering & sorting for books & shelves views.
@@ -105,7 +108,7 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials.custom-head-content');
return view('partials.custom-head');
}
/**

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Exceptions\NotFoundException;
use BookStack\Http\Controllers\Controller;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
@@ -27,12 +28,15 @@ class ImageController extends Controller
/**
* Provide an image file from storage.
* @throws NotFoundException
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
throw (new NotFoundException(trans('errors.image_not_found')))
->setSubtitle(trans('errors.image_not_found_subtitle'))
->setDetails(trans('errors.image_not_found_details'));
}
return response()->file($path);

View File

@@ -1,19 +1,19 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\View;
use BookStack\Entities\Tools\BookContents;
use BookStack\Entities\Tools\NextPreviousContentLocator;
use BookStack\Entities\Tools\PageContent;
use BookStack\Entities\Tools\PageEditActivity;
use BookStack\Entities\Models\Page;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Entities\Tools\PermissionsUpdater;
use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Throwable;
use Views;
class PageController extends Controller
{
@@ -142,7 +142,9 @@ class PageController extends Controller
$page->load(['comments.createdBy']);
}
Views::add($page);
$nextPreviousLocator = new NextPreviousContentLocator($page, $sidebarTree);
View::incrementFor($page);
$this->setPageTitle($page->getShortName());
return view('pages.show', [
'page' => $page,
@@ -150,7 +152,9 @@ class PageController extends Controller
'current' => $page,
'sidebarTree' => $sidebarTree,
'commentsEnabled' => $commentsEnabled,
'pageNav' => $pageNav
'pageNav' => $pageNav,
'next' => $nextPreviousLocator->getNext(),
'previous' => $nextPreviousLocator->getPrevious(),
]);
}
@@ -243,8 +247,8 @@ class PageController extends Controller
$updateTime = $draft->updated_at->timestamp;
return response()->json([
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'status' => 'success',
'message' => trans('entities.pages_edit_draft_save_at'),
'timestamp' => $updateTime
]);
}
@@ -267,7 +271,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@@ -283,7 +287,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName' => $page->getShortName()]));
return view('pages.delete', [
'book' => $page->book,
'page' => $page,
@@ -295,7 +299,6 @@ class PageController extends Controller
* Remove the specified page from storage.
* @throws NotFoundException
* @throws Throwable
* @throws NotifyException
*/
public function destroy(string $bookSlug, string $pageSlug)
{
@@ -311,7 +314,6 @@ class PageController extends Controller
/**
* Remove the specified draft page from storage.
* @throws NotFoundException
* @throws NotifyException
* @throws Throwable
*/
public function destroyDraft(string $bookSlug, int $pageId)
@@ -340,9 +342,9 @@ class PageController extends Controller
->paginate(20)
->setPath(url('/pages/recently-updated'));
return view('pages.detailed-listing', [
return view('common.detailed-listing-paginated', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
'entities' => $pages
]);
}
@@ -380,7 +382,7 @@ class PageController extends Controller
try {
$parent = $this->pageRepo->move($page, $entitySelection);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -424,7 +426,7 @@ class PageController extends Controller
try {
$pageCopy = $this->pageRepo->copy($page, $entitySelection, $newName);
} catch (Exception $exception) {
if ($exception instanceof PermissionsException) {
if ($exception instanceof PermissionsException) {
$this->showPermissionError();
}
@@ -445,7 +447,7 @@ class PageController extends Controller
$page = $this->pageRepo->getBySlug($bookSlug, $pageSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
return view('pages.permissions', [
'page' => $page,
'page' => $page,
]);
}

View File

@@ -1,6 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\Queries\Popular;
use BookStack\Entities\Tools\SearchRunner;
use BookStack\Entities\Tools\ShelfContext;
use BookStack\Entities\Tools\SearchOptions;
@@ -9,16 +9,13 @@ use Illuminate\Http\Request;
class SearchController extends Controller
{
protected $viewService;
protected $searchRunner;
protected $entityContextManager;
public function __construct(
ViewService $viewService,
SearchRunner $searchRunner,
ShelfContext $entityContextManager
) {
$this->viewService = $viewService;
$this->searchRunner = $searchRunner;
$this->entityContextManager = $entityContextManager;
}
@@ -82,7 +79,7 @@ class SearchController extends Controller
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchRunner->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
$entities = (new Popular)->run(20, 0, $entityTypes, $permission);
}
return view('search.entity-ajax-list', ['entities' => $entities]);

View File

@@ -28,6 +28,7 @@ class Localization
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'he' => 'he_IL',
'hr' => 'hr_HR',
'id' => 'id_ID',
'it' => 'it_IT',
'ja' => 'ja',

View File

@@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Favouritable
{
/**
* Get the related favourite instances.
*/
public function favourites(): MorphMany;
}

View File

@@ -0,0 +1,11 @@
<?php namespace BookStack\Interfaces;
use Illuminate\Database\Eloquent\Relations\MorphMany;
interface Viewable
{
/**
* Get all view instances for this viewable model.
*/
public function views(): MorphMany;
}

View File

@@ -3,7 +3,6 @@
namespace BookStack\Providers;
use BookStack\Actions\ActivityService;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Theming\ThemeService;
use BookStack\Uploads\ImageService;
@@ -32,10 +31,6 @@ class CustomFacadeProvider extends ServiceProvider
return $this->app->make(ActivityService::class);
});
$this->app->singleton('views', function () {
return $this->app->make(ViewService::class);
});
$this->app->singleton('images', function () {
return $this->app->make(ImageService::class);
});

View File

@@ -53,9 +53,9 @@ class ThemeService
/**
* @see SocialAuthService::addSocialDriver
*/
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler)
public function addSocialDriver(string $driverName, array $config, string $socialiteHandler, callable $configureForRedirect = null)
{
$socialAuthService = app()->make(SocialAuthService::class);
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler);
$socialAuthService->addSocialDriver($driverName, $config, $socialiteHandler, $configureForRedirect);
}
}

View File

@@ -3,12 +3,14 @@
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int id
* @property string name
* @property string path
* @property string extension
* @property ?Page page
* @property bool external
*/
class Attachment extends Model
@@ -31,9 +33,8 @@ class Attachment extends Model
/**
* Get the page this file was uploaded to.
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function page()
public function page(): BelongsTo
{
return $this->belongsTo(Page::class, 'uploaded_to');
}
@@ -41,12 +42,12 @@ class Attachment extends Model
/**
* Get the url of this file.
*/
public function getUrl(): string
public function getUrl($openInline = false): string
{
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
return url('/attachments/' . $this->id);
return url('/attachments/' . $this->id . ($openInline ? '?open=true' : ''));
}
/**

View File

@@ -3,8 +3,10 @@
use BookStack\Exceptions\FileUploadException;
use Exception;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Support\Str;
use Log;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class AttachmentService
@@ -38,11 +40,9 @@ class AttachmentService
/**
* Get an attachment from storage.
* @param Attachment $attachment
* @return string
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
* @throws FileNotFoundException
*/
public function getAttachmentFromStorage(Attachment $attachment)
public function getAttachmentFromStorage(Attachment $attachment): string
{
return $this->getStorage()->get($attachment->path);
}
@@ -202,7 +202,7 @@ class AttachmentService
try {
$storage->put($attachmentPath, $attachmentData);
} catch (Exception $e) {
\Log::error('Error when attempting file upload:' . $e->getMessage());
Log::error('Error when attempting file upload:' . $e->getMessage());
throw new FileUploadException(trans('errors.path_not_writable', ['filePath' => $attachmentPath]));
}

View File

@@ -3,8 +3,17 @@
use BookStack\Entities\Models\Page;
use BookStack\Model;
use BookStack\Traits\HasCreatorAndUpdater;
use Images;
/**
* @property int $id
* @property string $name
* @property string $url
* @property string $path
* @property string $type
* @property int $uploaded_to
* @property int $created_by
* @property int $updated_by
*/
class Image extends Model
{
use HasCreatorAndUpdater;
@@ -14,23 +23,18 @@ class Image extends Model
/**
* Get a thumbnail for this image.
* @param int $width
* @param int $height
* @param bool|false $keepRatio
* @return string
* @throws \Exception
*/
public function getThumb($width, $height, $keepRatio = false)
public function getThumb(int $width, int $height, bool $keepRatio = false): string
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
return app()->make(ImageService::class)->getThumbnail($this, $width, $height, $keepRatio);
}
/**
* Get the page this image has been uploaded to.
* Only applicable to gallery or drawio image types.
* @return Page|null
*/
public function getPage()
public function getPage(): ?Page
{
return $this->belongsTo(Page::class, 'uploaded_to')->first();
}

View File

@@ -130,6 +130,17 @@ class ImageRepo
return $image;
}
/**
* Save a new image from an existing image data string.
* @throws ImageUploadException
*/
public function saveNewFromData(string $imageName, string $imageData, string $type, int $uploadedTo = 0)
{
$image = $this->imageService->saveNew($imageName, $imageData, $type, $uploadedTo);
$this->loadThumbs($image);
return $image;
}
/**
* Save a drawing the the database.
* @throws ImageUploadException

View File

@@ -8,6 +8,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Contracts\Filesystem\Filesystem as Storage;
use Illuminate\Support\Str;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
@@ -106,8 +107,7 @@ class ImageService
}
try {
$storage->put($fullPath, $imageData);
$storage->setVisibility($fullPath, 'public');
$this->saveImageDataInPublicSpace($storage, $fullPath, $imageData);
} catch (Exception $e) {
\Log::error('Error when attempting image upload:' . $e->getMessage());
throw new ImageUploadException(trans('errors.path_not_writable', ['filePath' => $fullPath]));
@@ -132,6 +132,25 @@ class ImageService
return $image;
}
/**
* Save image data for the given path in the public space, if possible,
* for the provided storage mechanism.
*/
protected function saveImageDataInPublicSpace(Storage $storage, string $path, string $data)
{
$storage->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.
$usingS3 = strtolower(config('filesystems.images')) === 's3';
$usingS3Like = $usingS3 && !is_null(config('filesystems.disks.s3.endpoint'));
if (!$usingS3Like) {
$storage->setVisibility($path, 'public');
}
}
/**
* Clean up an image file name to be both URL and storage safe.
*/
@@ -191,8 +210,7 @@ class ImageService
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->saveImageDataInPublicSpace($storage, $thumbFilePath, $thumbData);
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);

View File

@@ -26,6 +26,7 @@ class UserAvatars
}
try {
$this->destroyAllForUser($user);
$avatar = $this->saveAvatarImage($user);
$user->avatar()->associate($avatar);
$user->save();
@@ -34,6 +35,35 @@ class UserAvatars
}
}
/**
* Assign a new avatar image to the given user using the given image data.
*/
public function assignToUserFromExistingData(User $user, string $imageData, string $extension): void
{
try {
$this->destroyAllForUser($user);
$avatar = $this->createAvatarImageFromData($user, $imageData, $extension);
$user->avatar()->associate($avatar);
$user->save();
} catch (Exception $e) {
Log::error('Failed to save user avatar image');
}
}
/**
* Destroy all user avatars uploaded to the given user.
*/
public function destroyAllForUser(User $user)
{
$profileImages = Image::query()->where('type', '=', 'user')
->where('uploaded_to', '=', $user->id)
->get();
foreach ($profileImages as $image) {
$this->imageService->destroy($image);
}
}
/**
* Save an avatar image from an external service.
* @throws Exception
@@ -50,8 +80,16 @@ class UserAvatars
];
$userAvatarUrl = strtr($avatarUrl, $replacements);
$imageName = str_replace(' ', '-', $user->id . '-avatar.png');
$imageData = $this->getAvatarImageData($userAvatarUrl);
return $this->createAvatarImageFromData($user, $imageData, 'png');
}
/**
* Creates a new image instance and saves it in the system as a new user avatar image.
*/
protected function createAvatarImageFromData(User $user, string $imageData, string $extension): Image
{
$imageName = str_replace(' ', '-', $user->id . '-avatar.' . $extension);
$image = $this->imageService->saveNew($imageName, $imageData, 'user', $user->id);
$image->created_by = $user->id;

View File

@@ -0,0 +1,70 @@
<?php namespace BookStack\Util;
use DOMDocument;
use DOMNodeList;
use DOMXPath;
class HtmlContentFilter
{
/**
* Remove all of the script elements from the given HTML.
*/
public static function removeScripts(string $html): string
{
if (empty($html)) {
return $html;
}
$html = '<body>' . $html . '</body>';
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//script');
static::removeNodes($scriptElems);
// Remove clickable links to JavaScript URI
$badLinks = $xPath->query('//*[contains(@href, \'javascript:\')]');
static::removeNodes($badLinks);
// Remove forms with calls to JavaScript URI
$badForms = $xPath->query('//*[contains(@action, \'javascript:\')] | //*[contains(@formaction, \'javascript:\')]');
static::removeNodes($badForms);
// Remove meta tag to prevent external redirects
$metaTags = $xPath->query('//meta[contains(@content, \'url\')]');
static::removeNodes($metaTags);
// Remove data or JavaScript iFrames
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
static::removeNodes($badIframes);
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
}
/**
* Removed all of the given DOMNodes.
*/
protected static function removeNodes(DOMNodeList $nodes): void
{
foreach ($nodes as $node) {
$node->parentNode->removeChild($node);
}
}
}

View File

@@ -8,6 +8,7 @@
"php": "^7.3|^8.0",
"ext-curl": "*",
"ext-dom": "*",
"ext-fileinfo": "*",
"ext-gd": "*",
"ext-json": "*",
"ext-mbstring": "*",

624
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -15,7 +15,7 @@ class CreateSearchIndexTable extends Migration
{
Schema::create('search_terms', function (Blueprint $table) {
$table->increments('id');
$table->string('term', 200);
$table->string('term', 180);
$table->string('entity_type', 100);
$table->integer('entity_id');
$table->integer('score');

View File

@@ -14,7 +14,7 @@ class AddRoleExternalAuthId extends Migration
public function up()
{
Schema::table('roles', function (Blueprint $table) {
$table->string('external_auth_id', 200)->default('');
$table->string('external_auth_id', 180)->default('');
$table->index('external_auth_id');
});
}

View File

@@ -37,8 +37,8 @@ class CreateBookshelvesTable extends Migration
Schema::create('bookshelves', function (Blueprint $table) {
$table->increments('id');
$table->string('name', 200);
$table->string('slug', 200);
$table->string('name', 180);
$table->string('slug', 180);
$table->text('description');
$table->integer('created_by')->nullable()->default(null);
$table->integer('updated_by')->nullable()->default(null);

View File

@@ -15,7 +15,7 @@ class AddUserSlug extends Migration
public function up()
{
Schema::table('users', function (Blueprint $table) {
$table->string('slug', 250);
$table->string('slug', 180);
});
$slugMap = [];

View File

@@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateFavouritesTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('favourites', function (Blueprint $table) {
$table->increments('id');
$table->integer('user_id')->index();
$table->integer('favouritable_id');
$table->string('favouritable_type', 100);
$table->timestamps();
$table->index(['favouritable_id', 'favouritable_type'], 'favouritable_index');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('favourites');
}
}

View File

@@ -95,4 +95,18 @@ Theme::listen(ThemeEvents::APP_BOOT, function($app) {
'name' => 'Reddit',
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle');
});
```
In some cases you may need to customize the driver before it performs a redirect.
This can be done by providing a callback as a fourth parameter like so:
```php
Theme::addSocialDriver('reddit', [
'client_id' => 'abc123',
'client_secret' => 'def456789',
'name' => 'Reddit',
], '\SocialiteProviders\Reddit\RedditExtendSocialite@handle', function($driver) {
$driver->with(['prompt' => 'select_account']);
$driver->scopes(['open_id']);
});
```

1350
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,17 +16,17 @@
},
"devDependencies": {
"chokidar-cli": "^2.1.0",
"esbuild": "0.7.8",
"esbuild": "0.12.8",
"livereload": "^0.9.3",
"npm-run-all": "^4.1.5",
"punycode": "^2.1.1",
"sass": "^1.32.8"
"sass": "^1.34.1"
},
"dependencies": {
"clipboard": "^2.0.8",
"codemirror": "^5.60.0",
"codemirror": "^5.61.1",
"dropzone": "^5.9.2",
"markdown-it": "^11.0.1",
"markdown-it": "^12.0.6",
"markdown-it-task-lists": "^2.1.1",
"sortablejs": "^1.13.0"
}

97
public/dist/app.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,4 +1,3 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm4.24 16L12 15.45 7.77 18l1.12-4.81-3.73-3.23 4.92-.42L12 5l1.92 4.53 4.92.42-3.73 3.23L16.23 18z"/>
</svg>

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 265 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M22 9.24l-7.19-.62L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21 12 17.27 18.18 21l-1.63-7.03L22 9.24zM12 15.4l-3.76 2.27 1-4.28-3.32-2.88 4.38-.38L12 6.1l1.71 4.04 4.38.38-3.32 2.88 1 4.28L12 15.4z"/></svg>

After

Width:  |  Height:  |  Size: 270 B

View File

@@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M17.63 5.84C17.27 5.33 16.67 5 16 5L5 5.01C3.9 5.01 3 5.9 3 7v10c0 1.1.9 1.99 2 1.99L16 19c.67 0 1.27-.33 1.63-.84L22 12l-4.37-6.16z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24"><path d="M21.41,11.41l-8.83-8.83C12.21,2.21,11.7,2,11.17,2H4C2.9,2,2,2.9,2,4v7.17c0,0.53,0.21,1.04,0.59,1.41l8.83,8.83 c0.78,0.78,2.05,0.78,2.83,0l7.17-7.17C22.2,13.46,22.2,12.2,21.41,11.41z M6.5,8C5.67,8,5,7.33,5,6.5S5.67,5,6.5,5S8,5.67,8,6.5 S7.33,8,6.5,8z"/></svg>

Before

Width:  |  Height:  |  Size: 258 B

After

Width:  |  Height:  |  Size: 361 B

View File

@@ -0,0 +1,47 @@
/**
* Attachments List
* Adds '?open=true' query to file attachment links
* when ctrl/cmd is pressed down.
* @extends {Component}
*/
class AttachmentsList {
setup() {
this.container = this.$el;
this.setupListeners();
}
setupListeners() {
const isExpectedKey = (event) => event.key === 'Control' || event.key === 'Meta';
window.addEventListener('keydown', event => {
if (isExpectedKey(event)) {
this.addOpenQueryToLinks();
}
}, {passive: true});
window.addEventListener('keyup', event => {
if (isExpectedKey(event)) {
this.removeOpenQueryFromLinks();
}
}, {passive: true});
}
addOpenQueryToLinks() {
const links = this.container.querySelectorAll('a.attachment-file');
for (const link of links) {
if (link.href.split('?')[1] !== 'open=true') {
link.href = link.href + '?open=true';
link.setAttribute('target', '_blank');
}
}
}
removeOpenQueryFromLinks() {
const links = this.container.querySelectorAll('a.attachment-file');
for (const link of links) {
link.href = link.href.split('?')[0];
link.removeAttribute('target');
}
}
}
export default AttachmentsList;

View File

@@ -2,6 +2,7 @@ import addRemoveRows from "./add-remove-rows.js"
import ajaxDeleteRow from "./ajax-delete-row.js"
import ajaxForm from "./ajax-form.js"
import attachments from "./attachments.js"
import attachmentsList from "./attachments-list.js"
import autoSuggest from "./auto-suggest.js"
import backToTop from "./back-to-top.js"
import bookSort from "./book-sort.js"
@@ -56,6 +57,7 @@ const componentMapping = {
"ajax-delete-row": ajaxDeleteRow,
"ajax-form": ajaxForm,
"attachments": attachments,
"attachments-list": attachmentsList,
"auto-suggest": autoSuggest,
"back-to-top": backToTop,
"book-sort": bookSort,

View File

@@ -14,6 +14,7 @@ class MarkdownEditor {
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
this.markdown = new MarkdownIt({html: true});
this.markdown.use(mdTasksLists, {label: true});
@@ -446,8 +447,7 @@ class MarkdownEditor {
this.insertDrawing(resp.data, cursorPos);
DrawIO.close();
}).catch(err => {
window.$events.emit('error', trans('errors.image_upload_error'));
console.log(err);
this.handleDrawingUploadError(err);
});
});
}
@@ -491,12 +491,20 @@ class MarkdownEditor {
this.cm.focus();
DrawIO.close();
}).catch(err => {
window.$events.emit('error', this.imageUploadErrorText);
console.log(err);
this.handleDrawingUploadError(err);
});
});
}
handleDrawingUploadError(error) {
if (error.status === 413) {
window.$events.emit('error', this.serverUploadLimitText);
} else {
window.$events.emit('error', this.imageUploadErrorText);
}
console.log(error);
}
// Make the editor full screen
actionFullScreen() {
const alreadyFullscreen = this.elem.classList.contains('fullscreen');

View File

@@ -283,6 +283,15 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
const id = "image-" + Math.random().toString(16).slice(2);
const loadingImage = window.baseUrl('/loading.gif');
const handleUploadError = (error) => {
if (error.status === 413) {
window.$events.emit('error', wysiwygComponent.serverUploadLimitText);
} else {
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
}
console.log(error);
};
// Handle updating an existing image
if (currentNode) {
DrawIO.close();
@@ -292,8 +301,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
pageEditor.dom.setAttrib(imgElem, 'src', img.url);
pageEditor.dom.setAttrib(currentNode, 'drawio-diagram', img.id);
} catch (err) {
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
handleUploadError(err);
}
return;
}
@@ -307,8 +315,7 @@ function drawIoPlugin(drawioUrl, isDarkMode, pageId, wysiwygComponent) {
pageEditor.dom.get(id).parentNode.setAttribute('drawio-diagram', img.id);
} catch (err) {
pageEditor.dom.remove(id);
window.$events.emit('error', wysiwygComponent.imageUploadErrorText);
console.log(err);
handleUploadError(err);
}
}, 5);
}
@@ -432,6 +439,7 @@ class WysiwygEditor {
this.pageId = this.$opts.pageId;
this.textDirection = this.$opts.textDirection;
this.imageUploadErrorText = this.$opts.imageUploadErrorText;
this.serverUploadLimitText = this.$opts.serverUploadLimitText;
this.isDarkMode = document.documentElement.classList.contains('dark-mode');
this.plugins = "image imagetools table textcolor paste link autolink fullscreen code customhr autosave lists codeeditor media";

View File

@@ -1,5 +1,5 @@
let iFrame = null;
let lastApprovedOrigin;
let onInit, onSave;
/**
@@ -19,15 +19,22 @@ function show(drawioUrl, onInitCallback, onSaveCallback) {
iFrame.setAttribute('class', 'fullscreen');
iFrame.style.backgroundColor = '#FFFFFF';
document.body.appendChild(iFrame);
lastApprovedOrigin = (new URL(drawioUrl)).origin;
}
function close() {
drawEventClose();
}
/**
* Receive and handle a message event from the draw.io window.
* @param {MessageEvent} event
*/
function drawReceive(event) {
if (!event.data || event.data.length < 1) return;
let message = JSON.parse(event.data);
if (event.origin !== lastApprovedOrigin) return;
const message = JSON.parse(event.data);
if (message.event === 'init') {
drawEventInit();
} else if (message.event === 'exit') {
@@ -62,7 +69,7 @@ function drawEventClose() {
}
function drawPostMessage(data) {
iFrame.contentWindow.postMessage(JSON.stringify(data), '*');
iFrame.contentWindow.postMessage(JSON.stringify(data), lastApprovedOrigin);
}
async function upload(imageData, pageUploadedToId) {

View File

@@ -33,7 +33,7 @@ return [
'book_delete' => 'تم حذف الكتاب',
'book_delete_notification' => 'تم حذف الكتاب بنجاح',
'book_sort' => 'تم سرد الكتاب',
'book_sort_notification' => 'تمت إعادة سرد الكتاب بنجاح',
'book_sort_notification' => 'أُعِيدَ سرد الكتاب بنجاح',
// Bookshelves
'bookshelf_create' => 'تم إنشاء رف الكتب',
@@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'تم تحديث الرف',
'bookshelf_delete_notification' => 'تم حذف الرف بنجاح',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'تم التعليق',
'permissions_update' => 'تحديث الأذونات',

View File

@@ -47,7 +47,7 @@ return [
'reset_password_success' => 'تمت استعادة كلمة المرور بنجاح.',
'email_reset_subject' => 'استعد كلمة المرور الخاصة بتطبيق :appName',
'email_reset_text' => 'تم إرسال هذه الرسالة بسبب تلقينا لطلب استعادة كلمة المرور الخاصة بحسابكم.',
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم, فلا حاجة لاتخاذ أية خطوات.',
'email_reset_not_requested' => 'إذا لم يتم طلب استعادة كلمة المرور من قبلكم، فلا حاجة لاتخاذ أية خطوات.',
// Email Confirmation
@@ -62,11 +62,11 @@ return [
'email_not_confirmed' => 'لم يتم تأكيد البريد الإلكتروني',
'email_not_confirmed_text' => 'لم يتم بعد تأكيد عنوان البريد الإلكتروني.',
'email_not_confirmed_click_link' => 'الرجاء الضغط على الرابط المرسل إلى بريدكم الإلكتروني بعد تسجيلكم.',
'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة, بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',
'email_not_confirmed_resend' => 'إذا لم يتم إيجاد الرسالة، بإمكانكم إعادة إرسال رسالة التأكيد عن طريق تعبئة النموذج أدناه.',
'email_not_confirmed_resend_button' => 'إعادة إرسال رسالة التأكيد',
// User Invite
'user_invite_email_subject' => 'تم دعوتك للإنضمام إلى صفحة الحالة الخاصة بـ :app_name!',
'user_invite_email_subject' => 'تمت دعوتك للانضمام إلى صفحة الحالة الخاصة بـ :app_name!',
'user_invite_email_greeting' => 'تم إنشاء حساب مستخدم لك على %site%.',
'user_invite_email_text' => 'انقر على الزر أدناه لتعيين كلمة مرور الحساب والحصول على الوصول:',
'user_invite_email_action' => 'كلمة سر المستخدم',

View File

@@ -40,22 +40,26 @@ return [
'remove' => 'إزالة',
'add' => 'إضافة',
'fullscreen' => 'شاشة كاملة',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'خيارات الترتيب',
'sort_direction_toggle' => 'الترتيب وفق الإتجاه',
'sort_options' => 'خيارات الفرز',
'sort_direction_toggle' => 'الفرز وفق الاتجاه',
'sort_ascending' => 'فرز تصاعدي',
'sort_descending' => 'فرز تنازلي',
'sort_name' => 'الاسم',
'sort_default' => 'Default',
'sort_default' => 'افتراضي',
'sort_created_at' => 'تاريخ الإنشاء',
'sort_updated_at' => 'تاريخ التحديث',
// Misc
'deleted_user' => 'حذف مستخدم',
'deleted_user' => 'المستخدم المحذوف',
'no_activity' => 'لا يوجد نشاط لعرضه',
'no_items' => 'لا توجد عناصر متوفرة',
'back_to_top' => 'العودة للبداية',
'back_to_top' => 'العودة إلى الأعلى',
'toggle_details' => 'عرض / إخفاء التفاصيل',
'toggle_thumbnails' => 'عرض / إخفاء الصور المصغرة',
'details' => 'التفاصيل',
@@ -65,7 +69,7 @@ return [
'breadcrumb' => 'شريط التنقل',
// Header
'header_menu_expand' => 'Expand Header Menu',
'header_menu_expand' => 'عرض القائمة',
'profile_menu' => 'قائمة ملف التعريف',
'view_profile' => 'عرض الملف الشخصي',
'edit_profile' => 'تعديل الملف الشخصي',
@@ -74,16 +78,16 @@ return [
// Layout tabs
'tab_info' => 'معلومات',
'tab_info_label' => 'Tab: Show Secondary Information',
'tab_info_label' => 'تبويب: إظهار المعلومات الثانوية',
'tab_content' => 'المحتوى',
'tab_content_label' => 'Tab: Show Primary Content',
'tab_content_label' => 'تبويب: إظهار المحتوى الأساسي',
// Email Content
'email_action_help' => 'إذا واجهتكم مشكلة بضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
'email_action_help' => 'إذا واجهتكم مشكلة عند ضغط زر ":actionText" فبإمكانكم نسخ الرابط أدناه ولصقه بالمتصفح:',
'email_rights' => 'جميع الحقوق محفوظة',
// Footer Link Options
// Not directly used but available for convenience to users.
'privacy_policy' => 'Privacy Policy',
'terms_of_service' => 'Terms of Service',
'privacy_policy' => 'سياسة الخصوصية',
'terms_of_service' => 'اتفاقية شروط الخدمة',
];

View File

@@ -15,7 +15,7 @@ return [
'image_load_more' => 'المزيد',
'image_image_name' => 'اسم الصورة',
'image_delete_used' => 'هذه الصورة مستخدمة بالصفحات أدناه.',
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة ؟',
'image_delete_confirm_text' => 'هل أنت متأكد من أنك تريد حذف هذه الصورة؟',
'image_select_image' => 'تحديد الصورة',
'image_dropzone' => 'قم بإسقاط الصورة أو اضغط هنا للرفع',
'images_deleted' => 'تم حذف الصور',

View File

@@ -11,7 +11,7 @@ return [
'recently_updated_pages' => 'صفحات حُدثت مؤخراً',
'recently_created_chapters' => 'فصول أنشئت مؤخراً',
'recently_created_books' => 'كتب أنشئت مؤخراً',
'recently_created_shelves' => 'الأرفف المنشأة مؤخراً',
'recently_created_shelves' => 'أرفف أنشئت مؤخراً',
'recently_update' => 'حُدثت مؤخراً',
'recently_viewed' => 'عُرضت مؤخراً',
'recent_activity' => 'نشاطات حديثة',
@@ -27,9 +27,11 @@ return [
'images' => 'صور',
'my_recent_drafts' => 'مسوداتي الحديثة',
'my_recently_viewed' => 'ما عرضته مؤخراً',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'لم تستعرض أي صفحات',
'no_pages_recently_created' => 'لم يتم إنشاء أي صفحات مؤخراً',
'no_pages_recently_updated' => 'لم يتم تحديث أي صفحات مؤخراً',
'no_pages_recently_created' => 'لم تنشأ أي صفحات مؤخراً',
'no_pages_recently_updated' => 'لم تُحدّث أي صفحات مؤخراً',
'export' => 'تصدير',
'export_html' => 'صفحة ويب',
'export_pdf' => 'ملف PDF',
@@ -37,7 +39,7 @@ return [
// Permissions and restrictions
'permissions' => 'الأذونات',
'permissions_intro' => 'في حال التفعيل, ستتم تبدية هذه الأذونات على أذونات الأدوار.',
'permissions_intro' => 'عند التفعيل، سوف تأخذ هذه الأذونات أولوية على أي صلاحية أخرى للدور.',
'permissions_enable' => 'تفعيل الأذونات المخصصة',
'permissions_save' => 'حفظ الأذونات',
'permissions_owner' => 'Owner',
@@ -55,8 +57,8 @@ return [
'search_exact_matches' => 'نتائج مطابقة تماماً',
'search_tags' => 'بحث الوسوم',
'search_options' => 'الخيارات',
'search_viewed_by_me' => 'تم استعراضها من قبلي',
'search_not_viewed_by_me' => 'لم يتم استعراضها من قبلي',
'search_viewed_by_me' => 'استعرضت من قبلي',
'search_not_viewed_by_me' => 'لم تستعرض من قبلي',
'search_permissions_set' => 'حزمة الأذونات',
'search_created_by_me' => 'أنشئت بواسطتي',
'search_updated_by_me' => 'حُدثت بواسطتي',
@@ -74,24 +76,24 @@ return [
'shelves' => 'الأرفف',
'x_shelves' => ':count رف|:count أرفف',
'shelves_long' => 'أرفف الكتب',
'shelves_empty' => 'لم يتم إنشاء أي أرفف',
'shelves_empty' => 'لم ينشأ أي رف',
'shelves_create' => 'إنشاء رف جديد',
'shelves_popular' => 'أرفف شعبية',
'shelves_popular' => 'أرفف رائجة',
'shelves_new' => 'أرفف جديدة',
'shelves_new_action' => 'رف جديد',
'shelves_popular_empty' => 'ستظهر هنا الأرفف الأكثر رواجًا.',
'shelves_new_empty' => 'ستظهر هنا الأرفف التي تم إنشاؤها مؤخرًا.',
'shelves_new_empty' => 'ستظهر هنا الأرفف التي أنشئت مؤخرًا.',
'shelves_save' => 'حفظ الرف',
'shelves_books' => 'كتب على هذا الرف',
'shelves_add_books' => 'إضافة كتب لهذا الرف',
'shelves_drag_books' => 'اسحب الكتب هنا لإضافتها لهذا الرف',
'shelves_drag_books' => 'اسحب الكتب هنا لإضافتها في هذا الرف',
'shelves_empty_contents' => 'لا توجد كتب مخصصة لهذا الرف',
'shelves_edit_and_assign' => 'تحرير الرف لإدراج كتب',
'shelves_edit_named' => 'تحرير رف الكتب: الاسم',
'shelves_edit_named' => 'تحرير رف الكتب :name',
'shelves_edit' => 'تحرير رف الكتب',
'shelves_delete' => 'حذف رف الكتب',
'shelves_delete_named' => 'حذف رف الكتب: الاسم',
'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب مع الاسم ':المُسمى به'. لن يتم حذف الكتب المتضمنة.",
'shelves_delete_named' => 'حذف رف الكتب :name',
'shelves_delete_explain' => "سيؤدي هذا إلى حذف رف الكتب المسمى ':name'، ولن تحذف الكتب المتضمنة فيه.",
'shelves_delete_confirmation' => 'هل أنت متأكد من أنك تريد حذف هذا الرف؟',
'shelves_permissions' => 'أذونات رف الكتب',
'shelves_permissions_updated' => 'تم تحديث أذونات رف الكتب',
@@ -99,7 +101,7 @@ return [
'shelves_copy_permissions_to_books' => 'نسخ أذونات الوصول إلى الكتب',
'shelves_copy_permissions' => 'نسخ الأذونات',
'shelves_copy_permissions_explain' => 'سيؤدي هذا إلى تطبيق إعدادات الأذونات الحالية لهذا الرف على جميع الكتب المتضمنة فيه. قبل التفعيل، تأكد من حفظ أي تغييرات في أذونات هذا الرف.',
'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى: عد الكتب',
'shelves_copy_permission_success' => 'تم نسخ أذونات رف الكتب إلى :count books',
// Books
'book' => 'كتاب',
@@ -115,7 +117,7 @@ return [
'books_create' => 'إنشاء كتاب جديد',
'books_delete' => 'حذف الكتاب',
'books_delete_named' => 'حذف كتاب :bookName',
'books_delete_explain' => 'سيتم حذف كتاب \':bookName\'. ستتم إزالة جميع الفصول والصفحات.',
'books_delete_explain' => 'سيتم حذف كتاب \':bookName\'، وأيضا حذف جميع الفصول والصفحات.',
'books_delete_confirmation' => 'تأكيد حذف الكتاب؟',
'books_edit' => 'تعديل الكتاب',
'books_edit_named' => 'تعديل كتاب :bookName',
@@ -215,7 +217,7 @@ return [
'pages_revisions_created_by' => 'أنشئ بواسطة',
'pages_revisions_date' => 'تاريخ المراجعة',
'pages_revisions_number' => '#',
'pages_revisions_numbered' => 'مراجعة #: رقم تعريفي',
'pages_revisions_numbered' => 'مراجعة #:id',
'pages_revisions_numbered_changes' => 'مراجعة #: رقم تعريفي التغييرات',
'pages_revisions_changelog' => 'سجل التعديل',
'pages_revisions_changes' => 'التعديلات',
@@ -228,7 +230,7 @@ return [
'pages_permissions_active' => 'أذونات الصفحة مفعلة',
'pages_initial_revision' => 'نشر مبدئي',
'pages_initial_name' => 'صفحة جديدة',
'pages_editing_draft_notification' => 'جار تعديل مسودة لم يتم حفظها من :timeDiff.',
'pages_editing_draft_notification' => 'جارٍ تعديل مسودة لم يتم حفظها من :timeDiff.',
'pages_draft_edited_notification' => 'تم تحديث هذه الصفحة منذ ذلك الوقت. من الأفضل التخلص من هذه المسودة.',
'pages_draft_edit_active' => [
'start_a' => ':count من المستخدمين بدأوا بتعديل هذه الصفحة',
@@ -237,7 +239,7 @@ return [
'time_b' => 'في آخر :minCount دقيقة/دقائق',
'message' => 'وقت البدء: احرص على عدم الكتابة فوق تحديثات بعضنا البعض!',
],
'pages_draft_discarded' => 'تم التخلص من المسودة. تم تحديث المحرر بمحتوى الصفحة الحالي',
'pages_draft_discarded' => 'تم التخلص من المسودة وتحديث المحرر بمحتوى الصفحة الحالي',
'pages_specific' => 'صفحة محددة',
'pages_is_template' => 'قالب الصفحة',
@@ -255,14 +257,14 @@ return [
'tags_remove' => 'إزالة هذه العلامة',
'attachments' => 'المرفقات',
'attachments_explain' => 'ارفع بعض الملفات أو أرفق بعض الروابط لعرضها بصفحتك. ستكون الملفات والروابط معروضة في الشريط الجانبي للصفحة.',
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا بلحظتها',
'attachments_explain_instant_save' => 'سيتم حفظ التغييرات هنا آنيا.',
'attachments_items' => 'العناصر المرفقة',
'attachments_upload' => 'رفع ملف',
'attachments_link' => 'إرفاق رابط',
'attachments_set_link' => 'تحديد الرابط',
'attachments_delete' => 'هل أنت متأكد من أنك تريد حذف هذا المرفق؟',
'attachments_dropzone' => 'أسقط الملفات أو اضغط هنا لإرفاق ملف',
'attachments_no_files' => 'لم يتم رفع أي ملفات',
'attachments_no_files' => 'لم تُرفع أي ملفات',
'attachments_explain_link' => 'بالإمكان إرفاق رابط في حال عدم تفضيل رفع ملف. قد يكون الرابط لصفحة أخرى أو لملف في أحد خدمات التخزين السحابي.',
'attachments_link_name' => 'اسم الرابط',
'attachment_link' => 'رابط المرفق',
@@ -287,7 +289,7 @@ return [
'templates_prepend_content' => 'بادئة محتوى الصفحة',
// Profile View
'profile_user_for_x' => 'المستخدم لـ : الوقت',
'profile_user_for_x' => 'المستخدم لـ :time',
'profile_created_content' => 'المحتوى المنشأ',
'profile_not_created_pages' => 'لم يتم إنشاء أي صفحات بواسطة :userName',
'profile_not_created_chapters' => 'لم يتم إنشاء أي فصول بواسطة :userName',
@@ -299,7 +301,7 @@ return [
'comments' => 'تعليقات',
'comment_add' => 'إضافة تعليق',
'comment_placeholder' => 'ضع تعليقاً هنا',
'comment_count' => '{0} ا توجد تعليقات|{1} تعليق واحد|{2} تعليقان|[3,*] :count تعليقات',
'comment_count' => '{0} لا توجد تعليقات|{1} تعليق واحد|{2} تعليقان[3,*] :count تعليقات',
'comment_save' => 'حفظ التعليق',
'comment_saving' => 'جار حفظ التعليق...',
'comment_deleting' => 'جار حذف التعليق...',
@@ -313,8 +315,8 @@ return [
'comment_in_reply_to' => 'رداً على :commentId',
// Revision
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذا الإصدار؟',
'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذا الإصدار؟ سيتم استبدال محتوى الصفحة الحالية.',
'revision_delete_success' => 'تم حذف الإصدار',
'revision_cannot_delete_latest' => 'لايمكن حذف آخر إصدار.'
'revision_delete_confirm' => 'هل أنت متأكد من أنك تريد حذف هذه المراجعة؟',
'revision_restore_confirm' => 'هل أنت متأكد من أنك تريد استعادة هذه المراجعة؟ سيتم استبدال محتوى الصفحة الحالية.',
'revision_delete_success' => 'تم حذف المراجعة',
'revision_cannot_delete_latest' => 'لايمكن حذف آخر مراجعة.'
];

View File

@@ -83,6 +83,9 @@ return [
'404_page_not_found' => 'لم يتم العثور على الصفحة',
'sorry_page_not_found' => 'عفواً, لا يمكن العثور على الصفحة التي تبحث عنها.',
'sorry_page_not_found_permission_warning' => 'إذا كنت تتوقع أن تكون هذه الصفحة موجودة، قد لا يكون لديك تصريح بمشاهدتها.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'return_home' => 'العودة للصفحة الرئيسية',
'error_occurred' => 'حدث خطأ',
'app_down' => ':appName لا يعمل حالياً',

View File

@@ -241,6 +241,7 @@ return [
'es_AR' => 'Español Argentina',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',
'hu' => 'Magyar',
'id' => 'Bahasa Indonesia',
'it' => 'Italian',

View File

@@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'изтрит рафт',
'bookshelf_delete_notification' => 'Рафтът беше успешно изтрит',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'коментирано на',
'permissions_update' => 'updated permissions',

View File

@@ -33,13 +33,17 @@ return [
'copy' => 'Копирай',
'reply' => 'Отговори',
'delete' => 'Изтрий',
'delete_confirm' => 'Confirm Deletion',
'delete_confirm' => 'Потвърдете изтриването',
'search' => 'Търси',
'search_clear' => 'Изчисти търсенето',
'reset' => 'Нулирай',
'remove' => 'Премахване',
'add' => 'Добави',
'fullscreen' => 'Пълен екран',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Опции за сортиране',
@@ -84,6 +88,6 @@ return [
// Footer Link Options
// Not directly used but available for convenience to users.
'privacy_policy' => 'Privacy Policy',
'terms_of_service' => 'Terms of Service',
'privacy_policy' => 'Лични данни',
'terms_of_service' => 'Общи условия',
];

View File

@@ -15,7 +15,7 @@ return [
'image_load_more' => 'Зареди повече',
'image_image_name' => 'Име на изображението',
'image_delete_used' => 'Това изображение е използвано в страницата по-долу.',
'image_delete_confirm_text' => 'Are you sure you want to delete this image?',
'image_delete_confirm_text' => 'Сигурни ли сте, че искате да изтриете това изображение?',
'image_select_image' => 'Изберете изображение',
'image_dropzone' => 'Поставете тук изображение или кликнете тук за да качите',
'images_deleted' => 'Изображението е изтрито',

View File

@@ -22,11 +22,13 @@ return [
'meta_created_name' => 'Създадено преди :timeLength от :user',
'meta_updated' => 'Актуализирано :timeLength',
'meta_updated_name' => 'Актуализирано преди :timeLength от :user',
'meta_owned_name' => 'Owned by :user',
'meta_owned_name' => 'Притежавано от :user',
'entity_select' => 'Избор на обект',
'images' => 'Изображения',
'my_recent_drafts' => 'Моите скорошни драфтове',
'my_recently_viewed' => 'Моите скорошни преглеждания',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'Моите фаворити',
'no_pages_viewed' => 'Не сте прегледали никакви страници',
'no_pages_recently_created' => 'Не са били създавани страници скоро',
'no_pages_recently_updated' => 'Не са били актуализирани страници скоро',
@@ -40,7 +42,7 @@ return [
'permissions_intro' => 'Веднъж добавени, тези права ще вземат приоритет над всички други установени права.',
'permissions_enable' => 'Разреши уникални права',
'permissions_save' => 'Запази права',
'permissions_owner' => 'Owner',
'permissions_owner' => 'Собственик',
// Search
'search_results' => 'Резултати от търсенето',
@@ -49,7 +51,7 @@ return [
'search_no_pages' => 'Няма страници отговарящи на търсенето',
'search_for_term' => 'Търси :term',
'search_more' => 'Още резултати',
'search_advanced' => 'Advanced Search',
'search_advanced' => 'Подробно търсене',
'search_terms' => 'Search Terms',
'search_content_type' => 'Тип на съдържание',
'search_exact_matches' => 'Точни съвпадения',
@@ -60,7 +62,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' => 'Обновено след',
@@ -260,7 +262,7 @@ return [
'attachments_upload' => 'Прикачен файл',
'attachments_link' => 'Прикачване на линк',
'attachments_set_link' => 'Поставяне на линк',
'attachments_delete' => 'Are you sure you want to delete this attachment?',
'attachments_delete' => 'Сигурни ли сте, че искате да изтриете прикачения файл?',
'attachments_dropzone' => 'Поставете файлове или цъкнете тук за да прикачите файл',
'attachments_no_files' => 'Няма прикачени фалове',
'attachments_explain_link' => 'Може да прикачите линк, ако не искате да качвате файл. Този линк може да бъде към друга страница или към файл в облакова пространство.',

View File

@@ -83,6 +83,9 @@ return [
'404_page_not_found' => 'Страницата не е намерена',
'sorry_page_not_found' => 'Страницата, която търсите не може да бъде намерена.',
'sorry_page_not_found_permission_warning' => 'Ако смятате, че тази страница съществува, най-вероятно нямате право да я преглеждате.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'return_home' => 'Назад към Начало',
'error_occurred' => 'Възникна грешка',
'app_down' => ':appName не е достъпно в момента',

View File

@@ -35,13 +35,13 @@ return [
'app_primary_color' => 'Основен цвят на приложението',
'app_primary_color_desc' => 'Изберете основния цвят на приложението, включително на банера, бутоните и линковете.',
'app_homepage' => 'Application Homepage',
'app_homepage_desc' => 'Select a view to show on the homepage instead of the default view. Page permissions are ignored for selected pages.',
'app_homepage_select' => 'Select a page',
'app_footer_links' => 'Footer Links',
'app_footer_links_desc' => 'Add links to show within the site footer. These will be displayed at the bottom of most pages, including those that do not require login. You can use a label of "trans::<key>" to use system-defined translations. For example: Using "trans::common.privacy_policy" will provide the translated text "Privacy Policy" and "trans::common.terms_of_service" will provide the translated text "Terms of Service".',
'app_homepage_desc' => 'Изберете начална страница, която ще замени изгледа по подразбиране. Дефинираните права на страницата, която е избрана ще бъдат игнорирани.',
'app_homepage_select' => 'Избери страница',
'app_footer_links' => 'Футър линкове',
'app_footer_links_desc' => 'Добави линк в съдържанието на футъра. Добавените линкове ще се показват долу в повечето страници, включително и в страниците, в които логването не е задължително. Можете да използвате заместител "trans::<key>", за да използвате дума дефинирана от системата. Пример: Използването на "trans::common.privacy_policy" ще покаже "Лични данни" или на "trans::common.terms_of_service" ще покаже "Общи условия".',
'app_footer_links_label' => 'Link Label',
'app_footer_links_url' => 'Link URL',
'app_footer_links_add' => 'Add Footer Link',
'app_footer_links_url' => 'Линк URL',
'app_footer_links_add' => 'Добави футър линк',
'app_disable_comments' => 'Disable Comments',
'app_disable_comments_toggle' => 'Disable comments',
'app_disable_comments_desc' => 'Disables comments across all pages in the application. <br> Existing comments are not shown.',
@@ -80,20 +80,20 @@ return [
'maint_image_cleanup_nothing_found' => 'No unused images found, Nothing deleted!',
'maint_send_test_email' => 'Send a Test Email',
'maint_send_test_email_desc' => 'This sends a test email to your email address specified in your profile.',
'maint_send_test_email_run' => 'Send test email',
'maint_send_test_email_success' => 'Email sent to :address',
'maint_send_test_email_mail_subject' => 'Test Email',
'maint_send_test_email_mail_greeting' => 'Email delivery seems to work!',
'maint_send_test_email_mail_text' => 'Congratulations! As you received this email notification, your email settings seem to be configured properly.',
'maint_recycle_bin_desc' => 'Deleted shelves, books, chapters & pages are sent to the recycle bin so they can be restored or permanently deleted. Older items in the recycle bin may be automatically removed after a while depending on system configuration.',
'maint_recycle_bin_open' => 'Open Recycle Bin',
'maint_send_test_email_run' => 'Изпрати тестов имейл',
'maint_send_test_email_success' => 'Имейл изпратен на :address',
'maint_send_test_email_mail_subject' => 'Тестов Имейл',
'maint_send_test_email_mail_greeting' => 'Изпращането на Имейл работи!',
'maint_send_test_email_mail_text' => 'Поздравления! След като получихте този имейл, Вашите имейл настройки са конфигурирани правилно.',
'maint_recycle_bin_desc' => 'Изтрити рафти, книги, глави и страници се преместват в кошчето, откъдето можете да ги възстановите или изтриете завинаги. Стари съдържания в кошчето ще бъдат изтрити автоматично след време, в зависимост от настройките на системата.',
'maint_recycle_bin_open' => 'Отвори Кошчето',
// Recycle Bin
'recycle_bin' => 'Recycle Bin',
'recycle_bin' => 'Кошче',
'recycle_bin_desc' => 'Here you can restore items that have been deleted or choose to permanently remove them from the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'recycle_bin_deleted_item' => 'Deleted Item',
'recycle_bin_deleted_by' => 'Deleted By',
'recycle_bin_deleted_at' => 'Deletion Time',
'recycle_bin_deleted_item' => 'Изтрит предмет',
'recycle_bin_deleted_by' => 'Изтрит от',
'recycle_bin_deleted_at' => 'Час на изтриване',
'recycle_bin_permanently_delete' => 'Permanently Delete',
'recycle_bin_restore' => 'Restore',
'recycle_bin_contents_empty' => 'The recycle bin is currently empty',
@@ -111,27 +111,27 @@ return [
'audit' => 'Audit Log',
'audit_desc' => 'This audit log displays a list of activities tracked in the system. This list is unfiltered unlike similar activity lists in the system where permission filters are applied.',
'audit_event_filter' => 'Event Filter',
'audit_event_filter_no_filter' => 'No Filter',
'audit_deleted_item' => 'Deleted Item',
'audit_deleted_item_name' => 'Name: :name',
'audit_table_user' => 'User',
'audit_table_event' => 'Event',
'audit_event_filter_no_filter' => 'Без филтър',
'audit_deleted_item' => 'Изтрит предмет',
'audit_deleted_item_name' => 'Име: :name',
'audit_table_user' => 'Потребител',
'audit_table_event' => 'Събитие',
'audit_table_related' => 'Related Item or Detail',
'audit_table_date' => 'Activity Date',
'audit_date_from' => 'Date Range From',
'audit_date_to' => 'Date Range To',
'audit_table_date' => 'Дата на активност',
'audit_date_from' => 'Време от',
'audit_date_to' => 'Време до',
// Role Settings
'roles' => 'Roles',
'role_user_roles' => 'User Roles',
'role_create' => 'Create New Role',
'role_create_success' => 'Role successfully created',
'role_delete' => 'Delete Role',
'role_delete_confirm' => 'This will delete the role with the name \':roleName\'.',
'role_delete_users_assigned' => 'This role has :userCount users assigned to it. If you would like to migrate the users from this role select a new role below.',
'role_delete_no_migration' => "Don't migrate users",
'role_delete_sure' => 'Are you sure you want to delete this role?',
'role_delete_success' => 'Role successfully deleted',
'roles' => 'Роли',
'role_user_roles' => 'Потребителски роли',
'role_create' => 'Създай нова роля',
'role_create_success' => 'Ролята беше успешно създадена',
'role_delete' => 'Изтрий роля',
'role_delete_confirm' => 'Това ще изтрие ролята \':roleName\'.',
'role_delete_users_assigned' => 'В тази роля се намират :userCount потребители. Ако искате да преместите тези потребители в друга роля, моля изберете нова роля отдолу.',
'role_delete_no_migration' => "Не премествай потребителите в нова роля",
'role_delete_sure' => 'Сигурни ли сте, че искате да изтриете тази роля?',
'role_delete_success' => 'Ролята беше успешно изтрита',
'role_edit' => 'Редактиране на роля',
'role_details' => 'Детайли на роля',
'role_name' => 'Име на ролята',
@@ -146,24 +146,24 @@ return [
'role_access_api' => 'Достъп до API на системата',
'role_manage_settings' => 'Управление на настройките на приложението',
'role_asset' => 'Настройки за достъп до активи',
'roles_system_warning' => 'Be aware that access to any of the above three permissions can allow a user to alter their own privileges or the privileges of others in the system. Only assign roles with these permissions to trusted users.',
'role_asset_desc' => 'Тези настойки за достъп контролират достъпа по подразбиране до активите в системата. Настойките за достъп до книги, глави и страници ще отменят тези настройки.',
'roles_system_warning' => 'Важно: Добавянето на потребител в някое от горните три роли може да му позволи да промени собствените си права или правата на другите в системата. Възлагайте тези роли само на доверени потребители.',
'role_asset_desc' => 'Тези настройки за достъп контролират достъпа по подразбиране до активите в системата. Настройките за достъп до книги, глави и страници ще отменят тези настройки.',
'role_asset_admins' => 'Администраторите автоматично получават достъп до цялото съдържание, но тези опции могат да показват или скриват опциите за потребителския интерфейс.',
'role_all' => 'Всички',
'role_own' => 'Собствени',
'role_controlled_by_asset' => 'Controlled by the asset they are uploaded to',
'role_save' => 'Save Role',
'role_update_success' => 'Role successfully updated',
'role_users' => 'Users in this role',
'role_users_none' => 'No users are currently assigned to this role',
'role_save' => 'Запази ролята',
'role_update_success' => 'Ролята беше успешно актуализирана',
'role_users' => 'Потребители в тази роля',
'role_users_none' => 'В момента няма потребители, назначени за тази роля',
// Users
'users' => 'Users',
'user_profile' => 'User Profile',
'users_add_new' => 'Add New User',
'users_search' => 'Search Users',
'users_latest_activity' => 'Latest Activity',
'users_details' => 'User Details',
'users' => 'Потребители',
'user_profile' => 'Потребителски профил',
'users_add_new' => 'Добави нов потребител',
'users_search' => 'Търси Потребители',
'users_latest_activity' => 'Последна активност',
'users_details' => 'Потребителски детайли',
'users_details_desc' => 'Set a display name and an email address for this user. The email address will be used for logging into the application.',
'users_details_desc_no_email' => 'Set a display name for this user so others can recognise them.',
'users_role' => 'User Roles',
@@ -176,13 +176,13 @@ return [
'users_external_auth_id_desc' => 'This is the ID used to match this user when communicating with your external authentication system.',
'users_password_warning' => 'Only fill the below if you would like to change your password.',
'users_system_public' => 'This user represents any guest users that visit your instance. It cannot be used to log in but is assigned automatically.',
'users_delete' => 'Delete User',
'users_delete_named' => 'Delete user :userName',
'users_delete_warning' => 'This will fully delete this user with the name \':userName\' from the system.',
'users_delete_confirm' => 'Are you sure you want to delete this user?',
'users_migrate_ownership' => 'Migrate Ownership',
'users_delete' => 'Изтрий потребител',
'users_delete_named' => 'Изтрий потребителя :userName',
'users_delete_warning' => 'Това изцяло ще изтрие този потребител с името \':userName\' от системата.',
'users_delete_confirm' => 'Сигурни ли сте, че искате да изтриете този потребител?',
'users_migrate_ownership' => 'Мигрирайте собствеността на сайта',
'users_migrate_ownership_desc' => 'Select a user here if you want another user to become the owner of all items currently owned by this user.',
'users_none_selected' => 'No user selected',
'users_none_selected' => 'Няма избрани потребители',
'users_delete_success' => 'User successfully removed',
'users_edit' => 'Edit User',
'users_edit_profile' => 'Edit Profile',
@@ -241,6 +241,7 @@ return [
'es_AR' => 'Español Argentina',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',
'hu' => 'Magyar',
'id' => 'Bahasa Indonesia',
'it' => 'Italian',

View File

@@ -8,67 +8,67 @@
return [
// Standard laravel validation lines
'accepted' => 'The :attribute must be accepted.',
'active_url' => 'The :attribute is not a valid URL.',
'after' => 'The :attribute must be a date after :date.',
'alpha' => 'The :attribute may only contain letters.',
'alpha_dash' => 'The :attribute may only contain letters, numbers, dashes and underscores.',
'alpha_num' => 'The :attribute may only contain letters and numbers.',
'array' => 'The :attribute must be an array.',
'before' => 'The :attribute must be a date before :date.',
'accepted' => ':attribute трябва да бъде одобрен.',
'active_url' => ':attribute не е валиден URL адрес.',
'after' => ':attribute трябва да е дата след :date.',
'alpha' => ':attribute може да съдържа само букви.',
'alpha_dash' => ':attribute може да съдържа само букви, числа, тире и долна черта.',
'alpha_num' => ':attribute може да съдържа само букви и числа.',
'array' => ':attribute трябва да е масив (array).',
'before' => ':attribute трябва да е дата след :date.',
'between' => [
'numeric' => 'The :attribute must be between :min and :max.',
'file' => 'The :attribute must be between :min and :max kilobytes.',
'string' => 'The :attribute must be between :min and :max characters.',
'numeric' => ':attribute трябва да е между :min и :max.',
'file' => ':attribute трябва да е между :min и :max килобайта.',
'string' => 'Дължината на :attribute трябва да бъде между :min и :max символа.',
'array' => 'The :attribute must have between :min and :max items.',
],
'boolean' => 'The :attribute field must be true or false.',
'confirmed' => 'The :attribute confirmation does not match.',
'date' => 'The :attribute is not a valid date.',
'date_format' => 'The :attribute does not match the format :format.',
'different' => 'The :attribute and :other must be different.',
'digits' => 'The :attribute must be :digits digits.',
'digits_between' => 'The :attribute must be between :min and :max digits.',
'email' => 'The :attribute must be a valid email address.',
'ends_with' => 'The :attribute must end with one of the following: :values',
'filled' => 'The :attribute field is required.',
'boolean' => 'Полето :attribute трябва да съдържа булева стойност (true или false).',
'confirmed' => 'Потвърждението на :attribute не съвпада.',
'date' => ':attribute не е валидна дата.',
'date_format' => ':attribute не е в посоченият формат - :format.',
'different' => ':attribute и :other трябва да са различни.',
'digits' => ':attribute трябва да съдържа :digits цифри.',
'digits_between' => ':attribute трябва да бъде с дължина между :min и :max цифри.',
'email' => ':attribute трябва да бъде валиден имейл адрес.',
'ends_with' => ':attribute трябва да свършва с един от следните символи: :values',
'filled' => 'Полето :attribute е задължителен.',
'gt' => [
'numeric' => 'The :attribute must be greater than :value.',
'file' => 'The :attribute must be greater than :value kilobytes.',
'string' => 'The :attribute must be greater than :value characters.',
'numeric' => ':attribute трябва да бъде по-голям от :value.',
'file' => 'Големината на :attribute трябва да бъде по-голямо от :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-голямо от :value символа.',
'array' => 'The :attribute must have more than :value items.',
],
'gte' => [
'numeric' => 'The :attribute must be greater than or equal :value.',
'file' => 'The :attribute must be greater than or equal :value kilobytes.',
'string' => 'The :attribute must be greater than or equal :value characters.',
'file' => 'Големината на :attribute трябва да бъде по-голямо или равно на :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-голямо или равно на :value символа.',
'array' => 'The :attribute must have :value items or more.',
],
'exists' => 'The selected :attribute is invalid.',
'image' => 'The :attribute must be an image.',
'image_extension' => 'The :attribute must have a valid & supported image extension.',
'in' => 'The selected :attribute is invalid.',
'integer' => 'The :attribute must be an integer.',
'ip' => 'The :attribute must be a valid IP address.',
'ipv4' => 'The :attribute must be a valid IPv4 address.',
'ipv6' => 'The :attribute must be a valid IPv6 address.',
'json' => 'The :attribute must be a valid JSON string.',
'exists' => 'Избраният :attribute е невалиден.',
'image' => ':attribute трябва да e изображение.',
'image_extension' => ':attribute трябва да е валиден и/или допустим графичен файлов формат.',
'in' => 'Избраният :attribute е невалиден.',
'integer' => ':attribute трябва да бъде цяло число.',
'ip' => ':attribute трябва да бъде валиден IP адрес.',
'ipv4' => ':attribute трябва да бъде валиден IPv4 адрес.',
'ipv6' => ':attribute трябва да бъде валиден IPv6 адрес.',
'json' => ':attribute трябва да съдържа валиден JSON.',
'lt' => [
'numeric' => 'The :attribute must be less than :value.',
'file' => 'The :attribute must be less than :value kilobytes.',
'string' => 'The :attribute must be less than :value characters.',
'numeric' => ':attribute трябва да бъде по-малко от :value.',
'file' => 'Големината на :attribute трябва да бъде по-малко от :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-малко от :value символа.',
'array' => 'The :attribute must have less than :value items.',
],
'lte' => [
'numeric' => 'The :attribute must be less than or equal :value.',
'file' => 'The :attribute must be less than or equal :value kilobytes.',
'string' => 'The :attribute must be less than or equal :value characters.',
'numeric' => ':attribute трябва да бъде по-малко или равно на :value.',
'file' => 'Големината на :attribute трябва да бъде по-малко или равно на :value килобайта.',
'string' => 'Дължината на :attribute трябва да бъде по-малко или равно на :value символа.',
'array' => 'The :attribute must not have more than :value items.',
],
'max' => [
'numeric' => 'The :attribute may not be greater than :max.',
'file' => 'The :attribute may not be greater than :max kilobytes.',
'string' => 'The :attribute may not be greater than :max characters.',
'numeric' => ':attribute не трябва да бъде по-голям от :max.',
'file' => 'Големината на :attribute не може да бъде по-голямо от :value килобайта.',
'string' => 'Дължината на :attribute не може да бъде по-голямо от :value символа.',
'array' => 'The :attribute may not have more than :max items.',
],
'mimes' => 'The :attribute must be a file of type: :values.',

View File

@@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'je izbrisao/la policu za knjige',
'bookshelf_delete_notification' => 'Polica za knjige Uspješno Izbrisana',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'je komentarisao/la na',
'permissions_update' => 'je ažurirao/la dozvole',

View File

@@ -40,6 +40,10 @@ return [
'remove' => 'Ukloni',
'add' => 'Dodaj',
'fullscreen' => 'Prikaz preko čitavog ekrana',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opcije sortiranja',

View File

@@ -27,6 +27,8 @@ return [
'images' => 'Slike',
'my_recent_drafts' => 'Moje nedavne skice',
'my_recently_viewed' => 'Moji nedavni pregledi',
'my_most_viewed_favourites' => 'My Most Viewed Favourites',
'my_favourites' => 'My Favourites',
'no_pages_viewed' => 'Niste pogledali nijednu stranicu',
'no_pages_recently_created' => 'Nijedna stranica nije napravljena nedavno',
'no_pages_recently_updated' => 'Niijedna stranica nije ažurirana nedavno',

View File

@@ -83,6 +83,9 @@ return [
'404_page_not_found' => 'Stranica nije pronađena',
'sorry_page_not_found' => 'Stranica koju ste tražili nije pronađena.',
'sorry_page_not_found_permission_warning' => 'Ako ste očekivali da ova stranica postoji, možda nemate privilegije da joj pristupite.',
'image_not_found' => 'Image Not Found',
'image_not_found_subtitle' => 'Sorry, The image file you were looking for could not be found.',
'image_not_found_details' => 'If you expected this image to exist it might have been deleted.',
'return_home' => 'Nazad na početnu stranu',
'error_occurred' => 'Desila se greška',
'app_down' => ':appName trenutno nije u funkciji',

View File

@@ -241,6 +241,7 @@ return [
'es_AR' => 'Español Argentina',
'fr' => 'Français',
'he' => 'עברית',
'hr' => 'Hrvatski',
'hu' => 'Magyar',
'id' => 'Bahasa Indonesia',
'it' => 'Italian',

View File

@@ -43,6 +43,10 @@ return [
'bookshelf_delete' => 'ha suprimit un prestatge',
'bookshelf_delete_notification' => 'Prestatge suprimit correctament',
// Favourites
'favourite_add_notification' => '":name" has been added to your favourites',
'favourite_remove_notification' => '":name" has been removed from your favourites',
// Other
'commented_on' => 'ha comentat a',
'permissions_update' => 'ha actualitzat els permisos',

View File

@@ -6,8 +6,8 @@
*/
return [
'failed' => 'Les credencials no coincideixen amb les que tenim emmagatzemades.',
'throttle' => 'Massa intents d\'iniciar la sessió. Torneu-ho a provar d\'aquí a :seconds segons.',
'failed' => 'Les credencials no coincideixen amb les que hi ha emmagatzemades.',
'throttle' => 'Massa intents d\'inici de sessió. Torna-ho a provar d\'aquí a :seconds segons.',
// Login & Register
'sign_up' => 'Registra-m\'hi',
@@ -28,7 +28,7 @@ return [
'create_account' => 'Crea el compte',
'already_have_account' => 'Ja teniu un compte?',
'dont_have_account' => 'No teniu cap compte?',
'social_login' => 'Inici de sessió social',
'social_login' => 'Inici de sessió amb xarxes social',
'social_registration' => 'Registre social',
'social_registration_text' => 'Registreu-vos i inicieu la sessió fent servir un altre servei.',

View File

@@ -7,7 +7,7 @@ return [
// Buttons
'cancel' => 'Cancel·la',
'confirm' => 'D\'acord',
'back' => 'Endarrere',
'back' => 'Enrere',
'save' => 'Desa',
'continue' => 'Continua',
'select' => 'Selecciona',
@@ -40,6 +40,10 @@ return [
'remove' => 'Elimina',
'add' => 'Afegeix',
'fullscreen' => 'Pantalla completa',
'favourite' => 'Favourite',
'unfavourite' => 'Unfavourite',
'next' => 'Next',
'previous' => 'Previous',
// Sort Options
'sort_options' => 'Opcions d\'ordenació',
@@ -47,7 +51,7 @@ return [
'sort_ascending' => 'Ordre ascendent',
'sort_descending' => 'Ordre descendent',
'sort_name' => 'Nom',
'sort_default' => 'Default',
'sort_default' => 'Per defecte',
'sort_created_at' => 'Data de creació',
'sort_updated_at' => 'Data d\'actualització',

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