Compare commits

...

214 Commits

Author SHA1 Message Date
Dan Brown
ecd56917e7 Updated version and assets for release v0.30.0 2020-09-20 10:33:18 +01:00
Dan Brown
e22c9cae91 Merge branch 'master' into release 2020-09-20 10:30:10 +01:00
Dan Brown
a6c20c321f Merged latest translation changes 2020-09-20 10:28:01 +01:00
Dan Brown
e12012a6fc Updated translation contributors 2020-09-20 09:15:02 +01:00
Dan Brown
73b4c6d947 Fixed some wording in example env 2020-09-19 23:09:08 +01:00
Dan Brown
9e11fc33fa Updated example env with helpful info
- Added comments to explain the use of the file.
- Added comments to advise that space/hash containing values would need
to be quoted.

Related to #2258
2020-09-19 16:09:43 +01:00
Dan Brown
ff46d81681 Merge branch 'jb-l10n-fix-czech' of git://github.com/jakubboucek/BookStack into jakubboucek-jb-l10n-fix-czech 2020-09-19 15:44:18 +01:00
Dan Brown
1f202f6dbc Updated locale lists for Bulgarian 2020-09-19 15:36:17 +01:00
Dan Brown
bccf4653cb New Crowdin translations (#2077)
* New translations entities.php (Portuguese, Brazilian)

* New translations entities.php (Persian)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Thai)

* New translations errors.php (German Informal)

* New translations entities.php (Spanish)

* New translations entities.php (French)

* New translations entities.php (Arabic)

* New translations entities.php (Arabic)

* New translations components.php (Portuguese, Brazilian)

* New translations entities.php (Portuguese, Brazilian)

* New translations auth.php (Italian)

* New translations common.php (Italian)

* New translations components.php (Italian)

* New translations entities.php (Italian)

* New translations settings.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations entities.php (Chinese Simplified)

* New translations settings.php (Spanish)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Ukrainian)

* New translations components.php (Portuguese)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations common.php (Slovenian)

* New translations components.php (Slovenian)

* New translations common.php (Swedish)

* New translations components.php (Swedish)

* New translations common.php (Turkish)

* New translations components.php (Turkish)

* New translations components.php (Ukrainian)

* New translations components.php (Polish)

* New translations common.php (Chinese Simplified)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations common.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Persian)

* New translations components.php (Persian)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations common.php (Thai)

* New translations components.php (Thai)

* New translations common.php (Portuguese)

* New translations common.php (Polish)

* New translations common.php (Italian)

* New translations common.php (Bulgarian)

* New translations components.php (Italian)

* New translations components.php (Chinese Simplified)

* New translations components.php (German)

* New translations components.php (Japanese)

* New translations components.php (Dutch)

* New translations components.php (German Informal)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations common.php (Arabic)

* New translations components.php (Arabic)

* New translations components.php (Bulgarian)

* New translations common.php (Dutch)

* New translations common.php (Czech)

* New translations components.php (Czech)

* New translations common.php (Danish)

* New translations components.php (Danish)

* New translations common.php (German)

* New translations common.php (Hebrew)

* New translations components.php (Hebrew)

* New translations common.php (Hungarian)

* New translations components.php (Hungarian)

* New translations common.php (Japanese)

* New translations common.php (Korean)

* New translations components.php (Korean)

* New translations common.php (German Informal)

* New translations components.php (German)

* New translations common.php (German)

* New translations entities.php (German)

* New translations common.php (French)

* New translations components.php (French)

* New translations common.php (Spanish)

* New translations components.php (Spanish)

* New translations components.php (Chinese Simplified)

* New translations common.php (Chinese Simplified)

* New translations common.php (Polish)

* New translations components.php (Polish)

* New translations auth.php (Polish)

* New translations entities.php (Polish)

* New translations errors.php (Polish)

* New translations passwords.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations common.php (Spanish, Argentina)

* New translations components.php (Spanish, Argentina)

* New translations auth.php (Spanish, Argentina)

* New translations entities.php (Spanish, Argentina)

* New translations passwords.php (Spanish, Argentina)

* New translations settings.php (Spanish, Argentina)

* New translations entities.php (German)

* New translations components.php (German Informal)

* New translations common.php (German Informal)

* New translations entities.php (German Informal)

* New translations settings.php (Italian)

* New translations settings.php (Dutch)

* New translations settings.php (Thai)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Korean)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Japanese)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (French)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations settings.php (Spanish)

* New translations settings.php (French)

* New translations components.php (Turkish)

* New translations settings.php (Turkish)

* New translations entities.php (Turkish)

* New translations common.php (Turkish)

* New translations components.php (Portuguese, Brazilian)

* New translations common.php (Portuguese, Brazilian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Chinese Simplified)

* New translations activities.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations activities.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations common.php (Chinese Traditional)

* New translations components.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations passwords.php (Chinese Traditional)

* New translations settings.php (German)

* New translations settings.php (German Informal)

* New translations activities.php (Slovak)

* New translations auth.php (Slovak)

* New translations auth.php (Slovak)

* New translations common.php (Slovak)

* New translations components.php (Slovak)

* New translations components.php (Slovak)

* New translations entities.php (Slovak)

* New translations common.php (Slovak)

* New translations entities.php (Slovak)

* New translations passwords.php (Slovak)

* New translations settings.php (Dutch)

* New translations components.php (Dutch)

* New translations entities.php (Dutch)

* New translations passwords.php (Dutch)

* New translations activities.php (Arabic)

* New translations entities.php (French)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Traditional)

* New translations auth.php (Chinese Traditional)

* New translations errors.php (Chinese Traditional)

* New translations activities.php (Japanese)

* New translations auth.php (Japanese)

* New translations entities.php (Chinese Traditional)

* New translations validation.php (Chinese Traditional)

* New translations common.php (Russian)

* New translations components.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations settings.php (Polish)

* New translations entities.php (Russian)

* New translations entities.php (Portuguese)

* New translations entities.php (Thai)

* New translations entities.php (Spanish, Argentina)

* New translations entities.php (Persian)

* New translations entities.php (Portuguese, Brazilian)

* New translations entities.php (Vietnamese)

* New translations entities.php (Chinese Traditional)

* New translations entities.php (Chinese Simplified)

* New translations entities.php (Ukrainian)

* New translations entities.php (Turkish)

* New translations entities.php (Swedish)

* New translations entities.php (Slovenian)

* New translations entities.php (Slovak)

* New translations entities.php (Polish)

* New translations entities.php (French)

* New translations entities.php (Dutch)

* New translations entities.php (Korean)

* New translations entities.php (Japanese)

* New translations entities.php (Italian)

* New translations entities.php (Hungarian)

* New translations entities.php (Hebrew)

* New translations entities.php (German)

* New translations entities.php (Danish)

* New translations entities.php (Czech)

* New translations entities.php (Bulgarian)

* New translations entities.php (Arabic)

* New translations entities.php (Spanish)

* New translations entities.php (German Informal)

* New translations entities.php (French)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese)

* New translations settings.php (Thai)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Slovenian)

* New translations settings.php (Slovak)

* New translations settings.php (Polish)

* New translations settings.php (French)

* New translations settings.php (Dutch)

* New translations settings.php (Korean)

* New translations settings.php (Japanese)

* New translations settings.php (Italian)

* New translations settings.php (Hungarian)

* New translations settings.php (Hebrew)

* New translations settings.php (German)

* New translations settings.php (Danish)

* New translations settings.php (Czech)

* New translations settings.php (Bulgarian)

* New translations settings.php (Arabic)

* New translations settings.php (Spanish)

* New translations settings.php (German Informal)

* New translations entities.php (Spanish)

* New translations settings.php (Spanish)
2020-09-19 15:22:32 +01:00
Dan Brown
31eec34b5d Moved decode and updated page plaintext decode test 2020-09-19 15:13:18 +01:00
Dan Brown
44f3508171 Merge branch 'preview-entities' of git://github.com/mr-vinn/BookStack into mr-vinn-preview-entities 2020-09-19 14:58:56 +01:00
Dan Brown
2e39e45886 Added test to check text gen decodes HTML entities 2020-09-19 14:58:18 +01:00
Dan Brown
458aa72c2f Updated composer deps 2020-09-19 12:12:48 +01:00
Dan Brown
78bf044a7a Added audit log interface
- Displays the currently tracked activities in the system.

Related to #2173 and #1167
2020-09-19 12:06:45 +01:00
Dan Brown
e5f0b4dd85 Split out Maintenance to separate controller 2020-09-19 09:24:58 +01:00
Vinnie Okada
311a12b7ef Decode HTML entities
Decode HTML entities in page text before saving it to the database.
2020-09-18 06:54:30 -06:00
Dan Brown
d9e2bddee4 Added some robustness to page draft saving
- Updated so that a warning is always shown on error, Not just on first
in chain.
- Added last-resort localStorage content saving.
2020-09-13 19:32:45 +01:00
Dan Brown
6578ac0b4a Fixed visible revision delete menu 2020-09-13 19:12:15 +01:00
Dan Brown
09c6d6c722 Added button for inserting attachment link to a page
For #1460
2020-09-13 18:58:05 +01:00
Dan Brown
ad48cd3e48 Continued implementation of attachment drag+drop
Cannot get working in chrome reliably due to conflicting handling of
events and drag+drop API. Getting attachment drop working breaks other
parts of TinyMCE.
Implementing current work as should still work for MD editor and within
FireFox.

Related to #1460
2020-09-13 18:31:14 +01:00
Dan Brown
e305ba14d9 Merge branch 'master' into attachment_drag_drop 2020-09-13 16:33:31 +01:00
Dan Brown
b87e97f99e Added punnycode since its reuquired by markdownit
Is a native, although depricated, nodejs module. Have installed manually
since esbuild could not resolve the nodejs module
2020-09-05 20:37:23 +01:00
Dan Brown
e5377d5f46 Updated saml2 slo config so url is used if no repsonse url
Updated config to change empty string to null since the empty string was
hitting an isset check which caused an empty string to be used instead
of the slo url as a backup option.

Closes #2002
2020-09-05 19:26:47 +01:00
Dan Brown
ff1ee2d71f Updated flow to ensure /register/confirm route is used where needed
Was accidentally skipped during previous updates. Will now be used on
saml, ldap & standard registration where required.
Uses session to know if the email was just sent and, if so, show the
confirmation route.
2020-09-05 17:26:48 +01:00
Dan Brown
c029741a17 Updated npm deps 2020-09-05 16:54:25 +01:00
Dan Brown
ac83c349da Migrated from webpack to esbuild 2020-09-05 16:50:20 +01:00
Jakub Bouček
fefcaa21e7 Fix English translations
- Fix obvious bug
- Reunite capitalisation
2020-08-31 20:45:09 +02:00
Jakub Bouček
6a36db3cde Czech translations: Fix broken labels 2020-08-31 20:45:09 +02:00
Jakub Bouček
c9352bfd42 Czech translations: Add new translations to cs, improve existing 2020-08-31 20:45:09 +02:00
Jakub Bouček
9c457d9ffe Fix Czech translations (email -> e-mail)
In Czech language "email" does not means "email" but "enamel paint", correct is "e-mail".
See in Wikipedia:
- https://cs.wikipedia.org/wiki/E-mail
- https://cs.wikipedia.org/wiki/Email_(barva)
2020-08-31 17:34:46 +02:00
Dan Brown
87a5340a05 Prevented email confirmation exception throw on registration
Was preventing any other registration actions from taking place such as
LDAP/SAML group sync. Email confirmation should be actioned by
middleware on post-registration redirect.

Added testing to cover.
Tested for LDAP, SAML and normal registration with email confirmation
required to ensure flows work as expected.

Fixes #2082
2020-08-04 17:54:50 +01:00
Dan Brown
c076ca408c Fixed non-visible horizontal rules in dark mode
Fixes #2209
2020-08-04 15:39:07 +01:00
Dan Brown
1ac11c1852 Added warning to role screen for important permissions
Warning related to permissions that could allow a person to promote
their own permissions to gain more privileges than expected.

For #2105.
2020-08-04 15:26:13 +01:00
Dan Brown
5f1ee5fb0e Removed role 'name' field from database
The 'name' field was really redundant and caused confusion in the
codebase, since the 'Display' name is often used and we have a
'system_name' for the admin and public role.

This fixes #2032, Where external auth group matching has confusing
behaviour as matching was done against the display_name, if no
external_auth field is set, but only roles with a match 'name' field
would be considered.

This also fixes and error where the role users migration, on role
delete, would not actually fire due to mis-matching http body keys.
Looks like this has been an issue from the start. Added some testing to
cover. Fixes #2211.

Also converted phpdoc to typehints in many areas of the reviewed code
during the above.
2020-08-04 14:55:01 +01:00
Dan Brown
a9f02550f0 Removed joint_permissions auto_increment id
Removed auto_incrementing id and set a primary key of the [role_id,
entity_type, entity_id, action] instead since this table could recieve a
lot of activity, especially when permission regeneration was automated,
leading to very high auto_increment counts which could max out the
integer limit.

Also updated some RolesTest comment endpoints to align with
recent route changes.

Should fix #2091
2020-08-04 13:02:31 +01:00
Dan Brown
7590ecd37c Updated some comment elements and standardised more JS
- Updated comment routes to be simpler.
- Updated comments JS to align better with updated component system.
- Documented available global JS functions/services.
- Removed redundant controller method.
- Added window.$events helpers for validation messages and
success/error.
- Updated JS events system to not be class based for simplicity.
- Added window.trans_plural method to handle pluralisation/replacements
where you already have the translation string itself.

Fixes #1836
2020-07-28 18:19:18 +01:00
Dan Brown
2c0fdf83c1 Updated public-login redirect to check url
Direct links to the login pages for public instances could lead to a
redirect back to an external page upon login.
This adds a check to ensure the URL is a URL expected from the current
bookstack instance, or at least under the same domain.

Fixes #2073
2020-07-28 16:29:06 +01:00
Dan Brown
2ed0317129 Updated functionality for logging failed access
- Added testing to cover.
- Linked logging into Laravel's monolog logging system and made log
channel configurable.
- Updated env var names to be specific to login access.
- Added extra locations as to where failed logins would be captured.

Related to #1881 and #728
2020-07-28 12:59:43 +01:00
Dan Brown
2f6ff07347 Merge branch 'auth' of git://github.com/benrubson/BookStack into benrubson-auth 2020-07-28 10:46:40 +01:00
Dan Brown
18f406d97b Started attachment drag/drop
Currently fighting between sortable and tinymce mechanisms which prevent
this working due to the different events stopping the drop event while
needing the dragover for cursor placement.
2020-07-28 10:45:28 +01:00
Dan Brown
76fcbd3752 Removed default anchor CSS filtering in dark mode
Due to causing content images to be rendered in unexpected ways.

- Also removed CSS filters from other image usage.
- Tweaked header CSS filtering to not be so aggressive.
- Forced WYSIWYG editor to be on its own layer since that would allow
massive larger performance increases in Safari, especially when using
dark mode.

Closes #2045.
Closes #2154.
2020-07-26 16:36:15 +01:00
Dan Brown
6e4132121c Updated pagination colors for visibility
Fixes #1839
2020-07-26 15:07:47 +01:00
Dan Brown
f5fefbdb06 Removed a few remaining vue references 2020-07-26 14:49:05 +01:00
Dan Brown
a46b248cf4 Fixed some image manager behaviour
fixed:
- Double click not working after tab usage.
- Synced edit form with select button.
2020-07-25 11:47:12 +01:00
Dan Brown
8213ea9a71 Fixed issue where URL params in image names would cause loading failure
Updated file name handling to route through str:slug to be cleaned up
a little.
Added testing to cover.

Fixes #2161
2020-07-25 11:18:40 +01:00
Dan Brown
03211ebea6 Removed unused tinymce imagetools plugin 2020-07-25 01:09:35 +01:00
Dan Brown
2bacc3c967 Removed vuejs from the project 2020-07-25 00:25:30 +01:00
Dan Brown
02dc3154e3 Converted image-manager to be component/HTML based
Instead of vue based.
2020-07-25 00:20:58 +01:00
Dan Brown
b6aa232205 Fixed issue where more images than expected could be deleted
When deleting images, images within the same directory, that have
a suffix of the delete image name, would also be deleted.

Added test to cover.
2020-07-24 23:41:59 +01:00
Dan Brown
b383f5776d Tweaked dropdown shadows a tad 2020-07-05 21:23:57 +01:00
Dan Brown
3bfd26bf86 Converted the page editor from vue to component 2020-07-05 21:18:17 +01:00
Dan Brown
9d6f574494 Updated attachment tests to align with front-end changes 2020-07-04 17:04:26 +01:00
Dan Brown
d41452f39c Finished breakdown of attachment vue into components 2020-07-04 16:53:02 +01:00
Dan Brown
14b6cd1091 Started migration of attachment manager from vue
- Created new dropzone component.
- Added standard component event system using custom DOM events.
- Added tabs component.
- Added ajax-delete-row component.
2020-06-30 22:12:45 +01:00
Dan Brown
8dc9689c6d Removed tests for removed ajax tag route 2020-06-29 23:46:08 +01:00
Dan Brown
181ae6d055 Fixed tag-manager loading on entity-creation 2020-06-29 23:40:34 +01:00
Dan Brown
573c4e26d5 Finished moving tag-manager from a vue to a component
Now tags load with the page, not via AJAX.
2020-06-29 22:11:03 +01:00
Dan Brown
4e107b9160 Started migrating tag manager JS to HTML-first component 2020-06-28 23:15:05 +01:00
Dan Brown
10305a4446 Converted entity-dash from vue to a component 2020-06-28 21:15:00 +01:00
Dan Brown
a5fa745749 Moved overlay component, migrated code-editor & added features
- Moved Code-editor from vue to component.
- Updated popup code so it background click only hides if the click
originated on the same background. Clicks within the popup will no
longer cause it to hide.
- Added session-level history tracking to code editor.
2020-06-28 00:06:47 +01:00
Dan Brown
9023f78cdc Merge branch 'master' of github.com:BookStackApp/BookStack 2020-06-27 17:19:05 +01:00
Dan Brown
8bc3e0f31a Merge branch 'master' of git://github.com/drzippie/BookStack into drzippie-master 2020-06-27 17:11:11 +01:00
Dan Brown
afed379c5c Merge pull request #2157 from Honvid/fix/lang_error
fix the translate error
2020-06-27 17:06:38 +01:00
Dan Brown
540119f133 Moved sass build out of webpack, updated npm deps
Moving sass out of webpack cleans the setup quite considerably and
brings a good speed improvement.
Made use of npm-run-all so the previous commands still run like before.
2020-06-27 16:52:26 +01:00
Dan Brown
d5de28c444 Merge branch 'use-dart-sass' of git://github.com/timoschwarzer/BookStack into timoschwarzer-use-dart-sass 2020-06-27 15:59:38 +01:00
Dan Brown
7a2e39212e Fixed empty search scenario 2020-06-27 13:37:18 +01:00
Dan Brown
715dee2d0e Converted search filters to not be vue based 2020-06-27 13:29:00 +01:00
Timo Schwarzer
0f55d776a6 Replace node-sass with dart-sass 2020-06-26 12:44:41 +02:00
Antonio Cortés (DrZippie)
d617dba61c removed test_slug_multi_byte_lower_casing and added new test test_slug_multi_byte_url_safe 2020-06-25 18:42:28 +02:00
Antonio Cortés (DrZippie)
ca202c1819 Added Illuminate\Support\Str::slug to generate slug from text to improve the creation of slugs with non-English characters 2020-06-25 18:08:13 +02:00
Dan Brown
76d02cd472 Started attempt at formalising component system used in BookStack
Added a document to try to define things.
Updated the loading so components are registed dynamically.
Added some standardised ways to reference other elems & define options
2020-06-24 20:38:08 +01:00
Honvid
118e31608a fix the bug for lang's extra letter. 2020-06-16 11:44:08 +08:00
Honvid
418fd9037f Merge pull request #1 from BookStackApp/master
sync the remote master
2020-06-10 07:46:06 +08:00
benrubson
9d7ce59b18 Move logFailedAccess into Activity 2020-05-23 15:37:38 +02:00
Dan Brown
71e7dd5894 Removed failing URL test
- Was found that the test was not testing the actual situation anyway.
- A work-around in the request creation, within testing, just happened
 to result in the desired outcome.

For reference: https://github.com/laravel/framework/pull/32345
2020-05-23 12:56:31 +01:00
Dan Brown
3502abdd49 Fixed revision issues caused by page fillable changes 2020-05-23 12:28:14 +01:00
Dan Brown
31514bae06 Updated framework and other deps 2020-05-23 11:50:44 +01:00
Dan Brown
19bfc8ad37 Prevented entity "Not Found" events from being logged
- Added testing to cover, which was more hassle than thought
  since Laravel did not have built in log test helpers, so:
- Added Log testing helper.

Related to #2110
2020-05-23 11:28:59 +01:00
benrubson
8f1f73defa Properly use env/config functions 2020-05-23 12:06:37 +02:00
Dan Brown
bf4a3b73f8 Updated listing endpoints to be clickable in api docs 2020-05-23 00:53:13 +01:00
Dan Brown
00c0815808 Fixed issue where updated page content would not be indexed
- Also updated html field of pages to not be fillable.
   (Since HTML should always go through app id parsing)

Related to #2042
2020-05-23 00:46:13 +01:00
Dan Brown
b61f950560 Incremented version number 2020-05-23 00:29:09 +01:00
Dan Brown
8a6cf0cdec Added chapters to the API 2020-05-23 00:28:41 +01:00
Dan Brown
24bad5034a Updated API auth to allow public user if given permission 2020-05-22 22:34:18 +01:00
Dan Brown
29ddb6e1b9 Updated version and assets for release v0.29.3 2020-05-12 22:34:01 +01:00
Dan Brown
2ff90e2ff0 Merge branch 'master' into release 2020-05-12 22:33:27 +01:00
Dan Brown
9666c8c0f7 Updated shelf-list view to enforce view permissions for child books
- Aligned shelf-homepage behaviour to match
- Updated testing to cover.

For #2111
2020-05-12 22:21:45 +01:00
benrubson
58df3ad956 Log failed accesses option 2020-05-03 16:20:02 +02:00
Dan Brown
04ecc128a2 Updated version and assets for release v0.29.2 2020-05-02 11:49:21 +01:00
Dan Brown
87d1d3423b Merge branch 'master' into release 2020-05-02 11:48:48 +01:00
Dan Brown
d3ec38bee3 Removed unused function in registration service 2020-05-02 01:07:30 +01:00
Dan Brown
413cac23ae Added command to regenerate comment content 2020-05-01 23:41:47 +01:00
Dan Brown
3c26e7b727 Updated comment md rendering to be server-side 2020-05-01 23:24:11 +01:00
Dan Brown
2a2d0aa15b Fixed incorrect color code causing yellow/orange code blocks 2020-04-29 18:28:26 +01:00
Dan Brown
4818192a2a Updated version and assets for release v0.29.1 2020-04-28 12:30:31 +01:00
Dan Brown
965dd97f54 Merge branch 'master' into release 2020-04-28 12:30:09 +01:00
Dan Brown
dfceab7cfa Updated translator attribution before release v0.29.1 2020-04-28 12:30:02 +01:00
Dan Brown
00c77e494b Updated ci with php7.4, update locale array 2020-04-28 12:28:19 +01:00
Dan Brown
ce8cea6a9f New Crowdin translations (#2071)
* New translations common.php (Korean)

* New translations settings.php (Korean)
2020-04-28 12:25:15 +01:00
Dan Brown
6f2a2c05bf New Crowdin translations (#2028)
* New translations settings.php (Chinese Simplified)

* New translations common.php (Spanish)

* New translations common.php (Spanish, Argentina)

* New translations common.php (Turkish)

* New translations common.php (French)

* New translations auth.php (Dutch)

* New translations common.php (Dutch)

* New translations entities.php (Dutch)

* New translations activities.php (Thai)

* New translations auth.php (Thai)

* New translations common.php (Thai)

* New translations components.php (Thai)

* New translations entities.php (Thai)

* New translations errors.php (Thai)

* New translations pagination.php (Thai)

* New translations passwords.php (Thai)

* New translations settings.php (Thai)

* New translations validation.php (Thai)
2020-04-28 10:19:42 +01:00
Dan Brown
898d0b5817 Added multi-select to book-sort interface
As discussed in #2064

Closes #2067
2020-04-27 16:53:27 +01:00
Dan Brown
4ef362143b Added auto-focus behaviour to page editor
- Will focus on title if the value of the field matches the default text
for the current user's language.
- Otherwise will focus on the editor body.
- Added and tested on both editors.

For #2036
2020-04-27 15:54:39 +01:00
Dan Brown
8ce38d2158 Fixed not shown existing-email warning on new ldap user
- Reduced the amount of different exceptions from LDAP attempt so they
can be handled more consistently.
- Added test to cover.
- Also cleaned up LDAP tests to reduce boilterplate mocks.

Fixes #2048
2020-04-26 12:13:00 +01:00
Dan Brown
468fec80de Updated WYSIWYG callout shortcut to handle child elems
- Will now search for a callout on/above the selected node rather than
only using the selected node.
- Issues previously where callout shortcut would not cycle if called
when child formatting was currently selected inside the callout.

For #2061
2020-04-26 09:26:41 +01:00
Dan Brown
2ec4ad1181 Tweaked ListingResponseBuilder to help avoid future issues
- Updated so none of the method mutate the query throughout the function
so that the query can be handled in a sane way, Since we were already
encountering issues due to internal method call order.
2020-04-25 22:15:59 +01:00
Dan Brown
a17b82bdde Fixed api query total not taking filters into account 2020-04-25 21:37:52 +01:00
Dan Brown
8fb1f7c361 Fixed floated content extending past page body
As shown in #2055
2020-04-25 19:59:23 +01:00
Dan Brown
c20110b6ae Fixed issue where callout and quotes overlap floated images
For #2055
2020-04-25 19:55:16 +01:00
Dan Brown
a880b1d5c5 Fixed selection not visible - dark theme codemirror
Fixes #2060
2020-04-25 19:19:41 +01:00
Dan Brown
07831df2d3 Updated user-create endpoint so saml and ldap is consistent. 2020-04-25 18:28:07 +01:00
Dan Brown
519283e643 Authenticated admins on all guards upon login
For #2031
2020-04-25 18:19:22 +01:00
Dan Brown
79a949836b Fixed incorrect API listing total when offset set
Fixes #2043
2020-04-25 16:38:11 +01:00
Dan Brown
195b74926c Updated version and assets for release v0.29.0 2020-04-13 16:10:23 +01:00
Dan Brown
2120db12b2 Merge branch 'master' into release 2020-04-13 16:10:11 +01:00
Dan Brown
0883b0533b Merge branch 'master' of github.com:BookStackApp/BookStack 2020-04-13 15:43:53 +01:00
Dan Brown
d030620846 Updated translator contributor file from crowdin 2020-04-13 15:43:20 +01:00
Dan Brown
687c4247ae New Crowdin translations (#2005)
* New translations settings.php (Portuguese)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Russian)

* New translations settings.php (Korean)

* New translations settings.php (Persian)

* New translations settings.php (Polish)

* New translations settings.php (Swedish)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Turkish)

* New translations settings.php (Slovak)

* New translations settings.php (Slovenian)

* New translations settings.php (Spanish)

* New translations settings.php (Czech)

* New translations settings.php (Danish)

* New translations settings.php (Dutch)

* New translations settings.php (Arabic)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Italian)

* New translations settings.php (Hungarian)

* New translations settings.php (Japanese)

* New translations settings.php (French)

* New translations settings.php (German)

* New translations settings.php (Hebrew)

* New translations settings.php (German Informal)

* New translations settings.php (Vietnamese)

* New translations settings.php (Ukrainian)

* New translations activities.php (Turkish)

* New translations activities.php (Turkish)

* New translations auth.php (Turkish)

* New translations common.php (Turkish)

* New translations auth.php (Turkish)

* New translations components.php (Turkish)

* New translations common.php (Turkish)

* New translations components.php (Turkish)

* New translations entities.php (Turkish)

* New translations entities.php (Turkish)

* New translations errors.php (Turkish)

* New translations entities.php (Turkish)

* New translations passwords.php (Turkish)

* New translations settings.php (Turkish)

* New translations errors.php (Turkish)

* New translations settings.php (Turkish)

* New translations settings.php (Turkish)

* New translations validation.php (Turkish)

* New translations settings.php (Turkish)

* New translations validation.php (Turkish)

* New translations common.php (Turkish)

* New translations components.php (Turkish)

* New translations validation.php (Turkish)

* New translations components.php (Turkish)

* New translations entities.php (Turkish)

* New translations errors.php (Turkish)

* New translations settings.php (Turkish)

* New translations validation.php (Turkish)

* New translations passwords.php (Arabic)

* New translations auth.php (Arabic)

* New translations auth.php (Slovak)

* New translations passwords.php (Russian)

* New translations passwords.php (Slovak)

* New translations auth.php (Slovenian)

* New translations passwords.php (Slovenian)

* New translations auth.php (Spanish)

* New translations passwords.php (Portuguese, Brazilian)

* New translations passwords.php (Polish)

* New translations auth.php (Portuguese)

* New translations auth.php (Russian)

* New translations passwords.php (Portuguese)

* New translations auth.php (Portuguese, Brazilian)

* New translations auth.php (Ukrainian)

* New translations passwords.php (Ukrainian)

* New translations auth.php (Vietnamese)

* New translations passwords.php (Vietnamese)

* New translations auth.php (German Informal)

* New translations passwords.php (German Informal)

* New translations passwords.php (Turkish)

* New translations passwords.php (Spanish)

* New translations auth.php (Spanish, Argentina)

* New translations passwords.php (Spanish, Argentina)

* New translations auth.php (Swedish)

* New translations passwords.php (Swedish)

* New translations auth.php (Turkish)

* New translations components.php (Turkish)

* New translations entities.php (Turkish)

* New translations auth.php (Polish)

* New translations passwords.php (Danish)

* New translations auth.php (Dutch)

* New translations passwords.php (Dutch)

* New translations auth.php (Danish)

* New translations auth.php (French)

* New translations passwords.php (French)

* New translations auth.php (Chinese Simplified)

* New translations passwords.php (Chinese Simplified)

* New translations auth.php (Chinese Traditional)

* New translations passwords.php (Chinese Traditional)

* New translations auth.php (Czech)

* New translations passwords.php (Czech)

* New translations auth.php (German)

* New translations auth.php (Korean)

* New translations auth.php (Japanese)

* New translations passwords.php (Japanese)

* New translations passwords.php (Korean)

* New translations auth.php (Persian)

* New translations passwords.php (Persian)

* New translations passwords.php (Italian)

* New translations passwords.php (German)

* New translations auth.php (Hebrew)

* New translations passwords.php (Hebrew)

* New translations auth.php (Hungarian)

* New translations passwords.php (Hungarian)

* New translations auth.php (Italian)

* New translations entities.php (Turkish)

* New translations settings.php (Turkish)

* New translations validation.php (Turkish)

* New translations passwords.php (Turkish)

* New translations entities.php (Turkish)

* New translations errors.php (Turkish)

* New translations validation.php (Turkish)

* New translations auth.php (Turkish)

* New translations auth.php (Spanish)

* New translations passwords.php (Spanish)

* New translations settings.php (Spanish)

* New translations auth.php (Spanish, Argentina)

* New translations passwords.php (Spanish, Argentina)

* New translations entities.php (Turkish)

* New translations auth.php (French)

* New translations passwords.php (French)

* New translations settings.php (French)

* New translations common.php (Russian)

* New translations common.php (Slovak)

* New translations common.php (Slovenian)

* New translations common.php (Spanish)

* New translations common.php (Portuguese)

* New translations common.php (Polish)

* New translations common.php (Portuguese, Brazilian)

* New translations common.php (Ukrainian)

* New translations common.php (Vietnamese)

* New translations common.php (German Informal)

* New translations common.php (Spanish, Argentina)

* New translations common.php (Swedish)

* New translations common.php (Turkish)

* New translations common.php (Danish)

* New translations common.php (Dutch)

* New translations common.php (French)

* New translations common.php (Arabic)

* New translations common.php (Chinese Simplified)

* New translations common.php (Czech)

* New translations common.php (Chinese Traditional)

* New translations common.php (Japanese)

* New translations common.php (Italian)

* New translations common.php (Korean)

* New translations common.php (Persian)

* New translations common.php (German)

* New translations common.php (Hebrew)

* New translations common.php (Hungarian)

* New translations auth.php (Russian)

* New translations common.php (Russian)

* New translations passwords.php (Russian)

* New translations passwords.php (German)

* New translations settings.php (German)

* New translations auth.php (German)

* New translations common.php (German)

* New translations settings.php (German Informal)

* New translations passwords.php (German Informal)

* New translations common.php (German Informal)

* New translations auth.php (German Informal)
2020-04-13 15:31:35 +01:00
Dan Brown
3a70e9d49c Merge pull request #2023 from jzoy/master
fix Chinese translation error
2020-04-12 19:15:00 +01:00
Dan Brown
88dfb40c63 Some further dark-mode fixes, added toggle to homepage
- Homepage toggle especially useful for not-logged-in users since they
do not have a dropdown.
2020-04-12 19:06:34 +01:00
Dan Brown
b80b6ed942 Merge pull request #2022 from BookStackApp/dark-mode
Addition of a user-selectable dark-mode option
2020-04-11 20:52:19 +01:00
Dan Brown
50669e3f4a Added tests and translations for dark-mode components 2020-04-11 20:44:23 +01:00
Dan Brown
573c848d51 Added dark/light mode toggle to profile dropdown menu
- Also fixed some remaining areas which needed dark mode support.
2020-04-11 20:37:51 +01:00
Dan Brown
d4b0e4acad Removed throttling from web-end requests
Generally seems to cause issues when secure images are in use.
Was added during laravel upgrade but laravel does not use this directly
for its web middleware anyway.
2020-04-11 20:02:07 +01:00
Dan Brown
b0b28e7b5e Rolled dark mode out to the editors
- Updated editor, and other area, styles to look okay in dark mode.
- Used tinyMCE theme generator to create dark mode theme.
- Updated tinymce to latest 4x version.
2020-04-11 15:48:08 +01:00
jzoy
eb94500dca Update settings.php 2020-04-11 21:29:09 +08:00
jzoy
df8ea0b81d fix Chinese translation error 2020-04-11 21:26:13 +08:00
Dan Brown
067cb9c5b7 Merge branch 'master' into dark-mode 2020-04-11 14:22:41 +01:00
jzoy
7673a2bd6c Merge pull request #2 from BookStackApp/master
fetch upstream
2020-04-11 21:18:17 +08:00
Dan Brown
627720c5af Fixed incorrect []Activity -> array conversion 2020-04-10 22:49:52 +01:00
Dan Brown
1ba5a1274c Started work on supporting a dark-mode
- Most elements done, but still need to do editors, tables and final
pass.
- Toggled only by quick js check at the moment, checking via css media
query. Need to make into user-preference toggle.

For #1234
2020-04-10 22:38:29 +01:00
Dan Brown
d4df18098f Cleaned up the activity service
- Added test to ensure activity on entity delete works as expected.
2020-04-10 20:55:33 +01:00
Dan Brown
7b8fe5fbc6 Added book-export endpoints to the API 2020-04-10 16:05:17 +01:00
Dan Brown
29705a25ce Reviewed and added testing for BookShelf API implementation
- Tweaked how books are passed on update to prevent unassignment if
parameter is not provided.
- Added books to validation so they show in docs.
- Added request/response examples.
- Added tests to cover.
- Added child book info to shelf info.

Review of #1908
2020-04-10 15:19:18 +01:00
Dan Brown
da1cea06ca Merge branch 'master' of git://github.com/osmansorkar/BookStack into osmansorkar-master 2020-04-10 13:49:28 +01:00
Dan Brown
ba1be9d710 Updated password reset process not to indicate if email exists
- Intended to prevent enumeration to check if a user exists.
- Updated messages on both the reqest-reset and set-password elements.
- Also updated notification auto-hide to be dynamic based upon the
amount of words within the notification.
- Added tests to cover.

For #2016
2020-04-10 13:38:08 +01:00
Dan Brown
053cbbd5b6 Updated view-change endpoints to be clearer, separated books and shelf
- Separated books-list and shelf-show view types to be saved separately.

During review of #1755
2020-04-10 12:49:16 +01:00
Dan Brown
b8c16b15a9 Merge branch 'feature_change_view_in_shelves_show' of git://github.com/philjak/BookStack into philjak-feature_change_view_in_shelves_show 2020-04-10 12:21:56 +01:00
Dan Brown
47e645909e Reviewed #1688, Show parent shelves on books page
- Moved list to the left of the page to align with other navigational
items.
- Hid list of no shelves, to help hide shelf references if not in use.
- Tweaked test to ensure it wasn't finding shelf name in breadcrumb
rather than list being tested.
2020-04-09 17:29:22 +01:00
Dan Brown
898cedf536 Merge branch 'feature/#1598' of git://github.com/cw1998/BookStack into cw1998-feature/#1598 2020-04-09 17:18:37 +01:00
Dan Brown
e83d2eedbb Added "update-url" command to find/replace url in the database
- Also aligned format of command descriptions.

Targeted most common columns.
Have not done revisions for the sake of keeping that
content true to how it was originally stored but could
cause unexpected behaviour.

For #1225
2020-04-09 16:59:26 +01:00
Dan Brown
1962c81742 Updated WYSIWYG entity link selector to set link display text
- Sets as entity name if the input is currently empty.

For #2014
2020-04-09 15:28:44 +01:00
Dan Brown
642db1387e Updated wysiwyg code-block insert flow to be mouseless
- Can now save a code block with Ctrl+Enter.
- Codemirror will be in focus on popup show.
- TinyMCE will get back focus on code save.

For #1972
2020-04-05 21:55:31 +01:00
Dan Brown
02f7ffe53c Removed overflow hidden from all lists
- Was causing ol list numbers to be cut off.

Fixes #1978
2020-04-05 18:05:51 +01:00
Dan Brown
5f61620cc2 Added support for changing the draw.io instance URL
- Allowed DRAWIO env option to be passed as URL to point to instance.
- Updated tests to check URL gets passed to pages correctly.
- Update default URL to be the default theme.

For #826
2020-04-05 17:27:16 +01:00
Dan Brown
ea9e9565ef Removed bmp and tiff support from uploaded images.
Fixes #1990
2020-04-05 16:15:05 +01:00
Dan Brown
feab756b9f Merge pull request #2003 from BookStackApp/rtl_styles_update
Updated styles to use logical properties/values, for improved RTL support
2020-04-05 14:15:23 +01:00
Dan Brown
fb08194af1 New Crowdin translations (#2004)
* New translations errors.php (Russian)

* New translations settings.php (Portuguese, Brazilian)

* New translations auth.php (Russian)

* New translations entities.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Slovak)

* New translations settings.php (Persian)

* New translations settings.php (Portuguese)

* New translations settings.php (Polish)

* New translations settings.php (Korean)

* New translations settings.php (Swedish)

* New translations validation.php (Swedish)

* New translations errors.php (Turkish)

* New translations settings.php (Turkish)

* New translations entities.php (Swedish)

* New translations settings.php (Ukrainian)

* New translations errors.php (Swedish)

* New translations entities.php (Slovenian)

* New translations errors.php (Slovenian)

* New translations pagination.php (Slovenian)

* New translations passwords.php (Slovenian)

* New translations settings.php (Slovenian)

* New translations validation.php (Slovenian)

* New translations settings.php (Spanish)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Czech)

* New translations pagination.php (Danish)

* New translations settings.php (Danish)

* New translations settings.php (Dutch)

* New translations settings.php (Arabic)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Hungarian)

* New translations errors.php (French)

* New translations errors.php (Italian)

* New translations settings.php (Italian)

* New translations settings.php (Japanese)

* New translations settings.php (French)

* New translations errors.php (German)

* New translations settings.php (German)

* New translations settings.php (Hebrew)

* New translations auth.php (Hebrew)

* New translations validation.php (Hebrew)

* New translations errors.php (Hebrew)

* New translations entities.php (Hebrew)

* New translations common.php (Hebrew)

* New translations settings.php (German Informal)

* New translations errors.php (German Informal)

* New translations settings.php (Vietnamese)
2020-04-05 14:12:23 +01:00
Dan Brown
f94fd44ff6 Updated styles to use logical properties/values
- Intended to improve RTL support in the interface.
- Also adds hebrew to language dropdown since that was missing.

Related to #1794
2020-04-05 13:07:19 +01:00
Dan Brown
f84bf8e883 Updated test files to be PSR-4 compliant
Closes #1924
2020-04-04 01:16:05 +01:00
Dan Brown
3500182c5f Updated drawing uploads to use user id in image name
- Instead of user name.
- Due to issues with advanced charts like emoji zero-width-joiners.
- Could also have security concerns on untrusted instances with certain
webserver config due to double extension possibilities.

Closes #1993
2020-04-04 00:48:32 +01:00
Dan Brown
ef416d3e86 Fixed editor JavaScript error in TemplateManager
- Caused when loading the editor with no templates in the system.
- Tried to init a search box that did not exist.
2020-04-04 00:09:58 +01:00
Dan Brown
c1fe068ffc Bumped npm packages up and ran audit-fix 2020-04-04 00:03:26 +01:00
Dan Brown
b0610d85da Updated socialite to fix deprecated GitHub auth method
- Also updated composer dependancies to cover symfony/http-foundation
security issue.

Fixes #1879
Related to #1989
2020-04-04 00:00:19 +01:00
Dan Brown
ed563fef28 Updated version and assets for release v0.28.3 2020-03-14 22:31:42 +00:00
Dan Brown
0d31a8e3f1 Merge branch 'master' into release 2020-03-14 22:31:11 +00:00
Dan Brown
64942268b8 Added Slovenian to available language options
Related to #1946
2020-03-14 22:24:27 +00:00
Dan Brown
b670ab61d6 Updated translations from crowdin
Squashed commit of the following:

commit 23861a31bb2398ca61655c584bd8c75ee9bccdad
Merge: d44acf4b 1c6287f2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 22:18:50 2020 +0000

    Merge branch 'master' into l10n_master

commit d44acf4b64
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 19:39:46 2020 +0000

    New translations errors.php (Portuguese, Brazilian)

commit c1a4cc5d12
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 19:14:49 2020 +0000

    New translations errors.php (Spanish, Argentina)

commit fb3c5dcffc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 19:14:48 2020 +0000

    New translations errors.php (Spanish)

commit f65c84635d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:42:05 2020 +0000

    New translations errors.php (German Informal)

commit 99fba6932a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:57 2020 +0000

    New translations errors.php (French)

commit bc4c9684b5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:55 2020 +0000

    New translations errors.php (Portuguese, Brazilian)

commit 0afce79807
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:43 2020 +0000

    New translations errors.php (Russian)

commit f3daf77b95
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:41 2020 +0000

    New translations errors.php (Hungarian)

commit 63848278a5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:40 2020 +0000

    New translations errors.php (Italian)

commit 53b5fce93c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:38 2020 +0000

    New translations errors.php (Japanese)

commit 2638f7a663
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:37 2020 +0000

    New translations errors.php (Korean)

commit f19ffa468c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:36 2020 +0000

    New translations errors.php (Persian)

commit 477ae0b845
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:34 2020 +0000

    New translations errors.php (Polish)

commit 56beebe12c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:33 2020 +0000

    New translations errors.php (German)

commit 2b6540654a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:32 2020 +0000

    New translations errors.php (Portuguese)

commit 80f7275011
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:30 2020 +0000

    New translations errors.php (Spanish)

commit 74c65f90ab
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:28 2020 +0000

    New translations errors.php (Spanish, Argentina)

commit 3bd9e4fb86
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:27 2020 +0000

    New translations errors.php (Swedish)

commit a698db00e3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:26 2020 +0000

    New translations errors.php (Turkish)

commit f3cfc63b5c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:25 2020 +0000

    New translations errors.php (Ukrainian)

commit 61eb76ac89
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:23 2020 +0000

    New translations errors.php (Vietnamese)

commit cfda94e2a8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:22 2020 +0000

    New translations errors.php (Slovak)

commit f0659025a9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:20 2020 +0000

    New translations errors.php (Dutch)

commit 27ac377ed6
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:19 2020 +0000

    New translations errors.php (Chinese Traditional)

commit 04bf21a325
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:18 2020 +0000

    New translations errors.php (Chinese Simplified)

commit 4dd5802979
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:16 2020 +0000

    New translations errors.php (Arabic)

commit 368cf2d248
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:11 2020 +0000

    New translations errors.php (Slovenian)

commit 52381df7be
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:08 2020 +0000

    New translations errors.php (Czech)

commit 7e8330510f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 14 18:41:07 2020 +0000

    New translations errors.php (Danish)

commit 1bf07c6acf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 22:57:41 2020 +0000

    New translations entities.php (Slovenian)

commit 4d2d120b57
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 22:27:35 2020 +0000

    New translations entities.php (Slovenian)

commit edfc88eb8e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 21:55:51 2020 +0000

    New translations entities.php (Slovenian)

commit 840ed35d34
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 21:22:08 2020 +0000

    New translations entities.php (Slovenian)

commit aa130af285
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 14:02:50 2020 +0000

    New translations common.php (Czech)

commit 5007fee528
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 14:02:25 2020 +0000

    New translations entities.php (Slovenian)

commit 1ad8874e6b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 13:30:21 2020 +0000

    New translations auth.php (Czech)

commit 56f17ff7d4
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 13:29:57 2020 +0000

    New translations entities.php (Slovenian)

commit 33789962cc
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 11:09:18 2020 +0000

    New translations components.php (Slovenian)

commit 1002cf9e3b
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 10:08:48 2020 +0000

    New translations components.php (Slovenian)

commit d5a6083ae8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 09:37:23 2020 +0000

    New translations components.php (Slovenian)

commit 0d8df3a72e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 09:07:01 2020 +0000

    New translations components.php (Slovenian)

commit 22337688f9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 09:07:00 2020 +0000

    New translations common.php (Slovenian)

commit ff812694e2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 08:20:44 2020 +0000

    New translations common.php (Slovenian)

commit 57b9e04927
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 06:17:11 2020 +0000

    New translations auth.php (Spanish)

commit 25343baac1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 06:16:52 2020 +0000

    New translations activities.php (Spanish)

commit a5c72ee5f1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Wed Mar 11 05:37:55 2020 +0000

    New translations activities.php (Spanish)

commit 6a6f28d095
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 15:49:14 2020 +0000

    New translations auth.php (Slovenian)

commit 33f30876e1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 15:18:30 2020 +0000

    New translations auth.php (Slovenian)

commit 8bf8bc0fe5
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 14:48:46 2020 +0000

    New translations auth.php (Slovenian)

commit 303dc07704
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 14:48:44 2020 +0000

    New translations activities.php (Slovenian)

commit c219fb67aa
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 09:59:45 2020 +0000

    New translations activities.php (Slovenian)

commit f45b6c6c82
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 08:34:32 2020 +0000

    New translations activities.php (Slovenian)

commit 7d690eb13f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 02:56:48 2020 +0000

    New translations entities.php (Russian)

commit 86aab0529e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Tue Mar 10 02:56:43 2020 +0000

    New translations common.php (Russian)

commit 4fc1945543
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 20:37:26 2020 +0000

    New translations errors.php (Russian)

commit 09791b736d
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 20:07:52 2020 +0000

    New translations settings.php (Russian)

commit 9cb04bea4c
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:50 2020 +0000

    New translations validation.php (Slovenian)

commit 7964a5a2a0
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:34 2020 +0000

    New translations settings.php (Slovenian)

commit 361be13ff7
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:32 2020 +0000

    New translations passwords.php (Slovenian)

commit 9b6a7f0f64
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:31 2020 +0000

    New translations pagination.php (Slovenian)

commit a815adc24f
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:30 2020 +0000

    New translations errors.php (Slovenian)

commit ae6040af3a
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:28 2020 +0000

    New translations entities.php (Slovenian)

commit f1d0177dce
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:26 2020 +0000

    New translations components.php (Slovenian)

commit 16ba9f1fe1
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:24 2020 +0000

    New translations common.php (Slovenian)

commit caa47464ba
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:23 2020 +0000

    New translations auth.php (Slovenian)

commit 4b9a78aef8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Mon Mar 9 18:49:21 2020 +0000

    New translations activities.php (Slovenian)

commit 6a55161fe9
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sun Mar 8 01:36:31 2020 +0000

    New translations common.php (French)

commit 94235393fa
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 7 18:49:01 2020 +0000

    New translations errors.php (Portuguese, Brazilian)

commit a0f75e7724
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 7 10:24:26 2020 +0000

    New translations settings.php (Russian)

commit 65437712a2
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 7 09:37:34 2020 +0000

    New translations auth.php (Russian)

commit f7f6a92dcf
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 7 00:01:07 2020 +0000

    New translations entities.php (Russian)

commit ac8819edd3
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Sat Mar 7 00:00:57 2020 +0000

    New translations settings.php (Russian)

commit f1290f2b87
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 23:23:44 2020 +0000

    New translations entities.php (Russian)

commit 8efb9bd571
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 22:45:53 2020 +0000

    New translations entities.php (Russian)

commit 4fd31df14e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 22:45:44 2020 +0000

    New translations settings.php (Russian)

commit 262a2b3e4e
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 22:11:41 2020 +0000

    New translations entities.php (Russian)

commit 15c98a64b8
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 15:10:54 2020 +0000

    New translations errors.php (French)

commit 7a274db381
Author: Dan Brown <ssddanbrown@googlemail.com>
Date:   Fri Mar 6 15:10:43 2020 +0000

    New translations settings.php (French)
2020-03-14 22:20:01 +00:00
Dan Brown
1c6287f245 Updated translator list from Crowdin 2020-03-14 22:15:01 +00:00
Dan Brown
775ee1bde3 Merge branch 'patch-1' of git://github.com/MikeyMJCO/BookStack into MikeyMJCO-patch-1 2020-03-14 20:53:28 +00:00
Dan Brown
82278bd0f4 Updated readme
- Updated dev details to be current.
- Added warning for auto-fixing via phpcbf.
- Added warning that existing issue does not mean accepted pull request.
2020-03-14 18:56:49 +00:00
Dan Brown
8a4e75ef32 Added a "Start Database" step to github action flow
https://github.blog/changelog/2020-02-21-github-actions-breaking-change-ubuntu-virtual-environments-will-no-longer-start-the-mysql-service-automatically/
2020-03-14 18:44:02 +00:00
Dan Brown
7f6cbead33 Performed review of "public intended" functionality provided in #1817
- Updated logic to take url from referrer rather than pass as a query parameter.
- Added tests to cover functionality.
- Updated 404 page with login action button if not signed in.
- Updated 404 page with text to indicate permissions may be affecting visibility.

Related to #1817 and #1706
2020-03-14 18:29:31 +00:00
Dan Brown
a95588dc2e Merge branch 'feature/public-login-redirect' of git://github.com/Xiphoseer/BookStack into Xiphoseer-feature/public-login-redirect 2020-03-14 17:46:30 +00:00
Dan Brown
217954694c Updated npm dependancies 2020-03-14 17:38:39 +00:00
Dan Brown
17da30aa29 Updated default mail options 2020-03-14 17:32:11 +00:00
Dan Brown
aa12e48d73 Merge branch 'TBK-validation_fixes' 2020-03-14 12:46:01 +00:00
Dan Brown
200772da72 Merge branch 'validation_fixes' of git://github.com/TBK/BookStack into TBK-validation_fixes 2020-03-14 12:42:59 +00:00
dependabot[bot]
d7760b1b25 Bump acorn from 6.4.0 to 6.4.1
Bumps [acorn](https://github.com/acornjs/acorn) from 6.4.0 to 6.4.1.
- [Release notes](https://github.com/acornjs/acorn/releases)
- [Commits](https://github.com/acornjs/acorn/compare/6.4.0...6.4.1)

Signed-off-by: dependabot[bot] <support@github.com>
2020-03-14 11:31:01 +00:00
Dan Brown
a5f972043b Updated primary color action text to be consistent
- With other similar picker components on the page.

As reported in #1930
2020-03-11 21:51:43 +00:00
Dan Brown
ae1f237a1f Merge branch 'Statium-master' 2020-03-11 21:43:16 +00:00
Statium
8a681fd796 Update entities.php
More accurate translation, error correction.
2020-03-11 21:40:50 +00:00
Statium
fa9944610d Update common.php
More accurate translation, error correction.
2020-03-11 21:38:29 +00:00
Statium
bebda6ff1a Update auth.php
More accurate translation, error correction.
2020-03-11 21:38:29 +00:00
Statium
991f6da9f4 Update activities.php
More accurate translation, error correction.
2020-03-11 21:35:31 +00:00
Dan Brown
c6ff6db784 Merge branch 'Statium-patch-1' 2020-03-11 21:28:09 +00:00
Statium
b58110000d Code refactoring
Removed extra spaces displayed in the header of the login and registration link.
2020-03-11 21:27:22 +00:00
Statium
f87c3b2660 Update setting-entity-color-picker.blade.php
Reducing indentation to one look in the application settings.
2020-03-11 21:23:04 +00:00
Dan Brown
59aefe5371 Updated social auth to take name from email if empty
- Added tests to cover.

Fixes #1853
2020-03-10 19:09:22 +00:00
Dan Brown
ccf92331c2 Updated readme with extra discord and issue list links 2020-03-06 20:26:11 +00:00
Dan Brown
30db8af460 Merge branch 'master' of git://github.com/ch0wm3in/BookStack into ch0wm3in-master 2020-03-06 20:10:57 +00:00
Dan Brown
56be10f1cd Merge branch 'perl_syntax_highlight' of git://github.com/Iyeyasu/BookStack into Iyeyasu-perl_syntax_highlight 2020-03-06 19:54:15 +00:00
Dan Brown
4b2654598c Merge branch 'master' of git://github.com/JHenneberg/BookStack into JHenneberg-master 2020-03-06 19:49:16 +00:00
Dan Brown
dbf5a87adb Lang setting list changes via Crowdin
* New translations settings.php (Italian)

* New translations settings.php (Vietnamese)

* New translations settings.php (Ukrainian)

* New translations settings.php (Turkish)

* New translations settings.php (Swedish)

* New translations settings.php (Spanish, Argentina)

* New translations settings.php (Spanish)

* New translations settings.php (Slovak)

* New translations settings.php (Portuguese)

* New translations settings.php (Polish)

* New translations settings.php (Persian)

* New translations settings.php (Korean)

* New translations settings.php (Japanese)

* New translations settings.php (German)

* New translations settings.php (Russian)

* New translations settings.php (French)

* New translations settings.php (Dutch)

* New translations settings.php (Czech)

* New translations settings.php (Chinese Traditional)

* New translations settings.php (Chinese Simplified)

* New translations settings.php (Arabic)

* New translations settings.php (Danish)

* New translations settings.php (Portuguese, Brazilian)

* New translations settings.php (Hungarian)

* New translations settings.php (German Informal)
2020-03-04 22:43:30 +00:00
Dan Brown
b94b945fb0 Merge branch 'master' of git://github.com/Binternet/BookStack into Binternet-master 2020-03-04 22:22:08 +00:00
Dan Brown
34616ac195 Updated lanauge lists to match latest translations 2020-03-04 22:14:25 +00:00
Dan Brown
6303c788ed New Crowdin translations (#1868)
* New translations settings.php (Spanish)

* New translations errors.php (Japanese)

* New translations errors.php (Japanese)

* New translations components.php (Japanese)

* New translations settings.php (German)

* New translations settings.php (German)

* New translations auth.php (German Informal)

* New translations errors.php (German Informal)

* New translations settings.php (German Informal)

* New translations entities.php (German Informal)

* New translations activities.php (Persian)

* New translations auth.php (Persian)

* New translations common.php (Persian)

* New translations components.php (Persian)

* New translations entities.php (Persian)

* New translations errors.php (Persian)

* New translations pagination.php (Persian)

* New translations passwords.php (Persian)

* New translations settings.php (Persian)

* New translations validation.php (Persian)

* New translations settings.php (German Informal)

* New translations validation.php (German Informal)

* New translations settings.php (French)

* New translations errors.php (French)

* New translations settings.php (French)

* New translations settings.php (Hungarian)

* New translations activities.php (Portuguese)

* New translations auth.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations passwords.php (Vietnamese)

* New translations pagination.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations common.php (Vietnamese)

* New translations activities.php (Vietnamese)

* New translations auth.php (Portuguese)

* New translations validation.php (Portuguese)

* New translations settings.php (Portuguese)

* New translations passwords.php (Portuguese)

* New translations pagination.php (Portuguese)

* New translations errors.php (Portuguese)

* New translations entities.php (Portuguese)

* New translations components.php (Portuguese)

* New translations common.php (Portuguese)

* New translations validation.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations auth.php (Vietnamese)

* New translations components.php (Vietnamese)

* New translations auth.php (Vietnamese)

* New translations activities.php (Vietnamese)

* New translations auth.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations pagination.php (Vietnamese)

* New translations passwords.php (Vietnamese)

* New translations common.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations common.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations validation.php (Vietnamese)

* New translations validation.php (Vietnamese)

* New translations validation.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations settings.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (French)

* New translations errors.php (German)

* New translations errors.php (Arabic)

* New translations errors.php (Czech)

* New translations errors.php (Danish)

* New translations errors.php (Dutch)

* New translations errors.php (Hungarian)

* New translations errors.php (Italian)

* New translations errors.php (Chinese Traditional)

* New translations errors.php (Swedish)

* New translations errors.php (Portuguese)

* New translations errors.php (Persian)

* New translations errors.php (German Informal)

* New translations errors.php (Ukrainian)

* New translations errors.php (Turkish)

* New translations errors.php (Korean)

* New translations errors.php (Spanish, Argentina)

* New translations errors.php (Spanish)

* New translations errors.php (Slovak)

* New translations errors.php (Russian)

* New translations errors.php (Polish)

* New translations errors.php (Japanese)

* New translations errors.php (Portuguese, Brazilian)

* New translations errors.php (Vietnamese)

* New translations errors.php (Spanish)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations entities.php (Vietnamese)

* New translations errors.php (Vietnamese)

* New translations auth.php (Swedish)

* New translations common.php (Swedish)

* New translations entities.php (Swedish)

* New translations settings.php (Swedish)

* New translations errors.php (Chinese Simplified)

* New translations errors.php (Russian)

* New translations errors.php (Russian)

* New translations common.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Russian)

* New translations errors.php (Hungarian)

* New translations settings.php (Hungarian)

* New translations settings.php (Russian)

* New translations errors.php (Russian)

* New translations settings.php (Russian)

* New translations activities.php (Russian)

* New translations auth.php (Russian)

* New translations components.php (Russian)

* New translations entities.php (Russian)

* New translations validation.php (Russian)

* New translations errors.php (Russian)

* New translations common.php (Russian)

* New translations entities.php (Russian)

* New translations errors.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Portuguese, Brazilian)

* New translations auth.php (Russian)

* New translations components.php (Russian)

* New translations entities.php (Russian)

* New translations errors.php (Russian)

* New translations errors.php (Russian)

* New translations settings.php (Russian)

* New translations passwords.php (Russian)

* New translations auth.php (Danish)

* New translations auth.php (Danish)

* New translations common.php (Danish)

* New translations components.php (Danish)

* New translations entities.php (Danish)

* New translations entities.php (Danish)

* New translations entities.php (Danish)

* New translations errors.php (Danish)

* New translations errors.php (Russian)

* New translations settings.php (Russian)

* New translations settings.php (Russian)

* New translations validation.php (Russian)

* New translations errors.php (Danish)

* New translations errors.php (Danish)

* New translations settings.php (Danish)

* New translations settings.php (Danish)

* New translations settings.php (Danish)

* New translations validation.php (Danish)

* New translations validation.php (Danish)

* New translations settings.php (Danish)

* New translations settings.php (Danish)

* New translations auth.php (Russian)

* New translations settings.php (Russian)

* New translations errors.php (Russian)

* New translations settings.php (Russian)

* New translations validation.php (Russian)

* New translations settings.php (Russian)
2020-03-04 21:58:04 +00:00
TBK
57f587a78b Allow book, shelf, settings & profile form input validation to skip image 2020-03-04 00:17:53 +01:00
TBK
d3737d5a87 Remove redundant getImageValidationRules method 2020-03-04 00:17:49 +01:00
TBK
5cd56f63ff Change check to verify that request is present and contains a file 2020-03-04 00:17:45 +01:00
osmansorkar
1859c7917f added api functionality to handle book Shelves 2020-02-23 11:41:49 +06:00
Mikey O'Toole
65c4985710 Resolve issue #1911 (Additional margin/padding present in nested lists)
Optionally we could consider removing the rule at line 274. This doesn't handle multiple layers of nested lists and would be better covered by the generic rule which checks for an `ol` or `ul` nested inside a `li` which is how MarkDown renders nested lists as HTML.
2020-02-20 14:25:23 +00:00
osmansorkar
01adf39be8 added Handle Authorization Header on .htaccess to solve Authorization problem get from laravel orginal github ripo 2020-02-19 12:44:23 +06:00
jzoy
27191d1a2c Merge pull request #1 from BookStackApp/master
同步作者更新内容
2020-02-18 22:30:53 +08:00
ch0wm3in
c1fe81466f Fixed 'interaction_required' response for azure
Azure Conditional Access policy 2FA returns 'interaction_required' 400 response https://github.com/SocialiteProviders/Providers/issues/208
2020-02-12 15:03:55 +01:00
benrubson
12a9a45747 Log failed accesses 2020-02-09 10:01:33 +01:00
JHenneberg
0df0227ad4 Added support for Fortran language
sorted import alphabetically
2020-02-07 13:45:19 +01:00
D4rt
b059744fb5 Add Perl syntax higlighting to code editor 2020-01-19 07:41:18 +02:00
Lior Broshi
80a50f1ecb added rtl support for hebrew + added to localMap 2019-12-29 23:06:54 +02:00
Lior Broshi
23ad8024ec resolved conflict 2019-12-29 23:03:10 +02:00
Lior Broshi
da03e34c67 added he locale to configuration 2019-12-29 23:01:45 +02:00
Lior Broshi
5f333eebf0 validation 2019-12-29 22:53:42 +02:00
Lior Broshi
8794f62ad8 settings 2019-12-24 23:36:35 +02:00
Lior Broshi
290610b77e more heb 2019-12-23 22:04:15 +02:00
Lior Broshi
b65d4cda0e pagination, passwords and part of settings 2019-12-21 22:34:50 +02:00
Daniel Seiler
afa501e75b Recall previous route when manually clicking login 2019-12-14 08:41:22 +01:00
jakob
6cd26e23a8 Allow toggling between grid and list view in shelf view (shelves.show) 2019-10-30 11:23:42 +01:00
Christopher Wilkinson
4ad4dfa55a Show bookshelves that a book belongs to on a book view
Closes #1598
2019-09-27 00:45:22 +01:00
Lior Broshi
974e52be5c errors.php 2019-09-08 23:08:49 +03:00
Lior Broshi
d84de86b40 entities.php 2019-09-07 22:46:30 +03:00
Lior Broshi
019085071a entities, not done yet 2019-08-15 23:24:45 +03:00
Lior Broshi
6c7e74f475 components 2019-07-22 22:57:00 +03:00
Lior Broshi
b153ffd019 common 2019-07-22 22:35:29 +03:00
Lior Broshi
9cfa391ab6 auth 2019-07-22 22:31:01 +03:00
Lior Broshi
de140ab5a5 started Hebrew translation, not done yet 2019-07-09 22:40:35 +03:00
579 changed files with 24729 additions and 11876 deletions

View File

@@ -1,3 +1,11 @@
# This file, when named as ".env" in the root of your BookStack install
# folder, is used for the core configuration of the application.
# By default this file contains the most common required options but
# a full list of options can be found in the '.env.example.complete' file.
# NOTE: If any of your values contain a space or a hash you will need to
# wrap the entire value in quotes. (eg. MAIL_FROM_NAME="BookStack Mailer")
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
@@ -5,7 +13,7 @@ APP_KEY=SomeRandomString
# Application URL
# Remove the hash below and set a URL if using BookStack behind
# a proxy, if using a third-party authentication option.
# a proxy or if using a third-party authentication option.
# This must be the root URL that you want to host BookStack on.
# All URL's in BookStack will be generated using this value.
#APP_URL=https://example.com
@@ -17,15 +25,18 @@ DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
# Can be 'smtp' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sender options
MAIL_FROM_NAME=BookStack
MAIL_FROM=bookstack@example.com
# SMTP mail options
# These settings can be checked using the "Send a Test Email"
# feature found in the "Settings > Maintenance" area of the system.
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# A full list of options can be found in the '.env.example.complete' file.
MAIL_ENCRYPTION=null

View File

@@ -238,7 +238,10 @@ DISABLE_EXTERNAL_SERVICES=false
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable Draw.io integration
# Enable draw.io integration
# Can simply be true/false to enable/disable the integration.
# Alternatively, It can be URL to the draw.io instance you want to use.
# For URLs, The following URL parameters should be included: embed=1&proto=json&spin=1
DRAWIO=true
# Default item listing view
@@ -267,4 +270,12 @@ API_DEFAULT_ITEM_COUNT=100
API_MAX_ITEM_COUNT=500
# The number of API requests that can be made per minute by a single user.
API_REQUESTS_PER_MIN=180
API_REQUESTS_PER_MIN=180
# Enable the logging of failed email+password logins with the given message.
# The default log channel below uses the php 'error_log' function which commonly
# results in messages being output to the webserver error logs.
# The message can contain a %u parameter which will be replaced with the login
# user identifier (Username or email).
LOG_FAILED_LOGIN_MESSAGE=false
LOG_FAILED_LOGIN_CHANNEL=errorlog_plain_webserver

View File

@@ -61,7 +61,7 @@ Rodrigo Saczuk Niz (rodrigoniz) :: Portuguese, Brazilian
aekramer :: Dutch
JachuPL :: Polish
milesteg :: Hungarian
Beenbag :: German
Beenbag :: German; German Informal
Lett3rs :: Danish
Julian (julian.henneberg) :: German; German Informal
3GNWn :: Danish
@@ -69,5 +69,54 @@ dbguichu :: Chinese Simplified
Randy Kim (hyunjun) :: Korean
Francesco M. Taurino (ftaurino) :: Italian
DanielFrederiksen :: Danish
Finn Wessel (19finnwessel6) :: German
Finn Wessel (19finnwessel6) :: German Informal; German
Gustav Kånåhols (Kurbitz) :: Swedish
Vuong Trung Hieu (fpooon) :: Vietnamese
Emil Petersen (emoyly) :: Danish
mrjaboozy :: Slovenian
Statium :: Russian
Mikkel Struntze (MStruntze) :: Danish
kostefun :: Russian
Tuyen.NG (tuyendev) :: Vietnamese
Ghost_chu (dbguichu) :: Chinese Simplified
Ziipen :: Danish
Samuel Schwarz (Guiph7quan) :: Czech
Aleph (toishoki) :: Turkish
Julio Alberto García (Yllelder) :: Spanish
Rafael (raribeir) :: Portuguese, Brazilian
Hiroyuki Odake (dakesan) :: Japanese
Alex Lee (qianmengnet) :: Chinese Simplified
swinn37 :: French
Hasan Özbey (the-turk) :: Turkish
rcy :: Swedish
Ali Yasir Yılmaz (ayyilmaz) :: Turkish
scureza :: Italian
Biepa :: German Informal; German
syecu :: Chinese Simplified
Lap1t0r :: French
Thinkverse (thinkverse) :: Swedish
alef (toishoki) :: Turkish
Robbert Feunekes (Muukuro) :: Dutch
seohyeon.joo :: Korean
Orenda (OREDNA) :: Bulgarian
Marek Pavelka (marapavelka) :: Czech
Venkinovec :: Czech
Tommy Ku (tommyku) :: Chinese Traditional; Japanese
Michał Bielejewski (bielej) :: Polish
jozefrebjak :: Slovak
Ikhwan Koo (Ikhwan.Koo) :: Korean
Whay (remkovdhoef) :: Dutch
jc7115 :: Chinese Traditional
주서현 (seohyeon.joo) :: Korean
ReadySystems :: Arabic
HFinch :: German; German Informal
brechtgijsens :: Dutch
Lowkey (v587ygq) :: Chinese Simplified
sdl-blue :: German Informal
sqlik :: Polish
Roy van Schaijk (royvanschaijk) :: Dutch
Simsimpicpic :: French
Zenahr Barzani (Zenahr) :: German; Japanese; Dutch; German Informal
tatsuya.info :: Japanese
fadiapp :: Arabic
Jakub “Jéžiš” Bouček (jakubboucek) :: Czech

View File

@@ -16,7 +16,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
php: [7.2, 7.3]
php: [7.2, 7.4]
steps:
- uses: actions/checkout@v1
@@ -31,6 +31,10 @@ jobs:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ matrix.php }}
- name: Start Database
run: |
sudo /etc/init.d/mysql start
- name: Setup Database
run: |
mysql -uroot -proot -e 'CREATE DATABASE IF NOT EXISTS `bookstack-test`;'

View File

@@ -50,10 +50,8 @@ class Activity extends Model
/**
* Checks if another Activity matches the general information of another.
* @param $activityB
* @return bool
*/
public function isSimilarTo($activityB)
public function isSimilarTo(Activity $activityB): bool
{
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
}

View File

@@ -1,8 +1,10 @@
<?php namespace BookStack\Actions;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Book;
use BookStack\Auth\User;
use BookStack\Entities\Entity;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Log;
class ActivityService
{
@@ -12,8 +14,6 @@ class ActivityService
/**
* ActivityService constructor.
* @param Activity $activity
* @param PermissionService $permissionService
*/
public function __construct(Activity $activity, PermissionService $permissionService)
{
@@ -24,11 +24,8 @@ class ActivityService
/**
* Add activity data to database.
* @param \BookStack\Entities\Entity $entity
* @param string $activityKey
* @param int $bookId
*/
public function add(Entity $entity, string $activityKey, int $bookId = null)
public function add(Entity $entity, string $activityKey, ?int $bookId = null)
{
$activity = $this->newActivityForUser($activityKey, $bookId);
$entity->activity()->save($activity);
@@ -37,11 +34,8 @@ class ActivityService
/**
* Adds a activity history with a message, without binding to a entity.
* @param string $activityKey
* @param string $message
* @param int $bookId
*/
public function addMessage(string $activityKey, string $message, int $bookId = null)
public function addMessage(string $activityKey, string $message, ?int $bookId = null)
{
$this->newActivityForUser($activityKey, $bookId)->forceFill([
'extra' => $message
@@ -52,14 +46,11 @@ class ActivityService
/**
* Get a new activity instance for the current user.
* @param string $key
* @param int|null $bookId
* @return Activity
*/
protected function newActivityForUser(string $key, int $bookId = null)
protected function newActivityForUser(string $key, ?int $bookId = null): Activity
{
return $this->activity->newInstance()->forceFill([
'key' => strtolower($key),
'key' => strtolower($key),
'user_id' => $this->user->id,
'book_id' => $bookId ?? 0,
]);
@@ -69,34 +60,27 @@ class ActivityService
* Removes the entity attachment from each of its activities
* and instead uses the 'extra' field with the entities name.
* Used when an entity is deleted.
* @param \BookStack\Entities\Entity $entity
* @return mixed
*/
public function removeEntity(Entity $entity)
public function removeEntity(Entity $entity): Collection
{
// TODO - Rewrite to db query.
$activities = $entity->activity;
foreach ($activities as $activity) {
$activity->extra = $entity->name;
$activity->entity_id = 0;
$activity->entity_type = null;
$activity->save();
}
$activities = $entity->activity()->get();
$entity->activity()->update([
'extra' => $entity->name,
'entity_id' => 0,
'entity_type' => '',
]);
return $activities;
}
/**
* Gets the latest activity.
* @param int $count
* @param int $page
* @return array
*/
public function latest($count = 20, $page = 0)
public function latest(int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
->with('user', 'entity')
->with(['user', 'entity'])
->skip($count * $page)
->take($count)
->get();
@@ -107,20 +91,16 @@ class ActivityService
/**
* Gets the latest activity for an entity, Filtering out similar
* items to prevent a message activity list.
* @param \BookStack\Entities\Entity $entity
* @param int $count
* @param int $page
* @return array
*/
public function entityActivity($entity, $count = 20, $page = 1)
public function entityActivity(Entity $entity, int $count = 20, int $page = 1): array
{
if ($entity->isA('book')) {
$query = $this->activity->where('book_id', '=', $entity->id);
$query = $this->activity->newQuery()->where('book_id', '=', $entity->id);
} else {
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
$query = $this->activity->newQuery()->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id);
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')
@@ -133,18 +113,18 @@ class ActivityService
}
/**
* Get latest activity for a user, Filtering out similar
* items.
* @param $user
* @param int $count
* @param int $page
* @return array
* Get latest activity for a user, Filtering out similar items.
*/
public function userActivity($user, $count = 20, $page = 0)
public function userActivity(User $user, int $count = 20, int $page = 0): array
{
$activityList = $this->permissionService
->filterRestrictedEntityRelations($this->activity, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->where('user_id', '=', $user->id)->skip($count * $page)->take($count)->get();
->orderBy('created_at', 'desc')
->where('user_id', '=', $user->id)
->skip($count * $page)
->take($count)
->get();
return $this->filterSimilar($activityList);
}
@@ -153,29 +133,26 @@ class ActivityService
* @param Activity[] $activities
* @return array
*/
protected function filterSimilar($activities)
protected function filterSimilar(iterable $activities): array
{
$newActivity = [];
$previousItem = false;
$previousItem = null;
foreach ($activities as $activityItem) {
if ($previousItem === false) {
$previousItem = $activityItem;
$newActivity[] = $activityItem;
continue;
}
if (!$activityItem->isSimilarTo($previousItem)) {
if (!$previousItem || !$activityItem->isSimilarTo($previousItem)) {
$newActivity[] = $activityItem;
}
$previousItem = $activityItem;
}
return $newActivity;
}
/**
* Flashes a notification message to the session if an appropriate message is available.
* @param $activityKey
*/
protected function setNotification($activityKey)
protected function setNotification(string $activityKey)
{
$notificationTextKey = 'activities.' . $activityKey . '_notification';
if (trans()->has($notificationTextKey)) {
@@ -183,4 +160,20 @@ class ActivityService
session()->flash('success', $message);
}
}
/**
* Log out a failed login attempt, Providing the given username
* as part of the message if the '%u' string is used.
*/
public function logFailedLogin(string $username)
{
$message = config('logging.failed_login.message');
if (!$message) {
return;
}
$message = str_replace("%u", $username, $message);
$channel = config('logging.failed_login.channel');
Log::channel($channel)->warning($message);
}
}

View File

@@ -2,9 +2,15 @@
use BookStack\Ownable;
/**
* @property string text
* @property string html
* @property int|null parent_id
* @property int local_id
*/
class Comment extends Ownable
{
protected $fillable = ['text', 'html', 'parent_id'];
protected $fillable = ['text', 'parent_id'];
protected $appends = ['created', 'updated'];
/**

View File

@@ -1,23 +1,20 @@
<?php namespace BookStack\Actions;
use BookStack\Entities\Entity;
use League\CommonMark\CommonMarkConverter;
/**
* Class CommentRepo
* @package BookStack\Repos
*/
class CommentRepo
{
/**
* @var \BookStack\Actions\Comment $comment
* @var Comment $comment
*/
protected $comment;
/**
* CommentRepo constructor.
* @param \BookStack\Actions\Comment $comment
*/
public function __construct(Comment $comment)
{
$this->comment = $comment;
@@ -25,65 +22,71 @@ class CommentRepo
/**
* Get a comment by ID.
* @param $id
* @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
*/
public function getById($id)
public function getById(int $id): Comment
{
return $this->comment->newQuery()->findOrFail($id);
}
/**
* Create a new comment on an entity.
* @param \BookStack\Entities\Entity $entity
* @param array $data
* @return \BookStack\Actions\Comment
*/
public function create(Entity $entity, $data = [])
public function create(Entity $entity, string $text, ?int $parent_id): Comment
{
$userId = user()->id;
$comment = $this->comment->newInstance($data);
$comment = $this->comment->newInstance();
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
$comment->created_by = $userId;
$comment->updated_by = $userId;
$comment->local_id = $this->getNextLocalId($entity);
$comment->parent_id = $parent_id;
$entity->comments()->save($comment);
return $comment;
}
/**
* Update an existing comment.
* @param \BookStack\Actions\Comment $comment
* @param array $input
* @return mixed
*/
public function update($comment, $input)
public function update(Comment $comment, string $text): Comment
{
$comment->updated_by = user()->id;
$comment->update($input);
$comment->text = $text;
$comment->html = $this->commentToHtml($text);
$comment->save();
return $comment;
}
/**
* Delete a comment from the system.
* @param \BookStack\Actions\Comment $comment
* @return mixed
*/
public function delete($comment)
public function delete(Comment $comment)
{
return $comment->delete();
$comment->delete();
}
/**
* Convert the given comment markdown text to HTML.
*/
public function commentToHtml(string $commentText): string
{
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'max_nesting_level' => 10,
'allow_unsafe_links' => false,
]);
return $converter->convertToHtml($commentText);
}
/**
* Get the next local ID relative to the linked entity.
* @param \BookStack\Entities\Entity $entity
* @return int
*/
protected function getNextLocalId(Entity $entity)
protected function getNextLocalId(Entity $entity): int
{
$comments = $entity->comments(false)->orderBy('local_id', 'desc')->first();
if ($comments === null) {
return 1;
}
return $comments->local_id + 1;
return ($comments->local_id ?? 0) + 1;
}
}

View File

@@ -9,6 +9,7 @@ use BookStack\Model;
class Tag extends Model
{
protected $fillable = ['name', 'value', 'order'];
protected $hidden = ['id', 'entity_id', 'entity_type'];
/**
* Get the entity that this tag belongs to

View File

@@ -2,71 +2,31 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use DB;
use Illuminate\Support\Collection;
/**
* Class TagRepo
* @package BookStack\Repos
*/
class TagRepo
{
protected $tag;
protected $entity;
protected $permissionService;
/**
* TagRepo constructor.
* @param \BookStack\Actions\Tag $attr
* @param \BookStack\Entities\Entity $ent
* @param \BookStack\Auth\Permissions\PermissionService $ps
*/
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
public function __construct(Tag $tag, PermissionService $ps)
{
$this->tag = $attr;
$this->entity = $ent;
$this->tag = $tag;
$this->permissionService = $ps;
}
/**
* Get an entity instance of its particular type.
* @param $entityType
* @param $entityId
* @param string $action
* @return \Illuminate\Database\Eloquent\Model|null|static
*/
public function getEntity($entityType, $entityId, $action = 'view')
{
$entityInstance = $this->entity->getEntityInstance($entityType);
$searchQuery = $entityInstance->where('id', '=', $entityId)->with('tags');
$searchQuery = $this->permissionService->enforceEntityRestrictions($entityType, $searchQuery, $action);
return $searchQuery->first();
}
/**
* Get all tags for a particular entity.
* @param string $entityType
* @param int $entityId
* @return mixed
*/
public function getForEntity($entityType, $entityId)
{
$entity = $this->getEntity($entityType, $entityId);
if ($entity === null) {
return collect();
}
return $entity->tags;
}
/**
* Get tag name suggestions from scanning existing tag names.
* If no search term is given the 50 most popular tag names are provided.
* @param $searchTerm
* @return array
*/
public function getNameSuggestions($searchTerm = false)
public function getNameSuggestions(?string $searchTerm): Collection
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('name');
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('name');
if ($searchTerm) {
$query = $query->where('name', 'LIKE', $searchTerm . '%')->orderBy('name', 'desc');
@@ -82,13 +42,10 @@ class TagRepo
* Get tag value suggestions from scanning existing tag values.
* If no search is given the 50 most popular values are provided.
* Passing a tagName will only find values for a tags with a particular name.
* @param $searchTerm
* @param $tagName
* @return array
*/
public function getValueSuggestions($searchTerm = false, $tagName = false)
public function getValueSuggestions(?string $searchTerm, ?string $tagName): Collection
{
$query = $this->tag->select('*', \DB::raw('count(*) as count'))->groupBy('value');
$query = $this->tag->select('*', DB::raw('count(*) as count'))->groupBy('value');
if ($searchTerm) {
$query = $query->where('value', 'LIKE', $searchTerm . '%')->orderBy('value', 'desc');
@@ -96,7 +53,7 @@ class TagRepo
$query = $query->orderBy('count', 'desc')->take(50);
}
if ($tagName !== false) {
if ($tagName) {
$query = $query->where('name', '=', $tagName);
}
@@ -106,35 +63,28 @@ class TagRepo
/**
* Save an array of tags to an entity
* @param \BookStack\Entities\Entity $entity
* @param array $tags
* @return array|\Illuminate\Database\Eloquent\Collection
*/
public function saveTagsToEntity(Entity $entity, $tags = [])
public function saveTagsToEntity(Entity $entity, array $tags = []): iterable
{
$entity->tags()->delete();
$newTags = [];
foreach ($tags as $tag) {
if (trim($tag['name']) === '') {
continue;
}
$newTags[] = $this->newInstanceFromInput($tag);
}
$newTags = collect($tags)->filter(function ($tag) {
return boolval(trim($tag['name']));
})->map(function ($tag) {
return $this->newInstanceFromInput($tag);
})->all();
return $entity->tags()->saveMany($newTags);
}
/**
* Create a new Tag instance from user input.
* @param $input
* @return \BookStack\Actions\Tag
* Input must be an array with a 'name' and an optional 'value' key.
*/
protected function newInstanceFromInput($input)
protected function newInstanceFromInput(array $input): Tag
{
$name = trim($input['name']);
$value = isset($input['value']) ? trim($input['value']) : '';
// Any other modification or cleanup required can go here
$values = ['name' => $name, 'value' => $value];
return $this->tag->newInstance($values);
return $this->tag->newInstance(['name' => $name, 'value' => $value]);
}
}

View File

@@ -3,6 +3,7 @@
use BookStack\Http\Controllers\Api\ApiController;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
@@ -117,6 +118,7 @@ class ApiDocsGenerator
'method' => $route->methods[0],
'controller' => $controller,
'controller_method' => $controllerMethod,
'controller_method_kebab' => Str::kebab($controllerMethod),
'base_model' => $baseModelName,
];
});

View File

@@ -36,8 +36,10 @@ class ListingResponseBuilder
*/
public function toResponse()
{
$data = $this->fetchData();
$total = $this->query->count();
$filteredQuery = $this->filterQuery($this->query);
$total = $filteredQuery->count();
$data = $this->fetchData($filteredQuery);
return response()->json([
'data' => $data,
@@ -48,23 +50,22 @@ class ListingResponseBuilder
/**
* Fetch the data to return in the response.
*/
protected function fetchData(): Collection
protected function fetchData(Builder $query): Collection
{
$this->applyCountAndOffset($this->query);
$this->applySorting($this->query);
$this->applyFiltering($this->query);
return $this->query->get($this->fields);
$query = $this->countAndOffsetQuery($query);
$query = $this->sortQuery($query);
return $query->get($this->fields);
}
/**
* Apply any filtering operations found in the request.
*/
protected function applyFiltering(Builder $query)
protected function filterQuery(Builder $query): Builder
{
$query = clone $query;
$requestFilters = $this->request->get('filter', []);
if (!is_array($requestFilters)) {
return;
return $query;
}
$queryFilters = collect($requestFilters)->map(function ($value, $key) {
@@ -73,7 +74,7 @@ class ListingResponseBuilder
return !is_null($value);
})->values()->toArray();
$query->where($queryFilters);
return $query->where($queryFilters);
}
/**
@@ -101,8 +102,9 @@ class ListingResponseBuilder
* Apply sorting operations to the query from given parameters
* otherwise falling back to the first given field, ascending.
*/
protected function applySorting(Builder $query)
protected function sortQuery(Builder $query): Builder
{
$query = clone $query;
$defaultSortName = $this->fields[0];
$direction = 'asc';
@@ -116,20 +118,21 @@ class ListingResponseBuilder
$sortName = $defaultSortName;
}
$query->orderBy($sortName, $direction);
return $query->orderBy($sortName, $direction);
}
/**
* Apply count and offset for paging, based on params from the request while falling
* back to system defined default, taking the max limit into account.
*/
protected function applyCountAndOffset(Builder $query)
protected function countAndOffsetQuery(Builder $query): Builder
{
$query = clone $query;
$offset = max(0, $this->request->get('offset', 0));
$maxCount = config('api.max_item_count');
$count = $this->request->get('count', config('api.default_item_count'));
$count = max(min($maxCount, $count), 1);
$query->skip($offset)->take($count);
return $query->skip($offset)->take($count);
}
}

View File

@@ -3,6 +3,8 @@
use BookStack\Auth\Role;
use BookStack\Auth\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
class ExternalAuthService
{
@@ -39,22 +41,14 @@ class ExternalAuthService
/**
* Match an array of group names to BookStack system roles.
* Formats group names to be lower-case and hyphenated.
* @param array $groupNames
* @return \Illuminate\Support\Collection
*/
protected function matchGroupsToSystemsRoles(array $groupNames)
protected function matchGroupsToSystemsRoles(array $groupNames): Collection
{
foreach ($groupNames as $i => $groupName) {
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
}
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
$query->whereIn('name', $groupNames);
foreach ($groupNames as $groupName) {
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
}
})->get();
$roles = Role::query()->get(['id', 'external_auth_id', 'display_name']);
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
return $this->roleMatchesGroupNames($role, $groupNames);
});

View File

@@ -60,10 +60,8 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
* @param array $credentials
* @param bool $remember
* @return bool
* @throws LoginAttemptEmailNeededException
* @throws LoginAttemptException
* @throws LdapException
* @throws UserRegistrationException
*/
public function attempt(array $credentials = [], $remember = false)
{
@@ -82,7 +80,11 @@ class LdapSessionGuard extends ExternalBaseSessionGuard
}
if (is_null($user)) {
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
try {
$user = $this->createNewFromLdapAndCreds($userDetails, $credentials);
} catch (UserRegistrationException $exception) {
throw new LoginAttemptException($exception->message);
}
}
// Sync LDAP groups if required

View File

@@ -71,15 +71,15 @@ class RegistrationService
// Start email confirmation flow if required
if ($this->emailConfirmationService->confirmationRequired() && !$emailConfirmed) {
$newUser->save();
$message = '';
try {
$this->emailConfirmationService->sendConfirmation($newUser);
session()->flash('sent-email-confirmation', true);
} catch (Exception $e) {
$message = trans('auth.email_confirm_send_error');
throw new UserRegistrationException($message, '/register/confirm');
}
throw new UserRegistrationException($message, '/register/confirm');
}
return $newUser;
@@ -106,13 +106,4 @@ class RegistrationService
}
}
/**
* Alias to the UserRepo method of the same name.
* Attaches the default system role, if configured, to the given user.
*/
public function attachDefaultRole(User $user): void
{
$this->userRepo->attachDefaultRole($user);
}
}

View File

@@ -311,7 +311,6 @@ class Saml2Service extends ExternalAuthService
/**
* Get the user from the database for the specified details.
* @throws SamlException
* @throws UserRegistrationException
*/
protected function getOrRegisterUser(array $userDetails): ?User

View File

@@ -233,6 +233,9 @@ class SocialAuthService
if ($driverName === 'google' && config('services.google.select_account')) {
$driver->with(['prompt' => 'select_account']);
}
if ($driverName === 'azure') {
$driver->with(['resource' => 'https://graph.windows.net']);
}
return $driver;
}

View File

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

View File

@@ -1,8 +1,9 @@
<?php namespace BookStack\Auth\Permissions;
use BookStack\Auth\Permissions;
use BookStack\Auth\Role;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
class PermissionsRepo
@@ -16,11 +17,8 @@ class PermissionsRepo
/**
* PermissionsRepo constructor.
* @param RolePermission $permission
* @param Role $role
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
*/
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
{
$this->permission = $permission;
$this->role = $role;
@@ -29,46 +27,34 @@ class PermissionsRepo
/**
* Get all the user roles from the system.
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getAllRoles()
public function getAllRoles(): Collection
{
return $this->role->all();
}
/**
* Get all the roles except for the provided one.
* @param Role $role
* @return mixed
*/
public function getAllRolesExcept(Role $role)
public function getAllRolesExcept(Role $role): Collection
{
return $this->role->where('id', '!=', $role->id)->get();
}
/**
* Get a role via its ID.
* @param $id
* @return mixed
*/
public function getRoleById($id)
public function getRoleById($id): Role
{
return $this->role->findOrFail($id);
return $this->role->newQuery()->findOrFail($id);
}
/**
* Save a new role into the system.
* @param array $roleData
* @return Role
*/
public function saveNewRole($roleData)
public function saveNewRole(array $roleData): Role
{
$role = $this->role->newInstance($roleData);
$role->name = str_replace(' ', '-', strtolower($roleData['display_name']));
// Prevent duplicate names
while ($this->role->where('name', '=', $role->name)->count() > 0) {
$role->name .= strtolower(Str::random(2));
}
$role->save();
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
@@ -80,13 +66,11 @@ class PermissionsRepo
/**
* Updates an existing role.
* Ensure Admin role always have core permissions.
* @param $roleId
* @param $roleData
* @throws PermissionsException
*/
public function updateRole($roleId, $roleData)
public function updateRole($roleId, array $roleData)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
if ($role->system_name === 'admin') {
@@ -108,16 +92,19 @@ class PermissionsRepo
/**
* Assign an list of permission names to an role.
* @param Role $role
* @param array $permissionNameArray
*/
public function assignRolePermissions(Role $role, $permissionNameArray = [])
public function assignRolePermissions(Role $role, array $permissionNameArray = [])
{
$permissions = [];
$permissionNameArray = array_values($permissionNameArray);
if ($permissionNameArray && count($permissionNameArray) > 0) {
$permissions = $this->permission->whereIn('name', $permissionNameArray)->pluck('id')->toArray();
if ($permissionNameArray) {
$permissions = $this->permission->newQuery()
->whereIn('name', $permissionNameArray)
->pluck('id')
->toArray();
}
$role->permissions()->sync($permissions);
}
@@ -126,13 +113,13 @@ class PermissionsRepo
* Check it's not an admin role or set as default before deleting.
* If an migration Role ID is specified the users assign to the current role
* will be added to the role of the specified id.
* @param $roleId
* @param $migrateRoleId
* @throws PermissionsException
* @throws Exception
*/
public function deleteRole($roleId, $migrateRoleId)
{
$role = $this->role->findOrFail($roleId);
/** @var Role $role */
$role = $this->role->newQuery()->findOrFail($roleId);
// Prevent deleting admin role or default registration role.
if ($role->system_name && in_array($role->system_name, $this->systemRoles)) {
@@ -142,9 +129,9 @@ class PermissionsRepo
}
if ($migrateRoleId) {
$newRole = $this->role->find($migrateRoleId);
$newRole = $this->role->newQuery()->find($migrateRoleId);
if ($newRole) {
$users = $role->users->pluck('id')->toArray();
$users = $role->users()->pluck('id')->toArray();
$newRole->users()->sync($users);
}
}

View File

@@ -3,6 +3,9 @@
use BookStack\Auth\Role;
use BookStack\Model;
/**
* @property int $id
*/
class RolePermission extends Model
{
/**

View File

@@ -3,13 +3,16 @@
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Model;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class Role
* @property int $id
* @property string $display_name
* @property string $description
* @property string $external_auth_id
* @package BookStack\Auth
* @property string $system_name
*/
class Role extends Model
{
@@ -26,9 +29,8 @@ class Role extends Model
/**
* Get all related JointPermissions.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function jointPermissions()
public function jointPermissions(): HasMany
{
return $this->hasMany(JointPermission::class);
}
@@ -43,10 +45,8 @@ class Role extends Model
/**
* Check if this role has a permission.
* @param $permissionName
* @return bool
*/
public function hasPermission($permissionName)
public function hasPermission(string $permissionName): bool
{
$permissions = $this->getRelationValue('permissions');
foreach ($permissions as $permission) {
@@ -59,7 +59,6 @@ class Role extends Model
/**
* Add a permission to this role.
* @param RolePermission $permission
*/
public function attachPermission(RolePermission $permission)
{
@@ -68,7 +67,6 @@ class Role extends Model
/**
* Detach a single permission from this role.
* @param RolePermission $permission
*/
public function detachPermission(RolePermission $permission)
{
@@ -76,39 +74,33 @@ class Role extends Model
}
/**
* Get the role object for the specified role.
* @param $roleName
* @return Role
* Get the role of the specified display name.
*/
public static function getRole($roleName)
public static function getRole(string $displayName): ?Role
{
return static::query()->where('name', '=', $roleName)->first();
return static::query()->where('display_name', '=', $displayName)->first();
}
/**
* Get the role object for the specified system role.
* @param $roleName
* @return Role
*/
public static function getSystemRole($roleName)
public static function getSystemRole(string $systemName): ?Role
{
return static::query()->where('system_name', '=', $roleName)->first();
return static::query()->where('system_name', '=', $systemName)->first();
}
/**
* Get all visible roles
* @return mixed
*/
public static function visible()
public static function visible(): Collection
{
return static::query()->where('hidden', '=', false)->orderBy('name')->get();
}
/**
* Get the roles that can be restricted.
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection
*/
public static function restrictable()
public static function restrictable(): Collection
{
return static::query()->where('system_name', '!=', 'admin')->get();
}

View File

@@ -47,7 +47,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
* The attributes excluded from the model's JSON form.
* @var array
*/
protected $hidden = ['password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email'];
protected $hidden = [
'password', 'remember_token', 'system_name', 'email_confirmed', 'external_auth_id', 'email',
'created_at', 'updated_at', 'image_id',
];
/**
* This holds the user's permissions when loaded.
@@ -98,12 +101,10 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Check if the user has a role.
* @param $role
* @return mixed
*/
public function hasRole($role)
public function hasRole($roleId): bool
{
return $this->roles->pluck('name')->contains($role);
return $this->roles->pluck('id')->contains($roleId);
}
/**
@@ -160,7 +161,6 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
/**
* Attach a role to this user.
* @param Role $role
*/
public function attachRole(Role $role)
{

View File

@@ -238,7 +238,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->newQuery()->orderBy('name', 'asc')->get();
return $this->role->newQuery()->orderBy('display_name', 'asc')->get();
}
/**

View File

@@ -52,7 +52,7 @@ return [
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'hu', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'ko', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW', 'tr'],
'locales' => ['en', 'ar', 'bg', 'cs', 'da', 'de', 'de_informal', 'es', 'es_AR', 'fa', 'fr', 'he', 'hu', 'it', 'ja', 'ko', 'nl', 'pt', 'pt_BR', 'sk', 'sl', 'sv', 'pl', 'ru', 'th', 'tr', 'uk', 'vi', 'zh_CN', 'zh_TW',],
// Application Fallback Locale
'fallback_locale' => 'en',

View File

@@ -1,5 +1,7 @@
<?php
use Monolog\Formatter\LineFormatter;
use Monolog\Handler\ErrorLogHandler;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
@@ -73,10 +75,38 @@ return [
'level' => 'debug',
],
// Custom errorlog implementation that logs out a plain,
// non-formatted message intended for the webserver log.
'errorlog_plain_webserver' => [
'driver' => 'monolog',
'level' => 'debug',
'handler' => ErrorLogHandler::class,
'handler_with' => [4],
'formatter' => LineFormatter::class,
'formatter_with' => [
'format' => "%message%",
],
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
// Testing channel
// Uses a shared testing instance during tests
// so that logs can be checked against.
'testing' => [
'driver' => 'testing',
],
],
// Failed Login Message
// Allows a configurable message to be logged when a login request fails.
'failed_login' => [
'message' => env('LOG_FAILED_LOGIN_MESSAGE', null),
'channel' => env('LOG_FAILED_LOGIN_CHANNEL', 'errorlog_plain_webserver'),
],
];

View File

@@ -101,7 +101,7 @@ return [
'url' => env('SAML2_IDP_SLO', null),
// URL location of the IdP where the SP will send the SLO Response (ResponseLocation)
// if not set, url for the SLO Request will be used
'responseUrl' => '',
'responseUrl' => null,
// SAML protocol binding to be used when returning the <Response>
// message. Onelogin Toolkit supports for this endpoint the
// HTTP-Redirect binding only

View File

@@ -13,7 +13,9 @@ return [
'enabled' => true,
'binary' => file_exists(base_path('wkhtmltopdf')) ? base_path('wkhtmltopdf') : env('WKHTMLTOPDF', false),
'timeout' => false,
'options' => [],
'options' => [
'outline' => true
],
'env' => [],
],
'image' => [

View File

@@ -18,7 +18,7 @@ class ClearViews extends Command
*
* @var string
*/
protected $description = 'Clear all view-counts for all entities.';
protected $description = 'Clear all view-counts for all entities';
/**
* Create a new command instance.

View File

@@ -23,7 +23,7 @@ class CopyShelfPermissions extends Command
*
* @var string
*/
protected $description = 'Copy shelf permissions to all child books.';
protected $description = 'Copy shelf permissions to all child books';
/**
* @var BookshelfRepo

View File

@@ -25,7 +25,7 @@ class DeleteUsers extends Command
*
* @var string
*/
protected $description = 'Delete users that are not "admin" or system users.';
protected $description = 'Delete users that are not "admin" or system users';
public function __construct(User $user, UserRepo $userRepo)
{

View File

@@ -0,0 +1,61 @@
<?php
namespace BookStack\Console\Commands;
use BookStack\Actions\Comment;
use BookStack\Actions\CommentRepo;
use Illuminate\Console\Command;
class RegenerateCommentContent extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:regenerate-comment-content {--database= : The database connection to use.}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Regenerate the stored HTML of all comments';
/**
* @var CommentRepo
*/
protected $commentRepo;
/**
* Create a new command instance.
*/
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
}
Comment::query()->chunk(100, function ($comments) {
foreach ($comments as $comment) {
$comment->html = $this->commentRepo->commentToHtml($comment->text);
$comment->save();
}
});
\DB::setDefaultConnection($connection);
$this->comment('Comment HTML content has been regenerated');
}
}

View File

@@ -30,8 +30,6 @@ class RegeneratePermissions extends Command
/**
* Create a new command instance.
*
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
*/
public function __construct(PermissionService $permissionService)
{

View File

@@ -3,6 +3,7 @@
namespace BookStack\Console\Commands;
use BookStack\Entities\SearchService;
use DB;
use Illuminate\Console\Command;
class RegenerateSearch extends Command
@@ -26,7 +27,7 @@ class RegenerateSearch extends Command
/**
* Create a new command instance.
*
* @param \BookStack\Entities\SearchService $searchService
* @param SearchService $searchService
*/
public function __construct(SearchService $searchService)
{
@@ -41,14 +42,14 @@ class RegenerateSearch extends Command
*/
public function handle()
{
$connection = \DB::getDefaultConnection();
$connection = DB::getDefaultConnection();
if ($this->option('database') !== null) {
\DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(\DB::connection($this->option('database')));
DB::setDefaultConnection($this->option('database'));
$this->searchService->setConnection(DB::connection($this->option('database')));
}
$this->searchService->indexAllEntities();
\DB::setDefaultConnection($connection);
DB::setDefaultConnection($connection);
$this->comment('Search index regenerated');
}
}

View File

@@ -0,0 +1,91 @@
<?php
namespace BookStack\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Database\Connection;
class UpdateUrl extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'bookstack:update-url
{oldUrl : URL to replace}
{newUrl : URL to use as the replacement}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Find and replace the given URLs in your BookStack database';
protected $db;
/**
* Create a new command instance.
*
* @return void
*/
public function __construct(Connection $db)
{
$this->db = $db;
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$oldUrl = str_replace("'", '', $this->argument('oldUrl'));
$newUrl = str_replace("'", '', $this->argument('newUrl'));
$urlPattern = '/https?:\/\/(.+)/';
if (!preg_match($urlPattern, $oldUrl) || !preg_match($urlPattern, $newUrl)) {
$this->error("The given urls are expected to be full urls starting with http:// or https://");
return 1;
}
if (!$this->checkUserOkayToProceed($oldUrl, $newUrl)) {
return 1;
}
$columnsToUpdateByTable = [
"attachments" => ["path"],
"pages" => ["html", "text", "markdown"],
"images" => ["url"],
"comments" => ["html", "text"],
];
foreach ($columnsToUpdateByTable as $table => $columns) {
foreach ($columns as $column) {
$changeCount = $this->db->table($table)->update([
$column => $this->db->raw("REPLACE({$column}, '{$oldUrl}', '{$newUrl}')")
]);
$this->info("Updated {$changeCount} rows in {$table}->{$column}");
}
}
$this->info("URL update procedure complete.");
return 0;
}
/**
* Warn the user of the dangers of this operation.
* Returns a boolean indicating if they've accepted the warnings.
*/
protected function checkUserOkayToProceed(string $oldUrl, string $newUrl): bool
{
$dangerWarning = "This will search for \"{$oldUrl}\" in your database and replace it with \"{$newUrl}\".\n";
$dangerWarning .= "Are you sure you want to proceed?";
$backupConfirmation = "This operation could cause issues if used incorrectly. Have you made a backup of your existing database?";
return $this->confirm($dangerWarning) && $this->confirm($backupConfirmation);
}
}

View File

@@ -19,7 +19,7 @@ class Book extends Entity implements HasCoverImage
public $searchFactor = 2;
protected $fillable = ['name', 'description'];
protected $hidden = ['restricted'];
protected $hidden = ['restricted', 'pivot', 'image_id'];
/**
* Get the url for this book.

View File

@@ -12,6 +12,8 @@ class Bookshelf extends Entity implements HasCoverImage
protected $fillable = ['name', 'description', 'image_id'];
protected $hidden = ['restricted', 'image_id'];
/**
* Get the books in this shelf.
* Should not be used directly since does not take into account permissions.

View File

@@ -12,6 +12,7 @@ class Chapter extends BookChild
public $searchFactor = 1.3;
protected $fillable = ['name', 'description', 'priority', 'book_id'];
protected $hidden = ['restricted', 'pivot'];
/**
* Get the pages that this chapter contains.

View File

@@ -238,10 +238,8 @@ class Entity extends Ownable
/**
* Gets a limited-length version of the entities name.
* @param int $length
* @return string
*/
public function getShortName($length = 25)
public function getShortName(int $length = 25): string
{
if (mb_strlen($this->name) <= $length) {
return $this->name;
@@ -288,7 +286,7 @@ class Entity extends Ownable
public function rebuildPermissions()
{
/** @noinspection PhpUnhandledExceptionInspection */
Permissions::buildJointPermissionsForEntity($this);
Permissions::buildJointPermissionsForEntity(clone $this);
}
/**
@@ -297,7 +295,7 @@ class Entity extends Ownable
public function indexForSearch()
{
$searchService = app()->make(SearchService::class);
$searchService->indexEntity($this);
$searchService->indexEntity(clone $this);
}
/**

View File

@@ -108,7 +108,7 @@ class PageContent
protected function toPlainText(): string
{
$html = $this->render(true);
return strip_tags($html);
return html_entity_decode(strip_tags($html));
}
/**

View File

@@ -21,12 +21,14 @@ use Permissions;
*/
class Page extends BookChild
{
protected $fillable = ['name', 'html', 'priority', 'markdown'];
protected $fillable = ['name', 'priority', 'markdown'];
protected $simpleAttributes = ['name', 'id', 'slug'];
public $textField = 'text';
protected $hidden = ['html', 'markdown', 'text', 'restricted', 'pivot'];
/**
* Get the entities that are visible to the current user.
*/

View File

@@ -28,8 +28,10 @@ class BookshelfRepo
*/
public function getAllPaginated(int $count = 20, string $sort = 'name', string $order = 'asc'): LengthAwarePaginator
{
return Bookshelf::visible()->with('visibleBooks')
->orderBy($sort, $order)->paginate($count);
return Bookshelf::visible()
->with('visibleBooks')
->orderBy($sort, $order)
->paginate($count);
}
/**
@@ -91,10 +93,14 @@ class BookshelfRepo
/**
* Create a new shelf in the system.
*/
public function update(Bookshelf $shelf, array $input, array $bookIds): Bookshelf
public function update(Bookshelf $shelf, array $input, ?array $bookIds): Bookshelf
{
$this->baseRepo->update($shelf, $input);
$this->updateBooks($shelf, $bookIds);
if (!is_null($bookIds)) {
$this->updateBooks($shelf, $bookIds);
}
return $shelf;
}

View File

@@ -180,12 +180,11 @@ class PageRepo
$page->template = ($input['template'] === 'true');
}
$pageContent = new PageContent($page);
$pageContent->setNewHTML($input['html']);
$this->baseRepo->update($page, $input);
// Update with new details
$page->fill($input);
$pageContent = new PageContent($page);
$pageContent->setNewHTML($input['html']);
$page->revision_count++;
if (setting('app-editor') !== 'markdown') {
@@ -211,7 +210,7 @@ class PageRepo
*/
protected function savePageRevision(Page $page, string $summary = null)
{
$revision = new PageRevision($page->toArray());
$revision = new PageRevision($page->getAttributes());
if (setting('app-editor') !== 'markdown') {
$revision->markdown = '';
@@ -279,7 +278,7 @@ class PageRepo
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
$page->fill($revision->toArray());
$content = new PageContent($page);
$content->setNewHTML($page->html);
$content->setNewHTML($revision->html);
$page->updated_by = user()->id;
$page->refreshSlug();
$page->save();

View File

@@ -0,0 +1,141 @@
<?php namespace BookStack\Entities;
use Illuminate\Http\Request;
class SearchOptions
{
/**
* @var array
*/
public $searches = [];
/**
* @var array
*/
public $exacts = [];
/**
* @var array
*/
public $tags = [];
/**
* @var array
*/
public $filters = [];
/**
* Create a new instance from a search string.
*/
public static function fromString(string $search): SearchOptions
{
$decoded = static::decode($search);
$instance = new static();
foreach ($decoded as $type => $value) {
$instance->$type = $value;
}
return $instance;
}
/**
* Create a new instance from a request.
* Will look for a classic string term and use that
* Otherwise we'll use the details from an advanced search form.
*/
public static function fromRequest(Request $request): SearchOptions
{
if (!$request->has('search') && !$request->has('term')) {
return static::fromString('');
}
if ($request->has('term')) {
return static::fromString($request->get('term'));
}
$instance = new static();
$inputs = $request->only(['search', 'types', 'filters', 'exact', 'tags']);
$instance->searches = explode(' ', $inputs['search'] ?? []);
$instance->exacts = array_filter($inputs['exact'] ?? []);
$instance->tags = array_filter($inputs['tags'] ?? []);
foreach (($inputs['filters'] ?? []) as $filterKey => $filterVal) {
if (empty($filterVal)) {
continue;
}
$instance->filters[$filterKey] = $filterVal === 'true' ? '' : $filterVal;
}
if (isset($inputs['types']) && count($inputs['types']) < 4) {
$instance->filters['type'] = implode('|', $inputs['types']);
}
return $instance;
}
/**
* Decode a search string into an array of terms.
*/
protected static function decode(string $searchString): array
{
$terms = [
'searches' => [],
'exacts' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exacts' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') {
$terms['searches'][] = $searchTerm;
}
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Encode this instance to a search string.
*/
public function toString(): string
{
$string = implode(' ', $this->searches ?? []);
foreach ($this->exacts as $term) {
$string .= ' "' . $term . '"';
}
foreach ($this->tags as $term) {
$string .= " [{$term}]";
}
foreach ($this->filters as $filterName => $filterVal) {
$string .= ' {' . $filterName . ($filterVal ? ':' . $filterVal : '') . '}';
}
return $string;
}
}

View File

@@ -39,10 +39,6 @@ class SearchService
/**
* SearchService constructor.
* @param SearchTerm $searchTerm
* @param EntityProvider $entityProvider
* @param Connection $db
* @param PermissionService $permissionService
*/
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
{
@@ -54,7 +50,6 @@ class SearchService
/**
* Set the database connection
* @param Connection $connection
*/
public function setConnection(Connection $connection)
{
@@ -63,23 +58,18 @@ class SearchService
/**
* Search all entities in the system.
* @param string $searchString
* @param string $entityType
* @param int $page
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
* @param string $action
* @return array[int, Collection];
* The provided count is for each entity to search,
* Total returned could can be larger and not guaranteed.
*/
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
public function searchEntities(SearchOptions $searchOpts, string $entityType = 'all', int $page = 1, int $count = 20, string $action = 'view'): array
{
$terms = $this->parseSearchString($searchString);
$entityTypes = array_keys($this->entityProvider->all());
$entityTypesToSearch = $entityTypes;
if ($entityType !== 'all') {
$entityTypesToSearch = $entityType;
} else if (isset($terms['filters']['type'])) {
$entityTypesToSearch = explode('|', $terms['filters']['type']);
} else if (isset($searchOpts->filters['type'])) {
$entityTypesToSearch = explode('|', $searchOpts->filters['type']);
}
$results = collect();
@@ -90,8 +80,8 @@ class SearchService
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->searchEntityTable($terms, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($terms, $entityType, $page, $count, $action, true);
$search = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action);
$entityTotal = $this->searchEntityTable($searchOpts, $entityType, $page, $count, $action, true);
if ($entityTotal > $page * $count) {
$hasMore = true;
}
@@ -103,29 +93,26 @@ class SearchService
'total' => $total,
'count' => count($results),
'has_more' => $hasMore,
'results' => $results->sortByDesc('score')->values()
'results' => $results->sortByDesc('score')->values(),
];
}
/**
* Search a book for entities
* @param integer $bookId
* @param string $searchString
* @return Collection
*/
public function searchBook($bookId, $searchString)
public function searchBook(int $bookId, string $searchString): Collection
{
$terms = $this->parseSearchString($searchString);
$opts = SearchOptions::fromString($searchString);
$entityTypes = ['page', 'chapter'];
$entityTypesToSearch = isset($terms['filters']['type']) ? explode('|', $terms['filters']['type']) : $entityTypes;
$entityTypesToSearch = isset($opts->filters['type']) ? explode('|', $opts->filters['type']) : $entityTypes;
$results = collect();
foreach ($entityTypesToSearch as $entityType) {
if (!in_array($entityType, $entityTypes)) {
continue;
}
$search = $this->buildEntitySearchQuery($terms, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$search = $this->buildEntitySearchQuery($opts, $entityType)->where('book_id', '=', $bookId)->take(20)->get();
$results = $results->merge($search);
}
return $results->sortByDesc('score')->take(20);
@@ -133,30 +120,23 @@ class SearchService
/**
* Search a book for entities
* @param integer $chapterId
* @param string $searchString
* @return Collection
*/
public function searchChapter($chapterId, $searchString)
public function searchChapter(int $chapterId, string $searchString): Collection
{
$terms = $this->parseSearchString($searchString);
$pages = $this->buildEntitySearchQuery($terms, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
$opts = SearchOptions::fromString($searchString);
$pages = $this->buildEntitySearchQuery($opts, 'page')->where('chapter_id', '=', $chapterId)->take(20)->get();
return $pages->sortByDesc('score');
}
/**
* Search across a particular entity type.
* @param array $terms
* @param string $entityType
* @param int $page
* @param int $count
* @param string $action
* @param bool $getCount Return the total count of the search
* Setting getCount = true will return the total
* matching instead of the items themselves.
* @return \Illuminate\Database\Eloquent\Collection|int|static[]
*/
public function searchEntityTable($terms, $entityType = 'page', $page = 1, $count = 20, $action = 'view', $getCount = false)
public function searchEntityTable(SearchOptions $searchOpts, string $entityType = 'page', int $page = 1, int $count = 20, string $action = 'view', bool $getCount = false)
{
$query = $this->buildEntitySearchQuery($terms, $entityType, $action);
$query = $this->buildEntitySearchQuery($searchOpts, $entityType, $action);
if ($getCount) {
return $query->count();
}
@@ -167,22 +147,18 @@ class SearchService
/**
* Create a search query for an entity
* @param array $terms
* @param string $entityType
* @param string $action
* @return EloquentBuilder
*/
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
protected function buildEntitySearchQuery(SearchOptions $searchOpts, string $entityType = 'page', string $action = 'view'): EloquentBuilder
{
$entity = $this->entityProvider->get($entityType);
$entitySelect = $entity->newQuery();
// Handle normal search terms
if (count($terms['search']) > 0) {
if (count($searchOpts->searches) > 0) {
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
$subQuery->where('entity_type', '=', $entity->getMorphClass());
$subQuery->where(function (Builder $query) use ($terms) {
foreach ($terms['search'] as $inputTerm) {
$subQuery->where(function (Builder $query) use ($searchOpts) {
foreach ($searchOpts->searches as $inputTerm) {
$query->orWhere('term', 'like', $inputTerm .'%');
}
})->groupBy('entity_type', 'entity_id');
@@ -193,9 +169,9 @@ class SearchService
}
// Handle exact term matching
if (count($terms['exact']) > 0) {
$entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
foreach ($terms['exact'] as $inputTerm) {
if (count($searchOpts->exacts) > 0) {
$entitySelect->where(function (EloquentBuilder $query) use ($searchOpts, $entity) {
foreach ($searchOpts->exacts as $inputTerm) {
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
$query->where('name', 'like', '%'.$inputTerm .'%')
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
@@ -205,12 +181,12 @@ class SearchService
}
// Handle tag searches
foreach ($terms['tags'] as $inputTerm) {
foreach ($searchOpts->tags as $inputTerm) {
$this->applyTagSearch($entitySelect, $inputTerm);
}
// Handle filters
foreach ($terms['filters'] as $filterTerm => $filterValue) {
foreach ($searchOpts->filters as $filterTerm => $filterValue) {
$functionName = Str::camel('filter_' . $filterTerm);
if (method_exists($this, $functionName)) {
$this->$functionName($entitySelect, $entity, $filterValue);
@@ -220,60 +196,10 @@ class SearchService
return $this->permissionService->enforceEntityRestrictions($entityType, $entitySelect, $action);
}
/**
* Parse a search string into components.
* @param $searchString
* @return array
*/
protected function parseSearchString($searchString)
{
$terms = [
'search' => [],
'exact' => [],
'tags' => [],
'filters' => []
];
$patterns = [
'exact' => '/"(.*?)"/',
'tags' => '/\[(.*?)\]/',
'filters' => '/\{(.*?)\}/'
];
// Parse special terms
foreach ($patterns as $termType => $pattern) {
$matches = [];
preg_match_all($pattern, $searchString, $matches);
if (count($matches) > 0) {
$terms[$termType] = $matches[1];
$searchString = preg_replace($pattern, '', $searchString);
}
}
// Parse standard terms
foreach (explode(' ', trim($searchString)) as $searchTerm) {
if ($searchTerm !== '') {
$terms['search'][] = $searchTerm;
}
}
// Split filter values out
$splitFilters = [];
foreach ($terms['filters'] as $filter) {
$explodedFilter = explode(':', $filter, 2);
$splitFilters[$explodedFilter[0]] = (count($explodedFilter) > 1) ? $explodedFilter[1] : '';
}
$terms['filters'] = $splitFilters;
return $terms;
}
/**
* Get the available query operators as a regex escaped list.
* @return mixed
*/
protected function getRegexEscapedOperators()
protected function getRegexEscapedOperators(): string
{
$escapedOperators = [];
foreach ($this->queryOperators as $operator) {
@@ -284,11 +210,8 @@ class SearchService
/**
* Apply a tag search term onto a entity query.
* @param EloquentBuilder $query
* @param string $tagTerm
* @return mixed
*/
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
protected function applyTagSearch(EloquentBuilder $query, string $tagTerm): EloquentBuilder
{
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
@@ -318,7 +241,6 @@ class SearchService
/**
* Index the given entity.
* @param Entity $entity
*/
public function indexEntity(Entity $entity)
{

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Entities;
use Illuminate\Support\Str;
class SlugGenerator
{
@@ -32,9 +34,7 @@ class SlugGenerator
*/
protected function formatNameAsSlug(string $name): string
{
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
$slug = preg_replace('/\s{2,}/', ' ', $slug);
$slug = str_replace(' ', '-', $slug);
$slug = Str::slug($name);
if ($slug === "") {
$slug = substr(md5(rand(1, 500)), 0, 5);
}

View File

@@ -9,7 +9,6 @@ use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -26,6 +25,7 @@ class Handler extends ExceptionHandler
HttpException::class,
ModelNotFoundException::class,
ValidationException::class,
NotFoundException::class,
];
/**

View File

@@ -8,8 +8,6 @@ class NotifyException extends \Exception
/**
* NotifyException constructor.
* @param string $message
* @param string $redirectLocation
*/
public function __construct(string $message, string $redirectLocation = "/")
{

View File

@@ -8,7 +8,7 @@ use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BooksApiController extends ApiController
class BookApiController extends ApiController
{
protected $bookRepo;
@@ -17,10 +17,12 @@ class BooksApiController extends ApiController
'create' => [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
'update' => [
'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
];

View File

@@ -0,0 +1,54 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Book;
use BookStack\Entities\ExportService;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
class BookExportApiController extends ApiController
{
protected $bookRepo;
protected $exportService;
/**
* BookExportController constructor.
*/
public function __construct(BookRepo $bookRepo, ExportService $exportService)
{
$this->bookRepo = $bookRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Export a book as a PDF file.
* @throws Throwable
*/
public function exportPdf(int $id)
{
$book = Book::visible()->findOrFail($id);
$pdfContent = $this->exportService->bookToPdf($book);
return $this->downloadResponse($pdfContent, $book->slug . '.pdf');
}
/**
* Export a book as a contained HTML file.
* @throws Throwable
*/
public function exportHtml(int $id)
{
$book = Book::visible()->findOrFail($id);
$htmlContent = $this->exportService->bookToContainedHtml($book);
return $this->downloadResponse($htmlContent, $book->slug . '.html');
}
/**
* Export a book as a plain text file.
*/
public function exportPlainText(int $id)
{
$book = Book::visible()->findOrFail($id);
$textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $book->slug . '.txt');
}
}

View File

@@ -0,0 +1,122 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Facades\Activity;
use BookStack\Entities\Repos\BookshelfRepo;
use BookStack\Entities\Bookshelf;
use Exception;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class BookshelfApiController extends ApiController
{
/**
* @var BookshelfRepo
*/
protected $bookshelfRepo;
protected $rules = [
'create' => [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'books' => 'array',
],
'update' => [
'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
'books' => 'array',
],
];
/**
* BookshelfApiController constructor.
* @param BookshelfRepo $bookshelfRepo
*/
public function __construct(BookshelfRepo $bookshelfRepo)
{
$this->bookshelfRepo = $bookshelfRepo;
}
/**
* Get a listing of shelves visible to the user.
*/
public function list()
{
$shelves = Bookshelf::visible();
return $this->apiListingResponse($shelves, [
'id', 'name', 'slug', 'description', 'created_at', 'updated_at', 'created_by', 'updated_by', 'image_id',
]);
}
/**
* Create a new shelf in the system.
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided.
* @throws ValidationException
*/
public function create(Request $request)
{
$this->checkPermission('bookshelf-create-all');
$requestData = $this->validate($request, $this->rules['create']);
$bookIds = $request->get('books', []);
$shelf = $this->bookshelfRepo->create($requestData, $bookIds);
Activity::add($shelf, 'bookshelf_create', $shelf->id);
return response()->json($shelf);
}
/**
* View the details of a single shelf.
*/
public function read(string $id)
{
$shelf = Bookshelf::visible()->with([
'tags', 'cover', 'createdBy', 'updatedBy',
'books' => function (BelongsToMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}
])->findOrFail($id);
return response()->json($shelf);
}
/**
* Update the details of a single shelf.
* An array of books IDs can be provided in the request. These
* will be added to the shelf in the same order as provided and overwrite
* any existing book assignments.
* @throws ValidationException
*/
public function update(Request $request, string $id)
{
$shelf = Bookshelf::visible()->findOrFail($id);
$this->checkOwnablePermission('bookshelf-update', $shelf);
$requestData = $this->validate($request, $this->rules['update']);
$bookIds = $request->get('books', null);
$shelf = $this->bookshelfRepo->update($shelf, $requestData, $bookIds);
Activity::add($shelf, 'bookshelf_update', $shelf->id);
return response()->json($shelf);
}
/**
* Delete a single shelf from the system.
* @throws Exception
*/
public function delete(string $id)
{
$shelf = Bookshelf::visible()->findOrFail($id);
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->bookshelfRepo->destroy($shelf);
Activity::addMessage('bookshelf_delete', $shelf->name);
return response('', 204);
}
}

View File

@@ -0,0 +1,104 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Book;
use BookStack\Entities\Chapter;
use BookStack\Entities\Repos\ChapterRepo;
use BookStack\Facades\Activity;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Http\Request;
class ChapterApiController extends ApiController
{
protected $chapterRepo;
protected $rules = [
'create' => [
'book_id' => 'required|integer',
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
'update' => [
'book_id' => 'integer',
'name' => 'string|min:1|max:255',
'description' => 'string|max:1000',
'tags' => 'array',
],
];
/**
* ChapterController constructor.
*/
public function __construct(ChapterRepo $chapterRepo)
{
$this->chapterRepo = $chapterRepo;
}
/**
* Get a listing of chapters visible to the user.
*/
public function list()
{
$chapters = Chapter::visible();
return $this->apiListingResponse($chapters, [
'id', 'book_id', 'name', 'slug', 'description', 'priority',
'created_at', 'updated_at', 'created_by', 'updated_by',
]);
}
/**
* Create a new chapter in the system.
*/
public function create(Request $request)
{
$this->validate($request, $this->rules['create']);
$bookId = $request->get('book_id');
$book = Book::visible()->findOrFail($bookId);
$this->checkOwnablePermission('chapter-create', $book);
$chapter = $this->chapterRepo->create($request->all(), $book);
Activity::add($chapter, 'chapter_create', $book->id);
return response()->json($chapter->load(['tags']));
}
/**
* View the details of a single chapter.
*/
public function read(string $id)
{
$chapter = Chapter::visible()->with(['tags', 'createdBy', 'updatedBy', 'pages' => function (HasMany $query) {
$query->visible()->get(['id', 'name', 'slug']);
}])->findOrFail($id);
return response()->json($chapter);
}
/**
* Update the details of a single chapter.
*/
public function update(Request $request, string $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-update', $chapter);
$updatedChapter = $this->chapterRepo->update($chapter, $request->all());
Activity::add($chapter, 'chapter_update', $chapter->book->id);
return response()->json($updatedChapter->load(['tags']));
}
/**
* Delete a chapter from the system.
*/
public function delete(string $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->chapterRepo->destroy($chapter);
Activity::addMessage('chapter_delete', $chapter->name, $chapter->book->id);
return response('', 204);
}
}

View File

@@ -0,0 +1,54 @@
<?php namespace BookStack\Http\Controllers\Api;
use BookStack\Entities\Chapter;
use BookStack\Entities\ExportService;
use BookStack\Entities\Repos\BookRepo;
use Throwable;
class ChapterExportApiController extends ApiController
{
protected $chapterRepo;
protected $exportService;
/**
* ChapterExportController constructor.
*/
public function __construct(BookRepo $chapterRepo, ExportService $exportService)
{
$this->chapterRepo = $chapterRepo;
$this->exportService = $exportService;
parent::__construct();
}
/**
* Export a chapter as a PDF file.
* @throws Throwable
*/
public function exportPdf(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$pdfContent = $this->exportService->chapterToPdf($chapter);
return $this->downloadResponse($pdfContent, $chapter->slug . '.pdf');
}
/**
* Export a chapter as a contained HTML file.
* @throws Throwable
*/
public function exportHtml(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$htmlContent = $this->exportService->chapterToContainedHtml($chapter);
return $this->downloadResponse($htmlContent, $chapter->slug . '.html');
}
/**
* Export a chapter as a plain text file.
*/
public function exportPlainText(int $id)
{
$chapter = Chapter::visible()->findOrFail($id);
$textContent = $this->exportService->chapterToPlainText($chapter);
return $this->downloadResponse($textContent, $chapter->slug . '.txt');
}
}

View File

@@ -8,6 +8,7 @@ use BookStack\Uploads\AttachmentService;
use Exception;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Support\MessageBag;
use Illuminate\Validation\ValidationException;
class AttachmentController extends Controller
@@ -60,25 +61,17 @@ class AttachmentController extends Controller
/**
* Update an uploaded attachment.
* @throws ValidationException
* @throws NotFoundException
*/
public function uploadUpdate(Request $request, $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'file' => 'required|file'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
}
$uploadedFile = $request->file('file');
@@ -92,57 +85,87 @@ class AttachmentController extends Controller
}
/**
* Update the details of an existing file.
* @throws ValidationException
* @throws NotFoundException
* Get the update form for an attachment.
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function update(Request $request, $attachmentId)
public function getUpdateForm(string $attachmentId)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'string|min:1|max:255'
]);
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
if (intval($pageId) !== intval($attachment->uploaded_to)) {
return $this->jsonError(trans('errors.attachment_page_mismatch'));
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Update the details of an existing file.
*/
public function update(Request $request, string $attachmentId)
{
$attachment = $this->attachment->newQuery()->findOrFail($attachmentId);
try {
$this->validate($request, [
'attachment_edit_name' => 'required|string|min:1|max:255',
'attachment_edit_url' => 'string|min:1|max:255'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-edit-form', array_merge($request->only(['attachment_edit_name', 'attachment_edit_url']), [
'attachment' => $attachment,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$attachment = $this->attachmentService->updateFile($attachment, $request->all());
return response()->json($attachment);
$this->checkOwnablePermission('view', $attachment->page);
$this->checkOwnablePermission('page-update', $attachment->page);
$this->checkOwnablePermission('attachment-create', $attachment);
$attachment = $this->attachmentService->updateFile($attachment, [
'name' => $request->get('attachment_edit_name'),
'link' => $request->get('attachment_edit_url'),
]);
return view('attachments.manager-edit-form', [
'attachment' => $attachment,
]);
}
/**
* Attach a link to a page.
* @throws ValidationException
* @throws NotFoundException
*/
public function attachLink(Request $request)
{
$this->validate($request, [
'uploaded_to' => 'required|integer|exists:pages,id',
'name' => 'required|string|min:1|max:255',
'link' => 'required|string|min:1|max:255'
]);
$pageId = $request->get('attachment_link_uploaded_to');
try {
$this->validate($request, [
'attachment_link_uploaded_to' => 'required|integer|exists:pages,id',
'attachment_link_name' => 'required|string|min:1|max:255',
'attachment_link_url' => 'required|string|min:1|max:255'
]);
} catch (ValidationException $exception) {
return response()->view('attachments.manager-link-form', array_merge($request->only(['attachment_link_name', 'attachment_link_url']), [
'pageId' => $pageId,
'errors' => new MessageBag($exception->errors()),
]), 422);
}
$pageId = $request->get('uploaded_to');
$page = $this->pageRepo->getById($pageId);
$this->checkPermission('attachment-create-all');
$this->checkOwnablePermission('page-update', $page);
$attachmentName = $request->get('name');
$link = $request->get('link');
$attachmentName = $request->get('attachment_link_name');
$link = $request->get('attachment_link_url');
$attachment = $this->attachmentService->saveNewFromLink($attachmentName, $link, $pageId);
return response()->json($attachment);
return view('attachments.manager-link-form', [
'pageId' => $pageId,
]);
}
/**
@@ -152,7 +175,9 @@ class AttachmentController extends Controller
{
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-view', $page);
return response()->json($page->attachments);
return view('attachments.manager-list', [
'attachments' => $page->attachments->all(),
]);
}
/**
@@ -163,14 +188,13 @@ class AttachmentController extends Controller
public function sortForPage(Request $request, int $pageId)
{
$this->validate($request, [
'files' => 'required|array',
'files.*.id' => 'required|integer',
'order' => 'required|array',
]);
$page = $this->pageRepo->getById($pageId);
$this->checkOwnablePermission('page-update', $page);
$attachments = $request->get('files');
$this->attachmentService->updateFileOrderWithinPage($attachments, $pageId);
$attachmentOrder = $request->get('order');
$this->attachmentService->updateFileOrderWithinPage($attachmentOrder, $pageId);
return response()->json(['message' => trans('entities.attachments_order_updated')]);
}
@@ -179,7 +203,7 @@ class AttachmentController extends Controller
* @throws FileNotFoundException
* @throws NotFoundException
*/
public function get(int $attachmentId)
public function get(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
try {
@@ -200,11 +224,9 @@ class AttachmentController extends Controller
/**
* Delete a specific attachment in the system.
* @param $attachmentId
* @return mixed
* @throws Exception
*/
public function delete(int $attachmentId)
public function delete(string $attachmentId)
{
$attachment = $this->attachment->findOrFail($attachmentId);
$this->checkOwnablePermission('attachment-delete', $attachment);

View File

@@ -0,0 +1,51 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Actions\Activity;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AuditLogController extends Controller
{
public function index(Request $request)
{
$this->checkPermission('settings-manage');
$this->checkPermission('users-manage');
$listDetails = [
'order' => $request->get('order', 'desc'),
'event' => $request->get('event', ''),
'sort' => $request->get('sort', 'created_at'),
'date_from' => $request->get('date_from', ''),
'date_to' => $request->get('date_to', ''),
];
$query = Activity::query()
->with(['entity', 'user'])
->orderBy($listDetails['sort'], $listDetails['order']);
if ($listDetails['event']) {
$query->where('key', '=', $listDetails['event']);
}
if ($listDetails['date_from']) {
$query->where('created_at', '>=', $listDetails['date_from']);
}
if ($listDetails['date_to']) {
$query->where('created_at', '<=', $listDetails['date_to']);
}
$activities = $query->paginate(100);
$activities->appends($listDetails);
$keys = DB::table('activities')->select('key')->distinct()->pluck('key');
$this->setPageTitle(trans('settings.audit'));
return view('settings.audit', [
'activities' => $activities,
'listDetails' => $listDetails,
'activityKeys' => $keys,
]);
}
}

View File

@@ -5,7 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Http\Request;
use Password;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
@@ -52,8 +52,8 @@ class ForgotPasswordController extends Controller
$request->only('email')
);
if ($response === Password::RESET_LINK_SENT) {
$message = trans('auth.reset_password_sent_success', ['email' => $request->get('email')]);
if ($response === Password::RESET_LINK_SENT || $response === Password::INVALID_USER) {
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
$this->showSuccessNotification($message);
return back()->with('status', trans($response));
}

View File

@@ -2,6 +2,7 @@
namespace BookStack\Http\Controllers\Auth;
use Activity;
use BookStack\Auth\Access\SocialAuthService;
use BookStack\Exceptions\LoginAttemptEmailNeededException;
use BookStack\Exceptions\LoginAttemptException;
@@ -76,6 +77,15 @@ class LoginController extends Controller
]);
}
// Store the previous location for redirect after login
$previous = url()->previous('');
if ($previous && $previous !== url('/login') && setting('app-public')) {
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
if ($isPreviousFromInstance) {
redirect()->setIntendedUrl($previous);
}
}
return view('auth.login', [
'socialDrivers' => $socialDrivers,
'authMethod' => $authMethod,
@@ -93,6 +103,7 @@ class LoginController extends Controller
public function login(Request $request)
{
$this->validateLogin($request);
$username = $request->get($this->username());
// If the class is using the ThrottlesLogins trait, we can automatically throttle
// the login attempts for this application. We'll key this by the username and
@@ -101,6 +112,7 @@ class LoginController extends Controller
$this->hasTooManyLoginAttempts($request)) {
$this->fireLockoutEvent($request);
Activity::logFailedLogin($username);
return $this->sendLockoutResponse($request);
}
@@ -109,6 +121,7 @@ class LoginController extends Controller
return $this->sendLoginResponse($request);
}
} catch (LoginAttemptException $exception) {
Activity::logFailedLogin($username);
return $this->sendLoginAttemptExceptionResponse($exception, $request);
}
@@ -117,9 +130,30 @@ class LoginController extends Controller
// user surpasses their maximum number of attempts they will get locked out.
$this->incrementLoginAttempts($request);
Activity::logFailedLogin($username);
return $this->sendFailedLoginResponse($request);
}
/**
* The user has been authenticated.
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
// Authenticate on all session guards if a likely admin
if ($user->can('users-manage') && $user->can('user-roles-manage')) {
$guards = ['standard', 'ldap', 'saml2'];
foreach ($guards as $guard) {
auth($guard)->login($user);
}
}
return redirect()->intended($this->redirectPath());
}
/**
* Validate the user login request.
*

View File

@@ -5,6 +5,7 @@ namespace BookStack\Http\Controllers\Auth;
use BookStack\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ResetPasswordController extends Controller
{
@@ -49,4 +50,24 @@ class ResetPasswordController extends Controller
return redirect($this->redirectPath())
->with('status', trans($response));
}
/**
* Get the response for a failed password reset.
*
* @param \Illuminate\Http\Request $request
* @param string $response
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Http\JsonResponse
*/
protected function sendResetFailedResponse(Request $request, $response)
{
// We show invalid users as invalid tokens as to not leak what
// users may exist in the system.
if ($response === Password::INVALID_USER) {
$response = Password::INVALID_TOKEN;
}
return redirect()->back()
->withInput($request->only('email'))
->withErrors(['email' => trans($response)]);
}
}

View File

@@ -123,6 +123,11 @@ class SocialController extends Controller
'password' => Str::random(32)
];
// Take name from email address if empty
if (!$userData['name']) {
$userData['name'] = explode('@', $userData['email'])[0];
}
$user = $this->registrationService->registerUser($userData, $socialAccount, $emailVerified);
auth()->login($user);

View File

@@ -86,7 +86,7 @@ class BookController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->getImageValidationRules(),
'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$bookshelf = null;
@@ -114,6 +114,7 @@ class BookController extends Controller
{
$book = $this->bookRepo->getBySlug($slug);
$bookChildren = (new BookContents($book))->getTree(true);
$bookParentShelves = $book->shelves()->visible()->get();
Views::add($book);
if ($request->has('shelf')) {
@@ -125,6 +126,7 @@ class BookController extends Controller
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'bookParentShelves' => $bookParentShelves,
'activity' => Activity::entityActivity($book, 20, 1)
]);
}
@@ -153,7 +155,7 @@ class BookController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->getImageValidationRules(),
'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$book = $this->bookRepo->update($book, $request->all());

View File

@@ -85,7 +85,7 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->getImageValidationRules(),
'image' => 'nullable|' . $this->getImageValidationRules(),
]);
$bookIds = explode(',', $request->get('books', ''));
@@ -107,10 +107,12 @@ class BookshelfController extends Controller
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$view = setting()->getForCurrentUser('bookshelf_view_type', config('app.views.books'));
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
'view' => $view,
'activity' => Activity::entityActivity($shelf, 20, 1)
]);
}
@@ -146,7 +148,7 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
'image' => 'nullable|' . $this->getImageValidationRules(),
]);

View File

@@ -10,9 +10,6 @@ class CommentController extends Controller
{
protected $commentRepo;
/**
* CommentController constructor.
*/
public function __construct(CommentRepo $commentRepo)
{
$this->commentRepo = $commentRepo;
@@ -23,11 +20,11 @@ class CommentController extends Controller
* Save a new comment for a Page
* @throws ValidationException
*/
public function savePageComment(Request $request, int $pageId, int $commentId = null)
public function savePageComment(Request $request, int $pageId)
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
'parent_id' => 'nullable|integer',
]);
$page = Page::visible()->find($pageId);
@@ -35,8 +32,6 @@ class CommentController extends Controller
return response('Not found', 404);
}
$this->checkOwnablePermission('page-view', $page);
// Prevent adding comments to draft pages
if ($page->draft) {
return $this->jsonError(trans('errors.cannot_add_comment_to_draft'), 400);
@@ -44,7 +39,7 @@ class CommentController extends Controller
// Create a new comment.
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments.comment', ['comment' => $comment]);
}
@@ -57,14 +52,13 @@ class CommentController extends Controller
{
$this->validate($request, [
'text' => 'required|string',
'html' => 'required|string',
]);
$comment = $this->commentRepo->getById($commentId);
$this->checkOwnablePermission('page-view', $comment->entity);
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
$comment = $this->commentRepo->update($comment, $request->get('text'));
return view('comments.comment', ['comment' => $comment]);
}

View File

@@ -8,6 +8,7 @@ use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Http\Exceptions\HttpResponseException;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller as BaseController;
use Illuminate\Validation\ValidationException;
abstract class Controller extends BaseController
{
@@ -132,23 +133,6 @@ abstract class Controller extends BaseController
return response()->json(['message' => $messageText, 'status' => 'error'], $statusCode);
}
/**
* Create the response for when a request fails validation.
* @param \Illuminate\Http\Request $request
* @param array $errors
* @return \Symfony\Component\HttpFoundation\Response
*/
protected function buildFailedValidationResponse(Request $request, array $errors)
{
if ($request->expectsJson()) {
return response()->json(['validation' => $errors], 422);
}
return redirect()->to($this->getRedirectUrl())
->withInput($request->input())
->withErrors($errors, $this->errorBag());
}
/**
* Create a response that forces a download in the browser.
* @param string $content
@@ -195,6 +179,6 @@ abstract class Controller extends BaseController
*/
protected function getImageValidationRules(): string
{
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,webp';
}
}

View File

@@ -69,11 +69,7 @@ class HomeController extends Controller
}
if ($homepageOption === 'bookshelves') {
$shelfRepo = app(BookshelfRepo::class);
$shelves = app(BookshelfRepo::class)->getAllPaginated(18, $commonData['sort'], $commonData['order']);
foreach ($shelves as $shelf) {
$shelf->books = $shelf->visibleBooks;
}
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('common.home-shelves', $data);
}

View File

@@ -4,6 +4,7 @@ namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
@@ -11,10 +12,6 @@ class DrawioImageController extends Controller
{
protected $imageRepo;
/**
* DrawioImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
@@ -24,8 +21,6 @@ class DrawioImageController extends Controller
/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
@@ -35,14 +30,15 @@ class DrawioImageController extends Controller
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
return view('components.image-manager-list', [
'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
* @throws Exception
*/
public function create(Request $request)
{
@@ -66,8 +62,6 @@ class DrawioImageController extends Controller
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAsBase64($id)
{
@@ -81,6 +75,7 @@ class DrawioImageController extends Controller
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);

View File

@@ -6,6 +6,7 @@ use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
use Illuminate\Validation\ValidationException;
class GalleryImageController extends Controller
{
@@ -13,7 +14,6 @@ class GalleryImageController extends Controller
/**
* GalleryImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
@@ -24,8 +24,6 @@ class GalleryImageController extends Controller
/**
* Get a list of gallery images, in a list.
* Can be paged and filtered by entity.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function list(Request $request)
{
@@ -35,20 +33,21 @@ class GalleryImageController extends Controller
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
return view('components.image-manager-list', [
'images' => $imgData['images'],
'hasMore' => $imgData['has_more'],
]);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
* @throws ValidationException
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => $this->imageRepo->getImageValidationRules()
'file' => $this->getImageValidationRules()
]);
try {

View File

@@ -6,8 +6,11 @@ use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Exception;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class ImageController extends Controller
{
@@ -17,9 +20,6 @@ class ImageController extends Controller
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
@@ -31,8 +31,6 @@ class ImageController extends Controller
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
@@ -47,13 +45,10 @@ class ImageController extends Controller
/**
* Update image details
* @param Request $request
* @param integer $id
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
* @throws ValidationException
*/
public function update(Request $request, $id)
public function update(Request $request, string $id)
{
$this->validate($request, [
'name' => 'required|min:2|string'
@@ -64,47 +59,50 @@ class ImageController extends Controller
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
$this->imageRepo->loadThumbs($image);
return view('components.image-manager-form', [
'image' => $image,
'dependantPages' => null,
]);
}
/**
* Show the usage of an image on pages.
* Get the form for editing the given image.
* @throws Exception
*/
public function usage(int $id)
public function edit(Request $request, string $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$pages = Page::visible()->where('html', 'like', '%' . $image->url . '%')->get(['id', 'name', 'slug', 'book_id']);
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
$page->text = '';
if ($request->has('delete')) {
$dependantPages = $this->imageRepo->getPagesUsingImage($image);
}
$result = count($pages) > 0 ? $pages : false;
return response()->json($result);
$this->imageRepo->loadThumbs($image);
return view('components.image-manager-form', [
'image' => $image,
'dependantPages' => $dependantPages ?? null,
]);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
* @throws Exception
*/
public function destroy($id)
public function destroy(string $id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->checkImagePermission($image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
return response('');
}
/**
* Check related page permission and ensure type is drawio or gallery.
* @param Image $image
*/
protected function checkImagePermission(Image $image)
{

View File

@@ -0,0 +1,68 @@
<?php
namespace BookStack\Http\Controllers;
use BookStack\Notifications\TestEmail;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
class MaintenanceController extends Controller
{
/**
* Show the page for application maintenance.
*/
public function index()
{
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings.maintenance', ['version' => $version]);
}
/**
* Action to clean-up images in the system.
*/
public function cleanupImages(Request $request, ImageService $imageService)
{
$this->checkPermission('settings-manage');
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
$dryRun = !($request->has('confirm'));
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
$this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
$this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
/**
* Action to send a test e-mail to the current user.
*/
public function sendTestEmail()
{
$this->checkPermission('settings-manage');
try {
user()->notify(new TestEmail());
$this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
} catch (\Exception $exception) {
$errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage();
$this->showErrorNotification($errorMessage);
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
}

View File

@@ -163,6 +163,8 @@ class PageController extends Controller
public function getPageAjax(int $pageId)
{
$page = $this->pageRepo->getById($pageId);
$page->setHidden(array_diff($page->getHidden(), ['html', 'markdown']));
$page->addHidden(['book']);
return response()->json($page);
}

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Managers\PageContent;
use BookStack\Entities\Repos\PageRepo;
use BookStack\Exceptions\NotFoundException;
use BookStack\Facades\Activity;
@@ -46,6 +47,9 @@ class PageRevisionController extends Controller
}
$page->fill($revision->toArray());
// TODO - Refactor PageContent so we don't need to juggle this
$page->html = $revision->html;
$page->html = (new PageContent($page))->render();
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages.revision', [
@@ -73,6 +77,9 @@ class PageRevisionController extends Controller
$diff = (new Htmldiff)->diff($prevContent, $revision->html);
$page->fill($revision->toArray());
// TODO - Refactor PageContent so we don't need to juggle this
$page->html = $revision->html;
$page->html = (new PageContent($page))->render();
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages.revision', [

View File

@@ -2,7 +2,9 @@
use BookStack\Auth\Permissions\PermissionsRepo;
use BookStack\Exceptions\PermissionsException;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
class PermissionController extends Controller
{
@@ -11,7 +13,6 @@ class PermissionController extends Controller
/**
* PermissionController constructor.
* @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
*/
public function __construct(PermissionsRepo $permissionsRepo)
{
@@ -31,7 +32,6 @@ class PermissionController extends Controller
/**
* Show the form to create a new role
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function createRole()
{
@@ -41,15 +41,13 @@ class PermissionController extends Controller
/**
* Store a new role in the system.
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function storeRole(Request $request)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
'display_name' => 'required|min:3|max:180',
'description' => 'max:180'
]);
$this->permissionsRepo->saveNewRole($request->all());
@@ -59,11 +57,9 @@ class PermissionController extends Controller
/**
* Show the form for editing a user role.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws PermissionsException
*/
public function editRole($id)
public function editRole(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
@@ -75,18 +71,14 @@ class PermissionController extends Controller
/**
* Updates a user role.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws PermissionsException
* @throws \Illuminate\Validation\ValidationException
* @throws ValidationException
*/
public function updateRole(Request $request, $id)
public function updateRole(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');
$this->validate($request, [
'display_name' => 'required|min:3|max:200',
'description' => 'max:250'
'display_name' => 'required|min:3|max:180',
'description' => 'max:180'
]);
$this->permissionsRepo->updateRole($id, $request->all());
@@ -97,10 +89,8 @@ class PermissionController extends Controller
/**
* Show the view to delete a role.
* Offers the chance to migrate users.
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showDeleteRole($id)
public function showDeleteRole(string $id)
{
$this->checkPermission('user-roles-manage');
$role = $this->permissionsRepo->getRoleById($id);
@@ -113,11 +103,9 @@ class PermissionController extends Controller
/**
* Delete a role from the system,
* Migrate from a previous role if set.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws Exception
*/
public function deleteRole(Request $request, $id)
public function deleteRole(Request $request, string $id)
{
$this->checkPermission('user-roles-manage');

View File

@@ -6,6 +6,7 @@ use BookStack\Entities\Bookshelf;
use BookStack\Entities\Entity;
use BookStack\Entities\Managers\EntityContext;
use BookStack\Entities\SearchService;
use BookStack\Entities\SearchOptions;
use Illuminate\Http\Request;
class SearchController extends Controller
@@ -33,20 +34,22 @@ class SearchController extends Controller
*/
public function search(Request $request)
{
$searchTerm = $request->get('term');
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
$searchOpts = SearchOptions::fromRequest($request);
$fullSearchString = $searchOpts->toString();
$this->setPageTitle(trans('entities.search_for_term', ['term' => $fullSearchString]));
$page = intval($request->get('page', '0')) ?: 1;
$nextPageLink = url('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
$nextPageLink = url('/search?term=' . urlencode($fullSearchString) . '&page=' . ($page+1));
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
$results = $this->searchService->searchEntities($searchOpts, 'all', $page, 20);
return view('search.all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
'searchTerm' => $fullSearchString,
'hasNextPage' => $results['has_more'],
'nextPageLink' => $nextPageLink
'nextPageLink' => $nextPageLink,
'options' => $searchOpts,
]);
}
@@ -84,7 +87,7 @@ class SearchController extends Controller
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
$entities = $this->searchService->searchEntities(SearchOptions::fromString($searchTerm), 'all', 1, 20, $permission)['results'];
} else {
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
}

View File

@@ -1,9 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\User;
use BookStack\Notifications\TestEmail;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
class SettingController extends Controller
@@ -44,7 +42,7 @@ class SettingController extends Controller
$this->preventAccessInDemoMode();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
'app_logo' => 'nullable|' . $this->getImageValidationRules(),
]);
// Cycles through posted settings and update them
@@ -57,7 +55,7 @@ class SettingController extends Controller
}
// Update logo image if set
if ($request->has('app_logo')) {
if ($request->hasFile('app_logo')) {
$logoFile = $request->file('app_logo');
$this->imageRepo->destroyByType('system');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
@@ -74,63 +72,4 @@ class SettingController extends Controller
$redirectLocation = '/settings#' . $request->get('section', '');
return redirect(rtrim($redirectLocation, '#'));
}
/**
* Show the page for application maintenance.
*/
public function showMaintenance()
{
$this->checkPermission('settings-manage');
$this->setPageTitle(trans('settings.maint'));
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings.maintenance', ['version' => $version]);
}
/**
* Action to clean-up images in the system.
*/
public function cleanupImages(Request $request, ImageService $imageService)
{
$this->checkPermission('settings-manage');
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
$dryRun = !($request->has('confirm'));
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
$deleteCount = count($imagesToDelete);
if ($deleteCount === 0) {
$this->showWarningNotification(trans('settings.maint_image_cleanup_nothing_found'));
return redirect('/settings/maintenance')->withInput();
}
if ($dryRun) {
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
} else {
$this->showSuccessNotification(trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
/**
* Action to send a test e-mail to the current user.
*/
public function sendTestEmail()
{
$this->checkPermission('settings-manage');
try {
user()->notify(new TestEmail());
$this->showSuccessNotification(trans('settings.maint_send_test_email_success', ['address' => user()->email]));
} catch (\Exception $exception) {
$errorMessage = trans('errors.maintenance_test_email_failure') . "\n" . $exception->getMessage();
$this->showErrorNotification($errorMessage);
}
return redirect('/settings/maintenance#image-cleanup')->withInput();
}
}

View File

@@ -10,7 +10,6 @@ class TagController extends Controller
/**
* TagController constructor.
* @param $tagRepo
*/
public function __construct(TagRepo $tagRepo)
{
@@ -18,39 +17,23 @@ class TagController extends Controller
parent::__construct();
}
/**
* Get all the Tags for a particular entity
* @param $entityType
* @param $entityId
* @return \Illuminate\Http\JsonResponse
*/
public function getForEntity($entityType, $entityId)
{
$tags = $this->tagRepo->getForEntity($entityType, $entityId);
return response()->json($tags);
}
/**
* Get tag name suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getNameSuggestions(Request $request)
{
$searchTerm = $request->get('search', false);
$searchTerm = $request->get('search', null);
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
return response()->json($suggestions);
}
/**
* Get tag value suggestions from a given search term.
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function getValueSuggestions(Request $request)
{
$searchTerm = $request->get('search', false);
$tagName = $request->get('name', false);
$searchTerm = $request->get('search', null);
$tagName = $request->get('name', null);
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
return response()->json($suggestions);
}

View File

@@ -7,7 +7,6 @@ use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Support\Str;
class UserController extends Controller
@@ -20,10 +19,6 @@ class UserController extends Controller
/**
* UserController constructor.
* @param User $user
* @param UserRepo $userRepo
* @param UserInviteService $inviteService
* @param ImageRepo $imageRepo
*/
public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
{
@@ -36,8 +31,6 @@ class UserController extends Controller
/**
* Display a listing of the users.
* @param Request $request
* @return Response
*/
public function index(Request $request)
{
@@ -55,7 +48,6 @@ class UserController extends Controller
/**
* Show the form for creating a new user.
* @return Response
*/
public function create()
{
@@ -67,16 +59,15 @@ class UserController extends Controller
/**
* Store a newly created user in storage.
* @param Request $request
* @return Response
* @throws UserUpdateException
* @throws \Illuminate\Validation\ValidationException
*/
public function store(Request $request)
{
$this->checkPermission('users-manage');
$validationRules = [
'name' => 'required',
'email' => 'required|email|unique:users,email'
'name' => 'required',
'email' => 'required|email|unique:users,email'
];
$authMethod = config('auth.method');
@@ -85,7 +76,7 @@ class UserController extends Controller
if ($authMethod === 'standard' && !$sendInvite) {
$validationRules['password'] = 'required|min:6';
$validationRules['password-confirm'] = 'required|same:password';
} elseif ($authMethod === 'ldap') {
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2') {
$validationRules['external_auth_id'] = 'required';
}
$this->validate($request, $validationRules);
@@ -94,7 +85,7 @@ class UserController extends Controller
if ($authMethod === 'standard') {
$user->password = bcrypt($request->get('password', Str::random(32)));
} elseif ($authMethod === 'ldap') {
} elseif ($authMethod === 'ldap' || $authMethod === 'saml2') {
$user->external_auth_id = $request->get('external_auth_id');
}
@@ -138,13 +129,11 @@ class UserController extends Controller
/**
* Update the specified user in storage.
* @param Request $request
* @param int $id
* @return Response
* @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Illuminate\Validation\ValidationException
*/
public function update(Request $request, $id)
public function update(Request $request, int $id)
{
$this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
@@ -155,7 +144,7 @@ class UserController extends Controller
'password' => 'min:6|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
'profile_image' => 'nullable|' . $this->getImageValidationRules(),
]);
$user = $this->userRepo->getById($id);
@@ -191,7 +180,7 @@ class UserController extends Controller
}
// Save profile image if in request
if ($request->has('profile_image')) {
if ($request->hasFile('profile_image')) {
$imageUpload = $request->file('profile_image');
$this->imageRepo->destroyImage($user->avatar);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
@@ -212,10 +201,8 @@ class UserController extends Controller
/**
* Show the user delete page.
* @param int $id
* @return \Illuminate\View\View
*/
public function delete($id)
public function delete(int $id)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
@@ -226,11 +213,9 @@ class UserController extends Controller
/**
* Remove the specified user from storage.
* @param int $id
* @return Response
* @throws \Exception
*/
public function destroy($id)
public function destroy(int $id)
{
$this->preventAccessInDemoMode();
$this->checkPermissionOrCurrentUser('users-manage', $id);
@@ -255,8 +240,6 @@ class UserController extends Controller
/**
* Show the user profile page
* @param $id
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showProfilePage($id)
{
@@ -276,34 +259,32 @@ class UserController extends Controller
/**
* Update the user's preferred book-list display setting.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse
*/
public function switchBookView(Request $request, $id)
public function switchBooksView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'books');
}
/**
* Update the user's preferred shelf-list display setting.
* @param Request $request
* @param $id
* @return \Illuminate\Http\RedirectResponse
*/
public function switchShelfView(Request $request, $id)
public function switchShelvesView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelves');
}
/**
* For a type of list, switch with stored view type for a user.
* @param integer $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
* Update the user's preferred shelf-view book list display setting.
*/
protected function switchViewType($userId, Request $request, string $listName)
public function switchShelfView(Request $request, int $id)
{
return $this->switchViewType($id, $request, 'bookshelf');
}
/**
* For a type of list, switch with stored view type for a user.
*/
protected function switchViewType(int $userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
@@ -321,10 +302,6 @@ class UserController extends Controller
/**
* Change the stored sort type for a particular view.
* @param Request $request
* @param string $id
* @param string $type
* @return \Illuminate\Http\RedirectResponse
*/
public function changeSort(Request $request, string $id, string $type)
{
@@ -335,12 +312,18 @@ class UserController extends Controller
return $this->changeListSort($id, $request, $type);
}
/**
* Toggle dark mode for the current user.
*/
public function toggleDarkMode()
{
$enabled = setting()->getForCurrentUser('dark-mode-enabled', false);
setting()->putUser(user(), 'dark-mode-enabled', $enabled ? 'false' : 'true');
return redirect()->back();
}
/**
* Update the stored section expansion preference for the given user.
* @param Request $request
* @param string $id
* @param string $key
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function updateExpansionPreference(Request $request, string $id, string $key)
{
@@ -359,10 +342,6 @@ class UserController extends Controller
/**
* Changed the stored preference for a list sort order.
* @param int $userId
* @param Request $request
* @param string $listName
* @return \Illuminate\Http\RedirectResponse
*/
protected function changeListSort(int $userId, Request $request, string $listName)
{

View File

@@ -26,7 +26,6 @@ class Kernel extends HttpKernel
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class,
\BookStack\Http\Middleware\VerifyCsrfToken::class,
\BookStack\Http\Middleware\Localization::class,
\BookStack\Http\Middleware\GlobalViewData::class,

View File

@@ -35,9 +35,9 @@ class ApiAuthenticate
{
// Return if the user is already found to be signed in via session-based auth.
// This is to make it easy to browser the API via browser after just logging into the system.
if (signedInUser()) {
if (signedInUser() || session()->isStarted()) {
$this->ensureEmailConfirmedIfRequested();
if (!auth()->user()->can('access-api')) {
if (!user()->can('access-api')) {
throw new ApiAuthException(trans('errors.api_user_no_api_permission'), 403);
}
return;

View File

@@ -44,6 +44,10 @@ class Authenticate
], 401);
}
if (session()->get('sent-email-confirmation') === true) {
return redirect('/register/confirm');
}
return redirect('/register/confirm/awaiting');
}
}

View File

@@ -11,7 +11,7 @@ class Localization
* Array of right-to-left locales
* @var array
*/
protected $rtlLocales = ['ar'];
protected $rtlLocales = ['ar', 'he'];
/**
* Map of BookStack locale names to best-estimate system locale names.
@@ -19,6 +19,7 @@ class Localization
*/
protected $localeMap = [
'ar' => 'ar',
'bg' => 'bg_BG',
'da' => 'da_DK',
'de' => 'de_DE',
'de_informal' => 'de_DE',
@@ -26,16 +27,20 @@ class Localization
'es' => 'es_ES',
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'he' => 'he_IL',
'it' => 'it_IT',
'ja' => 'ja',
'ko' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt' => 'pl_PT',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
'sl' => 'sl_SI',
'sv' => 'sv_SE',
'uk' => 'uk_UA',
'vi' => 'vi_VN',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
'tr' => 'tr_TR',

View File

@@ -2,6 +2,10 @@
use BookStack\Auth\User;
/**
* @property int created_by
* @property int updated_by
*/
abstract class Ownable extends Model
{
/**

View File

@@ -34,7 +34,7 @@ class AppServiceProvider extends ServiceProvider
// Custom validation methods
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
$validImageExtensions = ['png', 'jpg', 'jpeg', 'gif', 'webp'];
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
});

View File

@@ -3,6 +3,13 @@
use BookStack\Entities\Page;
use BookStack\Ownable;
/**
* @property int id
* @property string name
* @property string path
* @property string extension
* @property bool external
*/
class Attachment extends Ownable
{
protected $fillable = ['name', 'order'];
@@ -30,13 +37,28 @@ class Attachment extends Ownable
/**
* Get the url of this file.
* @return string
*/
public function getUrl()
public function getUrl(): string
{
if ($this->external && strpos($this->path, 'http') !== 0) {
return $this->path;
}
return url('/attachments/' . $this->id);
}
/**
* Generate a HTML link to this attachment.
*/
public function htmlLink(): string
{
return '<a target="_blank" href="'.e($this->getUrl()).'">'.e($this->name).'</a>';
}
/**
* Generate a markdown link to this attachment.
*/
public function markdownLink(): string
{
return '['. $this->name .']('. $this->getUrl() .')';
}
}

View File

@@ -109,14 +109,14 @@ class AttachmentService extends UploadService
}
/**
* Updates the file ordering for a listing of attached files.
* @param array $attachmentList
* @param $pageId
* Updates the ordering for a listing of attached files.
*/
public function updateFileOrderWithinPage($attachmentList, $pageId)
public function updateFileOrderWithinPage(array $attachmentOrder, string $pageId)
{
foreach ($attachmentList as $index => $attachment) {
Attachment::where('uploaded_to', '=', $pageId)->where('id', '=', $attachment['id'])->update(['order' => $index]);
foreach ($attachmentOrder as $index => $attachmentId) {
Attachment::query()->where('uploaded_to', '=', $pageId)
->where('id', '=', $attachmentId)
->update(['order' => $index]);
}
}

View File

@@ -138,7 +138,7 @@ class ImageRepo
*/
public function saveDrawing(string $base64Uri, int $uploadedTo): Image
{
$name = 'Drawing-' . user()->getShortName(40) . '-' . strval(time()) . '.png';
$name = 'Drawing-' . strval(user()->id) . '-' . strval(time()) . '.png';
return $this->imageService->saveNewFromBase64Uri($base64Uri, $name, 'drawio', $uploadedTo);
}
@@ -185,7 +185,7 @@ class ImageRepo
* Load thumbnails onto an image object.
* @throws Exception
*/
protected function loadThumbs(Image $image)
public function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150, false),
@@ -221,10 +221,18 @@ class ImageRepo
}
/**
* Get the validation rules for image files.
* Get the user visible pages using the given image.
*/
public function getImageValidationRules(): string
public function getPagesUsingImage(Image $image): array
{
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
$pages = Page::visible()
->where('html', 'like', '%' . $image->url . '%')
->get(['id', 'name', 'slug', 'book_id']);
foreach ($pages as $page) {
$page->url = $page->getUrl();
}
return $pages->all();
}
}

View File

@@ -124,29 +124,24 @@ class ImageService extends UploadService
}
/**
* Saves a new image
* @param string $imageName
* @param string $imageData
* @param string $type
* @param int $uploadedTo
* @return Image
* Save a new image into storage.
* @throws ImageUploadException
*/
private function saveNew($imageName, $imageData, $type, $uploadedTo = 0)
private function saveNew(string $imageName, string $imageData, string $type, int $uploadedTo = 0): Image
{
$storage = $this->getStorage($type);
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
$fileName = $this->cleanImageFileName($imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = Str::random(3) . $imageName;
while ($storage->exists($imagePath . $fileName)) {
$fileName = Str::random(3) . $fileName;
}
$fullPath = $imagePath . $imageName;
$fullPath = $imagePath . $fileName;
if ($secureUploads) {
$fullPath = $imagePath . Str::random(16) . '-' . $imageName;
$fullPath = $imagePath . Str::random(16) . '-' . $fileName;
}
try {
@@ -175,6 +170,23 @@ class ImageService extends UploadService
return $image;
}
/**
* Clean up an image file name to be both URL and storage safe.
*/
protected function cleanImageFileName(string $name): string
{
$name = str_replace(' ', '-', $name);
$nameParts = explode('.', $name);
$extension = array_pop($nameParts);
$name = implode('.', $nameParts);
$name = Str::slug($name);
if (strlen($name) === 0) {
$name = Str::random(10);
}
return $name . '.' . $extension;
}
/**
* Checks if the image is a gif. Returns true if it is, else false.
@@ -223,6 +235,7 @@ class ImageService extends UploadService
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
@@ -292,11 +305,9 @@ class ImageService extends UploadService
/**
* Destroys an image at the given path.
* Searches for image thumbnails in addition to main provided path..
* @param string $path
* @return bool
* Searches for image thumbnails in addition to main provided path.
*/
protected function destroyImagesFromPath(string $path)
protected function destroyImagesFromPath(string $path): bool
{
$storage = $this->getStorage();
@@ -306,8 +317,7 @@ class ImageService extends UploadService
// Delete image files
$imagesToDelete = $allImages->filter(function ($imagePath) use ($imageFileName) {
$expectedIndex = strlen($imagePath) - strlen($imageFileName);
return strpos($imagePath, $imageFileName) === $expectedIndex;
return basename($imagePath) === $imageFileName;
});
$storage->delete($imagesToDelete->all());

View File

@@ -153,10 +153,6 @@ function icon(string $name, array $attrs = []): string
* Generate a url with multiple parameters for sorting purposes.
* Works out the logic to set the correct sorting direction
* Discards empty parameters and allows overriding.
* @param string $path
* @param array $data
* @param array $overrideData
* @return string
*/
function sortUrl(string $path, array $data, array $overrideData = []): string
{
@@ -166,7 +162,7 @@ function sortUrl(string $path, array $data, array $overrideData = []): string
// Change sorting direction is already sorted on current attribute
if (isset($overrideData['sort']) && $overrideData['sort'] === $data['sort']) {
$queryData['order'] = ($data['order'] === 'asc') ? 'desc' : 'asc';
} else {
} elseif (isset($overrideData['sort'])) {
$queryData['order'] = 'asc';
}

View File

@@ -13,15 +13,18 @@
"ext-mbstring": "*",
"ext-tidy": "*",
"ext-xml": "*",
"barryvdh/laravel-dompdf": "^0.8.5",
"barryvdh/laravel-snappy": "^0.4.5",
"barryvdh/laravel-dompdf": "^0.8.6",
"barryvdh/laravel-snappy": "^0.4.7",
"doctrine/dbal": "^2.9",
"facade/ignition": "^1.4",
"fideloper/proxy": "^4.0",
"gathercontent/htmldiff": "^0.2.1",
"intervention/image": "^2.5",
"laravel/framework": "^6.12",
"laravel/socialite": "^4.2",
"laravel/framework": "^6.18",
"laravel/socialite": "^4.3.2",
"league/commonmark": "^1.4",
"league/flysystem-aws-s3-v3": "^1.0",
"nunomaduro/collision": "^3.0",
"onelogin/php-saml": "^3.3",
"predis/predis": "^1.1",
"socialiteproviders/discord": "^2.0",
@@ -29,9 +32,7 @@
"socialiteproviders/microsoft-azure": "^3.0",
"socialiteproviders/okta": "^1.0",
"socialiteproviders/slack": "^3.0",
"socialiteproviders/twitch": "^5.0",
"facade/ignition": "^1.4",
"nunomaduro/collision": "^3.0"
"socialiteproviders/twitch": "^5.0"
},
"require-dev": {
"barryvdh/laravel-debugbar": "^3.2.8",

2389
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class DropJointPermissionsId extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('joint_permissions', function (Blueprint $table) {
$table->dropColumn('id');
$table->primary(['role_id', 'entity_type', 'entity_id', 'action'], 'joint_primary');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('joint_permissions', function (Blueprint $table) {
$table->dropPrimary(['role_id', 'entity_type', 'entity_id', 'action']);
});
Schema::table('joint_permissions', function (Blueprint $table) {
$table->increments('id')->unsigned();
});
}
}

View File

@@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Facades\DB;
class RemoveRoleNameField extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('roles', function (Blueprint $table) {
$table->dropColumn('name');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('roles', function (Blueprint $table) {
$table->string('name')->index();
});
DB::table('roles')->update([
"name" => DB::raw("lower(replace(`display_name`, ' ', '-'))"),
]);
}
}

View File

@@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AddActivityIndexes extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('activities', function(Blueprint $table) {
$table->index('key');
$table->index('created_at');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('activities', function(Blueprint $table) {
$table->dropIndex('key');
$table->dropIndex('created_at');
});
}
}

View File

@@ -0,0 +1,9 @@
{
"book_id": 1,
"name": "My fantastic new chapter",
"description": "This is a great new chapter that I've created via the API",
"tags": [
{"name": "Category", "value": "Top Content"},
{"name": "Rating", "value": "Highest"}
]
}

View File

@@ -0,0 +1,9 @@
{
"book_id": 1,
"name": "My fantastic updated chapter",
"description": "This is an updated chapter that I've altered via the API",
"tags": [
{"name": "Category", "value": "Kinda Good Content"},
{"name": "Rating", "value": "Medium"}
]
}

View File

@@ -0,0 +1,5 @@
{
"name": "My shelf",
"description": "This is my shelf with some books",
"books": [5,1,3]
}

View File

@@ -0,0 +1,5 @@
{
"name": "My updated shelf",
"description": "This is my update shelf with some books",
"books": [5,1,3]
}

View File

@@ -7,19 +7,12 @@
"updated_at": "2020-01-12 14:11:51",
"created_by": {
"id": 1,
"name": "Admin",
"created_at": "2019-05-05 21:15:13",
"updated_at": "2019-12-16 12:18:37",
"image_id": 48
"name": "Admin"
},
"updated_by": {
"id": 1,
"name": "Admin",
"created_at": "2019-05-05 21:15:13",
"updated_at": "2019-12-16 12:18:37",
"image_id": 48
"name": "Admin"
},
"image_id": 452,
"tags": [
{
"id": 13,

View File

@@ -0,0 +1,38 @@
{
"book_id": 1,
"priority": 6,
"name": "My fantastic new chapter",
"description": "This is a great new chapter that I've created via the API",
"created_by": 1,
"updated_by": 1,
"slug": "my-fantastic-new-chapter",
"updated_at": "2020-05-22 22:59:55",
"created_at": "2020-05-22 22:59:55",
"id": 74,
"book": {
"id": 1,
"name": "BookStack User Guide",
"slug": "bookstack-user-guide",
"description": "This is a general guide on using BookStack on a day-to-day basis.",
"created_at": "2019-05-05 21:48:46",
"updated_at": "2019-12-11 20:57:31",
"created_by": 1,
"updated_by": 1
},
"tags": [
{
"name": "Category",
"value": "Top Content",
"order": 0,
"created_at": "2020-05-22 22:59:55",
"updated_at": "2020-05-22 22:59:55"
},
{
"name": "Rating",
"value": "Highest",
"order": 0,
"created_at": "2020-05-22 22:59:55",
"updated_at": "2020-05-22 22:59:55"
}
]
}

View File

@@ -0,0 +1,29 @@
{
"data": [
{
"id": 1,
"book_id": 1,
"name": "Content Creation",
"slug": "content-creation",
"description": "How to create documentation on whatever subject you need to write about.",
"priority": 3,
"created_at": "2019-05-05 21:49:56",
"updated_at": "2019-09-28 11:24:23",
"created_by": 1,
"updated_by": 1
},
{
"id": 2,
"book_id": 1,
"name": "Managing Content",
"slug": "managing-content",
"description": "How to keep things organised and orderly in the system for easier navigation and better user experience.",
"priority": 5,
"created_at": "2019-05-05 21:58:07",
"updated_at": "2019-10-17 15:05:34",
"created_by": 3,
"updated_by": 3
}
],
"total": 40
}

View File

@@ -0,0 +1,59 @@
{
"id": 1,
"book_id": 1,
"slug": "content-creation",
"name": "Content Creation",
"description": "How to create documentation on whatever subject you need to write about.",
"priority": 3,
"created_at": "2019-05-05 21:49:56",
"updated_at": "2019-09-28 11:24:23",
"created_by": {
"id": 1,
"name": "Admin"
},
"updated_by": {
"id": 1,
"name": "Admin"
},
"tags": [
{
"name": "Category",
"value": "Guide",
"order": 0,
"created_at": "2020-05-22 22:51:51",
"updated_at": "2020-05-22 22:51:51"
}
],
"pages": [
{
"id": 1,
"book_id": 1,
"chapter_id": 1,
"name": "How to create page content",
"slug": "how-to-create-page-content",
"priority": 0,
"created_at": "2019-05-05 21:49:58",
"updated_at": "2019-08-26 14:32:59",
"created_by": 1,
"updated_by": 1,
"draft": 0,
"revision_count": 2,
"template": 0
},
{
"id": 7,
"book_id": 1,
"chapter_id": 1,
"name": "Good book structure",
"slug": "good-book-structure",
"priority": 1,
"created_at": "2019-05-05 22:01:55",
"updated_at": "2019-06-06 12:03:04",
"created_by": 3,
"updated_by": 3,
"draft": 0,
"revision_count": 1,
"template": 0
}
]
}

View File

@@ -0,0 +1,38 @@
{
"id": 75,
"book_id": 1,
"slug": "my-fantastic-updated-chapter",
"name": "My fantastic updated chapter",
"description": "This is an updated chapter that I've altered via the API",
"priority": 7,
"created_at": "2020-05-22 23:03:35",
"updated_at": "2020-05-22 23:07:20",
"created_by": 1,
"updated_by": 1,
"book": {
"id": 1,
"name": "BookStack User Guide",
"slug": "bookstack-user-guide",
"description": "This is a general guide on using BookStack on a day-to-day basis.",
"created_at": "2019-05-05 21:48:46",
"updated_at": "2019-12-11 20:57:31",
"created_by": 1,
"updated_by": 1
},
"tags": [
{
"name": "Category",
"value": "Kinda Good Content",
"order": 0,
"created_at": "2020-05-22 23:07:20",
"updated_at": "2020-05-22 23:07:20"
},
{
"name": "Rating",
"value": "Medium",
"order": 0,
"created_at": "2020-05-22 23:07:20",
"updated_at": "2020-05-22 23:07:20"
}
]
}

View File

@@ -0,0 +1,10 @@
{
"name": "My shelf",
"description": "This is my shelf with some books",
"created_by": 1,
"updated_by": 1,
"slug": "my-shelf",
"updated_at": "2020-04-10 13:24:09",
"created_at": "2020-04-10 13:24:09",
"id": 14
}

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