Compare commits

...

388 Commits

Author SHA1 Message Date
Dan Brown
4f16129869 Updated version for release v0.26.3 2019-07-10 20:21:22 +01:00
Dan Brown
64a8037fdd Merge branch 'v0.26' into release 2019-07-10 20:19:54 +01:00
Dan Brown
c732970f6e Hardened page content script escaping
Increased range of tests to cover.

Fixes #1531
2019-07-10 20:17:22 +01:00
Dan Brown
7502ba1bc8 Updated version and assets for release v0.26.2 2019-05-27 13:48:20 +01:00
Dan Brown
33a04697ef Merge branch 'master' into release 2019-05-27 13:47:47 +01:00
Dan Brown
a602cdf401 Fixed some body card horizontal scroll and column collapse issues
As mentoined in #1441
2019-05-27 13:10:48 +01:00
Dan Brown
5aa741cb60 Prevented tri-layout sidebars being faded on mobile
As mentoined in #1441
2019-05-27 12:56:31 +01:00
Dan Brown
3ad1b42a74 Updated page delete to handle inactive custom homepage correctly
Fixes #1447
2019-05-27 12:40:19 +01:00
Dan Brown
2b7362fa94 Added highlighting to current book-tree item
Related to #1435
2019-05-25 16:52:44 +01:00
Dan Brown
35f35bcba5 Updated custom home views to use tri-layout
Closes #1423
2019-05-25 16:35:27 +01:00
Dan Brown
13c0386e84 Updated string functions to use mulitbyte versions where needed
Fixes #816
2019-05-25 16:15:19 +01:00
Dan Brown
78f5f44460 Updated page navigation click to show content tab on mobile
Fixes #1454
2019-05-25 15:37:49 +01:00
Dan Brown
35e6635379 Fixed chapter description not showing in book exports
Closes #1465
2019-05-25 15:21:02 +01:00
Dan Brown
8ae35f645a Fixed faulty baseUrl rewrites
Fixes #1452
May help #1377
2019-05-19 16:25:05 +01:00
Dan Brown
5470a9e035 Merge branch 'kostefun-patch-1' 2019-05-19 15:43:11 +01:00
Dan Brown
dbfe63ccf6 Fixed missing comma in RU translation array 2019-05-19 15:42:46 +01:00
Dan Brown
114f10d5ca Merge branch 'patch-1' of git://github.com/kostefun/BookStack into kostefun-patch-1 2019-05-19 15:41:48 +01:00
Dan Brown
5226ddd959 Merge pull request #1446 from kostefun/patch-5
Update common.php
2019-05-19 15:41:21 +01:00
Dan Brown
60013f776a Merge branch 'master' into patch-5 2019-05-19 15:40:54 +01:00
Dan Brown
ac0a070fc8 Merge pull request #1445 from kostefun/patch-4
Update entities.php
2019-05-19 15:40:15 +01:00
Dan Brown
b05659d7a3 Merge pull request #1443 from kostefun/patch-3
Update common.php
2019-05-19 15:39:48 +01:00
Dan Brown
3af4648dc3 Merge pull request #1437 from NootoNooto/patch-2
Added Dutch translations for some new texts
2019-05-19 15:36:05 +01:00
Dan Brown
e1e1ea6099 Amended page save button layout to fix z-index issues
- Added a new mobile save button instead of trying to reposition the
original.
- Also recuced the point where the editor top toolbar will collapse to
become x-scrollable.

Fixes #1424
2019-05-19 15:30:58 +01:00
Dan Brown
0c3dc50cd9 Added mobile search bar on search page
Since the header one hides on mobile devices.
Fixes #1450
2019-05-19 15:06:52 +01:00
Dan Brown
0a0ceb382e Doubled image upload display thumb size
Related to #1108
2019-05-19 14:52:17 +01:00
Dan Brown
896f88174a Updated page navigation logic to ignore empty headers
Fixes #1429
2019-05-15 21:02:11 +01:00
Dan Brown
0ee9e5c4db Updated both editors to ignore image paste if text data apparent
Designed to ignore image data when copying from a spreadsheet.
Fixes #987
2019-05-15 20:23:09 +01:00
kostefun
112f73c91c Update settings.php
Ru locale fix
2019-05-14 17:50:23 +07:00
kostefun
b47dc046e0 Update common.php 2019-05-13 17:40:09 +07:00
kostefun
215d84d705 Update entities.php 2019-05-13 17:31:19 +07:00
kostefun
c459d86b58 Update common.php 2019-05-13 17:21:05 +07:00
kostefun
adf0d5fce2 Update settings.php 2019-05-13 17:09:50 +07:00
Nooto
cb355c8aad Modified Bookshelf texts 2019-05-08 23:57:44 +02:00
Nooto
d0e351b942 Added translations for Bookshelves 2019-05-08 23:51:34 +02:00
Nooto
e3d570e928 Update activities.php 2019-05-08 23:25:13 +02:00
Nooto
e00c170d85 Update common.php 2019-05-08 23:24:22 +02:00
Nooto
e430dad38c Added translations for View All, Copy, Reply, etc 2019-05-08 23:05:30 +02:00
Dan Brown
b70a5c0cdb Updated version and assets for release v0.26.1 2019-05-07 23:05:47 +01:00
Dan Brown
9443ae9f40 Merge branch 'master' into release 2019-05-07 23:05:10 +01:00
Dan Brown
d62d2384cb Updated guest settings system to format value as per non-guest
Fixes #1431
2019-05-07 22:56:48 +01:00
Dan Brown
a981dc41cb Merge pull request #1433 from Hambern/master
Updated the Swedish language files
2019-05-07 22:45:42 +01:00
Dan Brown
97ffbaa740 Fixed issue where books titles could be leaked via shelf home view
- Also added test to cover
Fixes #1425
2019-05-07 22:42:48 +01:00
vagrant
8051558d3a Updated the Swedish language files 2019-05-07 21:29:30 +00:00
Dan Brown
7ef059e254 Fixed some editor image/drawing upload endpoints
Fixes #1428
2019-05-07 22:23:44 +01:00
Dan Brown
4329fee2c9 Fixed 404 card header fonts
Fixes #1427
2019-05-07 22:10:54 +01:00
Dan Brown
b1cf5ab309 Standardised login tab order and evened card padding
Closes #1418
2019-05-07 22:07:50 +01:00
Dan Brown
b67d9f4036 Updated verison for 0.26 dev path 2019-05-07 21:55:41 +01:00
Dan Brown
224d9e7a7d Merge pull request #1420 from moucho/master
Spanish translation
2019-05-07 21:52:02 +01:00
Dan Brown
31419c3913 Merge pull request #1419 from Mant1kor/master
Ukrainian translation update
2019-05-07 21:51:15 +01:00
moucho
ba36d36597 Spanish 2019-05-07 01:28:00 +02:00
Mantikor
ac7d6b8737 Update validation.php 2019-05-06 23:11:23 +03:00
Mantikor
e52dab825a Update settings.php 2019-05-06 23:05:48 +03:00
Mantikor
33f51d5b78 Update passwords.php 2019-05-06 22:27:17 +03:00
Mantikor
ae7529376b Update pagination.php 2019-05-06 22:26:43 +03:00
Mantikor
aefd4d423c Update errors.php 2019-05-06 22:25:40 +03:00
Mantikor
bad44391d4 Update auth.php 2019-05-06 22:21:29 +03:00
Mantikor
1880633be3 Update common.php 2019-05-06 22:21:15 +03:00
Mantikor
8273f8b16e Update entities.php 2019-05-06 22:20:31 +03:00
Mantikor
0600f752eb Update components.php 2019-05-06 21:57:22 +03:00
Mantikor
342dc8948c Update common.php 2019-05-06 21:56:10 +03:00
Mantikor
3e24e44106 Update auth.php 2019-05-06 21:45:16 +03:00
Mantikor
404d11d0eb Update activities.php 2019-05-06 21:40:04 +03:00
Mantikor
07b889547d Merge pull request #1 from BookStackApp/master
merge changes
2019-05-06 21:33:09 +03:00
Dan Brown
220c2a4102 Updated version and assets for release v0.26.0 2019-05-06 18:58:56 +01:00
Dan Brown
e9914eb301 Merge branch 'master' into release 2019-05-06 18:57:58 +01:00
Dan Brown
e47a7e0b97 Merge pull request #1417 from Hambern/master
Updated the swedish lang files
2019-05-06 18:44:51 +01:00
vagrant
989309fdff Updated the swedish lang files 2019-05-06 17:19:51 +00:00
Dan Brown
6797c91eeb Updated dropdowns to close all others before opening 2019-05-06 17:59:17 +01:00
Dan Brown
b9ad3f9f65 Fixed intersection observer check on iOS 2019-05-06 16:08:08 +01:00
Dan Brown
7a8678e5f7 Tweaked colors for accessibility, applied fixes found during testing
- Fixed overriding h3 content header style.
- Updated notification styling to be less overwhelming.
- Increased floated image margin.
- Adjusted callout icon placement.
- Fixed tinymce fullscreen zindex issue.
2019-05-06 00:15:03 +01:00
Dan Brown
ba09dad1fe Fixed shelf activity display & updated book sort operation 2019-05-05 15:54:22 +01:00
Dan Brown
5910e00fb8 Made app core timezone configurable via env
Related to #1407
2019-05-05 15:09:04 +01:00
Dan Brown
3f83c548f8 Ran phpcbf 2019-05-05 14:54:37 +01:00
Dan Brown
adc866cb3d Added ability for dropdown menu to be bottom of dom body
- Used when a dropdown is within a scrollable section such as editor
toolbar on mobile.
- Also made mobile page save button more obvious by increasing size and
inverting color.
2019-05-05 14:43:26 +01:00
Dan Brown
ad542f0407 Prevented potential inline JS event usage
- Removes 'on*' attributes from elements.
- Also updated script logic to remove scripts instead of escaping.
- All JS injection removal now uses DomDocument + xpath parsing.
2019-05-05 13:53:37 +01:00
Dan Brown
15786e2630 Merge pull request #1410 from BookStackApp/image_management_rewrite
Image management rewrite
2019-05-04 18:16:58 +01:00
Dan Brown
8c190324ac Updated existing image tests to reflect changes
- Also added some new tests
2019-05-04 18:11:19 +01:00
Dan Brown
79f6dc00a3 Change image-selector to not use manager
- Now changes the images directly for user, system & cover.
- Extra permission checks added to edit & delete actions.
2019-05-04 15:50:29 +01:00
Dan Brown
cb832a2c10 Started diversion to not using image manager for cover/system/user 2019-04-27 14:55:23 +01:00
Dan Brown
a87ae16010 Started extraction of image controller to separate controllers 2019-04-27 14:18:00 +01:00
Dan Brown
aeb1fc4d49 Started rewriting back-end image managment 2019-04-21 15:52:29 +01:00
Dan Brown
6428f32483 Prevented invalid form inputs having incorrect padding 2019-04-21 14:11:49 +01:00
Dan Brown
884e20cc5e Updated TinyMCE to version 4.9.4 2019-04-21 13:49:46 +01:00
Dan Brown
fc761784a1 Merge branch 'cw1998-fix/registraion-form-validation' 2019-04-21 12:52:11 +01:00
Dan Brown
e0c229114f Updated register link text/placement on login card
- Also extracted "Already have account?" text to translation files.
2019-04-21 12:45:09 +01:00
Dan Brown
4e49d06182 Merge branch 'fix/registraion-form-validation' of git://github.com/cw1998/BookStack into cw1998-fix/registraion-form-validation 2019-04-21 12:24:39 +01:00
Dan Brown
2bb06463d5 Added deeper content id de-duplication
Closes #1393
2019-04-21 12:22:41 +01:00
Dan Brown
4d6df37963 Merge branch 'XVilka-patch-1' 2019-04-20 13:32:03 +01:00
Dan Brown
553d3ce861 Merge branch 'patch-1' of git://github.com/XVilka/BookStack into XVilka-patch-1 2019-04-20 13:30:50 +01:00
Dan Brown
0bc5ccba32 Add revision restore confirm and changed http method
Closes #1321
2019-04-20 13:25:16 +01:00
Dan Brown
ed330f246c Updated md drawing mngr shortcut to work on mac cmd key
Closes #1228
2019-04-20 13:12:35 +01:00
Dan Brown
6c66a8935a Added test to check page HTML id de-duplication
Relates to #1393
2019-04-20 13:01:56 +01:00
Dan Brown
c653618eab Merge branch 'cw1998-fix/dot-in-role-names' 2019-04-20 11:26:48 +01:00
Dan Brown
efc034bd8d Merge branch 'fix/dot-in-role-names' of git://github.com/cw1998/BookStack into cw1998-fix/dot-in-role-names 2019-04-16 23:08:23 +01:00
Dan Brown
c24764018a Updated ldap server option parsing to work with protocol and port
- Aligns with PHP behaviour where ports is ignore for full LDAP URI.
- Added tests to check format being passed to LDAP is as expected.
- May be related to #1220
- Related to #1386 and #1278
2019-04-16 22:47:53 +01:00
Christopher Wilkinson
c8cf6731e2 Add min length validation on name on register form & add sign up link 2019-04-16 12:18:51 +01:00
Dan Brown
d0db0f8e26 Reduced markup for books icon 2019-04-15 21:21:54 +01:00
Dan Brown
c380c10d54 Prevented bad duplicate IDs causing major exception
Related to #1393
2019-04-15 21:20:32 +01:00
Dan Brown
95d4149d5e Merge branch 'cw1998-feature/create-book-button-on-shelves' 2019-04-15 20:45:14 +01:00
Dan Brown
7f3f6e65b9 Aligned item creation wording and updated shelf-book-add logic 2019-04-15 20:45:04 +01:00
Christopher Wilkinson
29f17fd154 Replace dots with something else on user create and edit screens 2019-04-15 15:42:18 +01:00
Christopher Wilkinson
84419005e7 Update create new book button on shelves to 2019 design 2019-04-15 10:56:21 +01:00
Christopher Wilkinson
d3cd369247 Fix phpcs issues 2019-04-15 09:27:17 +01:00
Christopher Wilkinson
50a9c71de0 Add tests for creating a book and adding directly to a shelf 2019-04-15 09:27:17 +01:00
Christopher Wilkinson
faa3a8b842 Add button to add a book directly from a shelf view 2019-04-15 09:27:17 +01:00
Dan Brown
c836862d89 Tweaked header font size to fit redesign better 2019-04-14 14:20:53 +01:00
Dan Brown
03073dd9e4 Merge pull request #1153 from BookStackApp/2019-design
WIP: 2019 design
2019-04-14 13:54:36 +01:00
Dan Brown
ee58bea8b7 Updated user references to be app-default-supporting functions 2019-04-14 13:19:33 +01:00
Dan Brown
9406b4d4c9 Updated view toggle to store date
Also added test for user list order preferences
2019-04-14 13:01:51 +01:00
Dan Brown
01be72d5e2 Updated markdown editor for mobile
Also tweaked padding and responsivness on many common elements
2019-04-14 12:04:20 +01:00
Dan Brown
21e1123d12 Updated editor usability on mobile 2019-04-13 18:30:11 +01:00
Dan Brown
8d358e4894 Updated tri-layout on mobile to be tab based 2019-04-13 17:36:27 +01:00
Dan Brown
f797d2da20 Added column select-all to role permission table 2019-04-13 13:16:18 +01:00
Dan Brown
d3cc261320 Fixed "Add comment" layout when no comments exist 2019-04-13 12:58:57 +01:00
Dan Brown
cc24d478aa Organised entity action buttons a little more 2019-04-13 12:46:15 +01:00
Dan Brown
07adfb2ff1 Added select-all helpers to permission tables 2019-04-13 12:07:27 +01:00
Dan Brown
36481bb73f Updated guest page-create intermediate page 2019-04-13 11:30:19 +01:00
Dan Brown
4d5e47a2d2 Updated empty container item states 2019-04-13 11:24:41 +01:00
Dan Brown
d66fab8bee Updated and aligned entity dashboard elements 2019-04-13 11:09:17 +01:00
Dan Brown
2694bb8fab Updated the design of the comments section 2019-04-13 10:50:24 +01:00
Dan Brown
b12ae6d11b Added bookshelves to breadcrumbs
- Updated breadcrumb dropdown switchers and back-end sibling code to handle new breadcrumbs.
- Added breadcrumb view composer and EntityContext system to mangage
tracking if in the context of a bookshelf.
2019-04-07 18:28:11 +01:00
Dan Brown
221a483b40 Standardised view referencing to dot-notation 2019-04-07 12:00:09 +01:00
Dan Brown
4a127c29a5 Removed important color overrides to a tags 2019-04-07 11:36:50 +01:00
Dan Brown
8c21b5345d Cleaned up usage of some core scss files 2019-04-07 11:34:40 +01:00
Dan Brown
0a06e2bce3 Actioned some todo items, Cleaned old grid css 2019-04-07 09:57:48 +01:00
Dan Brown
d9cde4123d Fixed entity excerpt function signature misalignment 2019-04-06 18:47:27 +01:00
Dan Brown
7cda9b026e Updated tests to suit layout changes, Updated 404 page
- Also replaced 'or' usage in templates with null coalescing operator
2019-04-06 18:36:17 +01:00
Dan Brown
67ed4710b6 Cleaned up old toolbar usage 2019-04-06 17:43:44 +01:00
Dan Brown
745a0bb98d Updated custom homepage views 2019-04-06 17:31:59 +01:00
Dan Brown
aedff7dc6d Added book selector to books sort
Now more efficient rather than listing all books in the system.
2019-04-06 16:59:04 +01:00
Dan Brown
17969c0bbf Added shelves and search shortcuts to profile page 2019-04-06 16:21:20 +01:00
Dan Brown
666ced9c3b Fixed tri-layout overflow in some scenarios 2019-03-30 17:07:01 +00:00
Dan Brown
37bf7f11e4 Implemented new design in entity selector
- Also showed entity path in search.
- Cleaned popular entity fetch logic.
- Cleaned entity selector JS code a little
2019-03-30 16:54:15 +00:00
Dan Brown
bda8aa414b Fixed up edit views to use new layout
- Also updated chapter pages in books view to show detail
2019-03-30 15:49:14 +00:00
Dan Brown
60d175a9b9 Cleaned up sidebar book tree and moved details
- Also made top-spacing more consistent
2019-03-30 15:15:01 +00:00
Dan Brown
42e908c7f0 Cleaned up some existing tri-column views 2019-03-30 14:27:00 +00:00
Dan Brown
53a26a365c Merge branch 'master' into 2019-design 2019-03-30 13:17:29 +00:00
Dan Brown
4ee0fde0ac Updated readme with security info 2019-03-24 20:42:52 +00:00
Dan Brown
934512d09c Updated version and assets for release v0.25.5 2019-03-24 19:45:17 +00:00
Dan Brown
9102c90986 Merge branch 'master' into release 2019-03-24 19:45:00 +00:00
Dan Brown
9879a0d12c Added helper text for no_double_extension validation 2019-03-24 19:40:45 +00:00
Dan Brown
5d2e80bff3 Merge pull request #1348 from agvol/master
Russian translation
2019-03-24 19:14:53 +00:00
Dan Brown
83818234c8 Merge pull request #1347 from cima/czech-translation
Czech translation
2019-03-24 19:13:48 +00:00
Dan Brown
5c00187138 Merge pull request #1327 from leomartinez/master
Updated 'Spanish Argentina' translation.
2019-03-24 19:11:01 +00:00
Dan Brown
193e2ffebe Prevent dbl exts. on img upload, Randomized attachment upload names 2019-03-24 19:08:21 +00:00
agvol
b2a5d07787 Update russian lang 2019-03-23 10:22:54 +03:00
agvol
15cf9f8b32 Update russian lang 2019-03-23 10:18:44 +03:00
agvol
f9adfee47a Update russian lang 2019-03-23 10:04:50 +03:00
Martin Šimek
2e988277f0 Czech translation
+ Unit test repair
2019-03-23 00:35:42 +01:00
Martin Šimek
4e8e28c35f Czech translation
+ Typos
+ errors.php
2019-03-22 22:52:26 +01:00
Anton Kochkov
4cbfb2bf13 Enable syntax highlight for OCaml, Haskell, Rust 2019-03-22 16:42:02 +08:00
Dan Brown
c3e74219c4 Updated version and assets for release v0.25.4 2019-03-21 19:46:19 +00:00
Dan Brown
13c9d7bc2d Merge branch 'master' into release 2019-03-21 19:43:48 +00:00
Dan Brown
f5fe524e6c Added extension whitelist for image uploads
- A continuation of the security issues addressed in v0.25.3
2019-03-21 19:43:15 +00:00
Dan Brown
119b539586 Updated version and assets for release v0.25.3 2019-03-21 00:03:26 +00:00
Dan Brown
29a5c180f0 Merge branch 'master' into release 2019-03-21 00:02:33 +00:00
Dan Brown
37b91b6b0e Hardened image file validation by removing custom validation
- Added test to check PHP files cannot be uploaded as an image.
2019-03-20 23:59:55 +00:00
Martin Šimek
b3a4d8af2a Czech translation
+ Czech language (cs)
+ settings.php in english populated by new option
Note: validation php taken from  lavarel official translation (https://github.com/caouecs/Laravel-lang/blob/master/src/cs/validation.php)
2019-03-19 22:45:14 +01:00
Dan Brown
8b7bee7c67 Updated standard entity lists 2019-03-17 15:07:03 +00:00
Dan Brown
837d2bc582 Improved login, Fixed breadcrumbs & improved grid thumbnails 2019-03-16 16:00:41 +00:00
Leonardo Martinez
64cd499542 Updated 'Spanish Argentina' translation. 2019-03-12 11:46:02 -03:00
Dan Brown
5f2d226f09 Merge branch 'master' into 2019-design 2019-03-10 21:40:02 +00:00
Dan Brown
7906602291 Updated version and assets for release v0.25.2 2019-03-10 13:45:21 +00:00
Dan Brown
6dafe773ff Merge branch 'master' into release 2019-03-10 13:44:29 +00:00
Dan Brown
00703fa817 Merge branch 'dfanara-feature-ldap-attributes' 2019-03-10 10:55:36 +00:00
Dan Brown
44c537de1a Performed some LDAP service/test cleanup 2019-03-10 10:54:19 +00:00
Dan Brown
6bccf0e64a Merge branch 'feature-ldap-attributes' of git://github.com/dfanara/BookStack into dfanara-feature-ldap-attributes 2019-03-10 10:31:09 +00:00
Dan Brown
042a6f9760 Updated shelf menu item to show on custom permission
- Extended new 'userCanOnAny' helper to take a entity class for
filtering.

Closes #1201
2019-03-09 21:15:45 +00:00
Dan Brown
04287745e4 Merge branch 'mark-james-Copy-For-View-Only' 2019-03-09 16:52:47 +00:00
Dan Brown
5c9b528517 Abstracted userCanCreatePage helper to work for any permisison
- Added test to cover scenario where someone with create-own permission
would want to copy a viewable item into a container entity that they
own.
2019-03-09 16:50:22 +00:00
Dan Brown
6be2d3f28c Merge branch 'Copy-For-View-Only' of git://github.com/mark-james/BookStack into mark-james-Copy-For-View-Only 2019-03-09 16:12:12 +00:00
Daniel Fanara
6d20bdc1fb Preserve original display_name_attribute configuration values. 2019-03-09 01:13:30 -05:00
Daniel Fanara
502ea608bf Issue #1306 - Unit Tests for LdapService Changes 2019-03-09 01:08:49 -05:00
Daniel Fanara
55b07c7076 Issue #1306 - Specify display name attribute from LDAP 2019-03-08 23:55:11 -05:00
Dan Brown
33e999909f Merge branch 'fix-1186' 2019-03-08 22:57:57 +00:00
Dan Brown
6be95cd2ac Re-centered dropzone error arrow 2019-03-08 22:57:24 +00:00
Dan Brown
f467185e86 Merge branch 'master' into fix-1186 2019-03-08 22:47:31 +00:00
Dan Brown
646fd822c5 Updated redis config logic, Now takes a password
- Previous config did not use multiple servers in any way.
- Cluster will now be created automatically if multiple servers given.
- Removed REDIS_CLUSTER option.

Closes #1283
2019-03-08 22:42:48 +00:00
Dan Brown
d96baf2d4a Set 'uploaded_to' parameters for editor-pasted/dragged images
Allows image-listing permission system to work as intended.
Fixes #1287
2019-03-08 21:32:31 +00:00
Dan Brown
1c312906bc Added a configurable upload size limit
Closes #1293
2019-03-08 21:06:37 +00:00
Dan Brown
9126c87f2b Merge pull request #1272 from Xiphoseer/patch-1
Add german translations for shelves
2019-03-07 22:14:35 +00:00
Dan Brown
579d98a908 Merge pull request #1314 from maantje/patch-2
Update Dutch password_hint translation to correspond with validation
2019-03-07 21:53:08 +00:00
Dan Brown
98a4359198 Updated user language select to use correct default
- Updated localisation system to take note of system defaul locale
before replacing the current locale
Fixes #1316
2019-03-07 21:09:23 +00:00
Jamie Schouten
6f710225b5 Update Dutch password_hint translation to correspond with validation rule
At the moment the translation says ```Minimaal 5 tekens``` which means your password should be at least 5 characters long. But a 5 character long password is not allowed by the validator. 

I think this was a translation error from the English one where it says ```Must be over 5 characters```. To make the Dutch translation correct the Dutch translation should be changed to ```Minimaal 6 tekens```.

```
    /**
     * Get a validator for an incoming registration request.
     *
     * @param  array $data
     * @return \Illuminate\Contracts\Validation\Validator
     */
    protected function validator(array $data)
    {
        return Validator::make($data, [
            'name' => 'required|max:255',
            'email' => 'required|email|max:255|unique:users',
            'password' => 'required|min:6',
        ]);
    }
```
2019-03-06 17:10:15 +01:00
Dan Brown
b273b9d6d0 Improved alignment classes used by WYSIWYG editor
- Fixed table cells being floated, Fixes #1284.
- Made it possible to easily center linked images.
2019-03-02 09:08:01 +00:00
Dan Brown
e471d0c52a Added lua to code languages
Closes #1223
2019-03-02 08:52:14 +00:00
Dan Brown
0a431e3223 Merge pull request #1263 from christophert/add-powershellmarkup
Add Powershell Code Markup
2019-03-02 08:45:08 +00:00
Dan Brown
035a0d8efb Added experimental breadcrumb traversal 2019-02-24 15:57:35 +00:00
Dan Brown
e70423c73f Standardized breadcrumbs a little further with icons 2019-02-17 17:52:42 +00:00
Dan Brown
8445304fe9 Added book sort helper buttons 2019-02-17 11:44:02 +00:00
Dan Brown
f1e571a57c Made shelf listing more unique & efficient
- Now includes listing of all books within.
2019-02-16 17:13:01 +00:00
Dan Brown
e9be2b7174 Standardized setting casing 2019-02-16 15:39:23 +00:00
Dan Brown
352cbbd074 Updated design of page navigation box 2019-02-16 15:39:11 +00:00
Dan Brown
b00b319e83 Re-arranged some list items to flexbox instead of grid
Since flexbox is better supported on a wider range of elements
2019-02-16 15:05:18 +00:00
Dan Brown
a112c11df8 Re-ordered and updated main settings page 2019-02-16 14:17:35 +00:00
Xiphoseer
058cc2cbd6 Update entities.php 2019-02-12 12:30:43 +01:00
Xiphoseer
edd98c00e5 Update entities.php 2019-02-12 12:30:12 +01:00
Xiphoseer
19bb11a1c9 Update entities.php
Add informal german shelve localisations
2019-02-12 12:28:48 +01:00
Xiphoseer
9511d10ec8 Update entities.php
Add german shelve localizations
2019-02-12 12:24:01 +01:00
Dan Brown
3286f29a61 Merge branch 'master' into 2019-design 2019-02-09 14:58:38 +00:00
Christopher Tran
77d3bd31a6 add powershell code block link 2019-02-06 01:06:59 -05:00
Dan Brown
56004abdf4 Added high-level release and roadmap info to readme
Closes #1259
2019-02-06 00:09:39 +00:00
Dan Brown
df6f6e2d77 Merge branch 'master' of github.com:BookStackApp/BookStack 2019-02-04 19:57:43 +00:00
Dan Brown
ba1b3fc181 Made some readme tweaks 2019-02-04 19:57:21 +00:00
Dan Brown
67d4fa0c65 Updated npm packages and migrated webpack css plugin 2019-02-03 18:21:21 +00:00
Dan Brown
49deab3a02 Fixed page edit to have white background 2019-02-03 17:53:54 +00:00
Dan Brown
5325870271 Updated auth pages to new design, Removed public layout 2019-02-03 17:34:15 +00:00
Dan Brown
138f5d5c4f Updated user and shelf views to new design 2019-02-03 13:45:45 +00:00
Dan Brown
880d4f35da Started the migration of the setting views 2019-02-02 15:49:57 +00:00
Dan Brown
20988962fe Migrated a whole load more page/chapter/shelf views 2019-02-02 11:41:41 +00:00
Dan Brown
32603362a6 Updated a bunch of book views 2019-01-31 20:37:12 +00:00
abijeet
9dba9ca178 Fixes tooltip on the image manager.
Fixes #1186
2019-01-27 19:43:31 +05:30
Abijeet Patro
8a4a81629f Merge pull request #1237 from BookStackApp/phpcs-fixes
PHPCS related fixes.
2019-01-27 16:04:30 +05:30
abijeet
5ef0992d5b PHPCS related fixes. 2019-01-27 15:59:23 +05:30
Dan Brown
25bc28a1be Updated version and assets for release v0.25.1 2019-01-20 15:42:32 +00:00
Dan Brown
4c561c7fa0 Merge branch 'master' into release 2019-01-20 15:41:24 +00:00
Dan Brown
12be7d0086 Added extra s3 config parameters for use s3-like service compatibility
For #1192 and #1195
2019-01-20 15:23:49 +00:00
Dan Brown
ba0af9214e Updated socialite to work around google+ API shutdown
Fixes #1190
Will require docs update
2019-01-20 14:58:06 +00:00
Dan Brown
36424a24b5 Added ability for date format strings to be localized by back-end
Requires the locale to be installed on the system-side.
Closes #1214
2019-01-19 12:11:18 +00:00
Dan Brown
a70ee9664a Fixed firefox page print view and removed comments from prints
Closes #1211
2019-01-19 11:33:27 +00:00
Dan Brown
156c0a88e9 Updated sidebar to prevent rubber-banding with comments disabled
Fixes #1218
2019-01-19 11:10:46 +00:00
Dan Brown
0efed43389 Converted more views to new layout and made breadcrumbs more flexible 2019-01-13 15:54:55 +00:00
Dan Brown
163a57cf70 Merge branch 'master' into 2019-design 2019-01-13 14:10:27 +00:00
Dan Brown
95b3e78573 Updated version and assets for release v0.25.0 2019-01-12 22:48:53 +00:00
Dan Brown
63a345bc93 Merge branch 'master' into release 2019-01-12 22:47:07 +00:00
Dan Brown
a3ccde8698 Updated TinyMCE and fixed TinyMCE/Codemirror cursor jumping
For #1162
2019-01-12 19:23:18 +00:00
Dan Brown
9700b7ccea Merge pull request #1205 from BookStackApp/env-cleanup
Simplified example env and created full example copy
2019-01-06 16:05:51 +00:00
Dan Brown
54c428c375 Commented APP_URL by default to prevent upgrade path issues 2019-01-06 16:01:24 +00:00
Dan Brown
ebe5d643f3 Simplified example env and created full example copy 2019-01-06 15:46:16 +00:00
Dan Brown
e66ddbc17b Merge pull request #1197 from moucho/master
Spanish update
2019-01-06 14:37:11 +00:00
Dan Brown
0e0a17cc30 Prevented page text content includes
Avoids possible permission issues where included content shown in search or preview
where the user would not normally have permission to view the included content.

Closes #1178
2019-01-05 17:18:40 +00:00
Dan Brown
ffceb4092e Merge branch 'cw1998-fix/#1110' 2019-01-05 15:22:59 +00:00
Dan Brown
50e5527483 Added test to cover "users" header link in correct permission conditions 2019-01-05 15:22:47 +00:00
Dan Brown
f63fd4beca Merge branch 'fix/#1110' of git://github.com/cw1998/BookStack into cw1998-fix/#1110 2019-01-05 15:12:33 +00:00
Dan Brown
d682a0157f Merge branch 'qianmengnet-master' 2019-01-05 15:01:38 +00:00
Dan Brown
70ad707c3c Tweaked profile page anchor links and swapped register/login links
Also added test for login/register links on non-auth app view
Relates to #1146
2019-01-05 15:01:16 +00:00
Dan Brown
3062bf1876 Merge branch 'master' of git://github.com/qianmengnet/BookStack into qianmengnet-master 2019-01-05 14:47:47 +00:00
Dan Brown
a2087fe3ff Made delete permissions a requirement for move operations
Closes #1200
2019-01-05 14:39:40 +00:00
Mark James
19770d2792 Use joint_permissions to determine is a user has an available page or chapter to copy. 2019-01-02 16:55:28 +11:00
Mark James
99c6d70c51 Initial updates to allow for page copy when the user can read the page but can't update it. 2018-12-31 17:01:49 +11:00
mark-james
0830521e60 Merge pull request #1 from BookStackApp/master
Update From Bookstack
2018-12-31 09:00:19 +11:00
moucho
2c48f4f7e8 Format update 2018-12-24 12:41:42 +01:00
Dan Brown
7f95b51b00 Rolled tri-layout to page edit and book-create 2018-12-09 16:51:31 +00:00
Dan Brown
ff0b9004bc Cleaned existing grid view up a little 2018-12-09 14:04:28 +00:00
Dan Brown
8fd8652bbf Added tri-layout desktop sticky-scroll 2018-12-09 13:42:35 +00:00
Dan Brown
e1474194db Added responsive functionality to tri-layout view. 2018-12-08 23:34:06 +00:00
Dan Brown
4c574c22a8 Implemented functionality to make books sort function
Also changed public user settings to be stored in session rather than DB.
Cleaned existing list view type logic.
2018-12-07 18:33:53 +00:00
Dan Brown
0b976d9f91 Added list sorting styles, Yet to add functionality 2018-12-01 21:28:21 +00:00
Dan Brown
2a882a43ff Updated books listing to three column layout design 2018-12-01 20:28:17 +00:00
Dan Brown
aabd4c0412 Started looking at the books listing design 2018-12-01 16:29:57 +00:00
Dan Brown
d39fc84301 Merge branch 'master' into 2019-design 2018-12-01 13:57:23 +00:00
Dan Brown
e093a172cb Updated assets and version for release v0.24.3 2018-11-27 21:52:20 +00:00
Dan Brown
4b01f8934b Merge branch 'master' into release 2018-11-27 21:51:32 +00:00
qianmengnet
3c796b1ae7 Add "register" to nav.
Add "register" to nav.You need to click "login" to find register, which is not convenient for people who are not familiar with the app.
2018-11-26 09:05:38 +08:00
qianmengnet
b7915cc7b0 Add anchor link to "Created Content" on the "View Profile"
Add 3 anchor link to "Created Content" on the "View Profile" page and click to jump to the page section
2018-11-26 08:47:49 +08:00
Christopher Wilkinson
54b36cd305 Show users link in top nav if user is signed in and only manages users 2018-11-13 13:43:20 +00:00
Dan Brown
4df11701e7 Updated book-tree design and abstracted breadcrumb template 2018-11-11 13:11:36 +00:00
Dan Brown
4a872012c5 Merge branch 'master' into 2019-design 2018-11-11 11:44:35 +00:00
Dan Brown
bc116b45b5 Re-updated assets for release v0.24.2 2018-11-10 16:10:22 +00:00
Dan Brown
a059960b9e Merge branch 'master' into release 2018-11-10 16:09:14 +00:00
Dan Brown
7770966fed Updated assets for release v0.24.2 2018-11-10 16:01:55 +00:00
Dan Brown
d7adcf6c69 Merge branch 'master' into release 2018-11-10 16:01:01 +00:00
Dan Brown
c356612612 Added sidebar layout size tweaks 2018-11-04 14:41:52 +00:00
Dan Brown
0e395b1e21 Started reworking of page-show design
- Updated core toolbar & breadcrumb design
2018-10-21 20:05:11 +01:00
Dan Brown
89be30ff0e Started on a design update
- Added base of new grid system.
- Added new margin/padding/visiblity helpers.
- Made header collapse to overflow menu on mobile.
2018-10-16 18:49:56 +01:00
Dan Brown
04a364dcc3 Incremented version for v0.24.1 2018-09-24 16:34:16 +01:00
Dan Brown
db83ac7eaa Merge branch 'master' into release 2018-09-24 16:32:30 +01:00
Dan Brown
3ca9dddf61 Merge branch 'master' into release 2018-09-24 15:59:39 +01:00
Dan Brown
bf74f53ca7 Updated assets for release and incremented version 2018-09-24 12:18:27 +01:00
Dan Brown
9d67efb4a4 Merge branch 'master' into release 2018-09-24 12:08:21 +01:00
Dan Brown
3a39b9f440 Merge pull request #1022 from BookStackApp/revert-983-master
Revert "Update german translation"
2018-09-22 18:33:29 +01:00
Dan Brown
27f7aab375 Revert "Update german translation" 2018-09-22 18:33:15 +01:00
Dan Brown
337da0c467 Merge pull request #983 from vriic/master
Update german translation
2018-09-22 18:27:04 +01:00
Nikolai Nikolajevic
f56b3560c4 Update german translation 2018-08-23 16:17:46 +02:00
Dan Brown
02dfe11ce6 Increment version for release v0.23.2 2018-08-19 15:33:23 +01:00
Dan Brown
83d06beb70 Merge branch 'master' into release 2018-08-19 15:33:10 +01:00
Dan Brown
a8cfc059c8 Updated version for release v0.23.1 2018-08-12 14:22:53 +01:00
Dan Brown
1614b2bab0 Merge branch 'master' into release 2018-08-12 14:22:17 +01:00
Dan Brown
4bdec0d214 Updated version and assets for release v0.23 2018-07-29 20:28:49 +01:00
Dan Brown
6a7d7e7c2b Merge branch 'master' into release 2018-07-29 20:26:00 +01:00
Dan Brown
30d4674657 Updated assets for release v0.22 2018-05-28 14:19:14 +01:00
Dan Brown
9f961f95f8 Merge branch 'master' into release 2018-05-28 14:19:04 +01:00
Dan Brown
bab99a26ec Updated assets and version for v0.21 release 2018-04-22 20:21:22 +01:00
Dan Brown
9a7fecd269 Merge branch 'master' into release 2018-04-22 20:19:02 +01:00
Dan Brown
a8dc0d449b Updated the version because i'm such a plonker
And forgot to do this last release.
I wonder if there's a simple commit hook that could prevent the same two
versions twice in a row?
2018-03-30 15:41:46 +01:00
Dan Brown
a0381f76bf Merge branch 'v0.20' into release 2018-03-30 15:33:23 +01:00
Dan Brown
6102f66daa Updated assets for release v0.20.1 2018-03-25 16:58:14 +01:00
Dan Brown
c6134d162d Merge branch 'master' into release 2018-03-25 16:54:48 +01:00
Dan Brown
2046f9b9de Updated assets for release v0.20.0 2018-02-11 18:20:17 +00:00
Dan Brown
ac3ba594a4 Merge branch 'master' into release and updated version 2018-02-11 18:19:38 +00:00
Dan Brown
22df25a480 Updated assets and version for v0.19.0 2017-12-10 18:21:07 +00:00
Dan Brown
8b30c7f02e Merge branch 'master' into release 2017-12-10 18:19:20 +00:00
Dan Brown
757cdddc7c Updated version and JS for release v0.18.5 2017-11-11 18:33:04 +00:00
Dan Brown
df95e99680 Updated assets and version for release v0.18.4 2017-10-15 19:28:29 +01:00
Dan Brown
5a6d544db7 Merge branch 'master' into release 2017-10-15 19:27:50 +01:00
Dan Brown
16117d329c Merge branch 'master' into release, Updated version 2017-10-06 21:05:45 +01:00
Dan Brown
e90da18ada Updated assets and version for v0.18.2 release 2017-10-01 18:12:59 +01:00
Dan Brown
a08d80e1cc Merge branch 'master' into release 2017-10-01 18:12:07 +01:00
Dan Brown
6258175922 Updated assets and version for v0.18.1 release 2017-09-20 21:36:17 +01:00
Dan Brown
15736777a0 Merge branch 'master' into release 2017-09-20 21:35:33 +01:00
Dan Brown
75915e8a94 Updated assets for release v0.18 2017-09-10 17:07:57 +01:00
Dan Brown
9bde0ae4ea Merge branch 'master' into release 2017-09-10 17:05:05 +01:00
Dan Brown
0c802d1f86 Updated assets and version for release v0.17.4 2017-07-28 13:04:21 +01:00
Dan Brown
b7a96c6466 Merge branch 'master' into release 2017-07-28 13:03:36 +01:00
Dan Brown
4b645a82c7 Updated version for release 2017-07-22 17:27:01 +01:00
Dan Brown
d599b77b6f Merge branch 'master' into release 2017-07-22 17:26:44 +01:00
Dan Brown
26e93dc8c1 Updated assets and version for release v0.17.2 2017-07-22 16:49:07 +01:00
Dan Brown
a4c9a8491b Merge branch 'master' into release 2017-07-22 16:46:57 +01:00
Dan Brown
70ee636d87 Updated css and version for release 2017-07-10 20:52:32 +01:00
Dan Brown
b35f6dbb03 Merge branch 'master' into release 2017-07-10 20:51:25 +01:00
Dan Brown
67d9e24d8f Merge branch 'master' into release
Also updated assets, Version number
2017-07-02 22:52:26 +01:00
Dan Brown
3903fda6ca Incremented version 2017-06-04 15:38:49 +01:00
Dan Brown
441e46ebaa Merge branch 'v0.16' into release 2017-06-04 15:38:29 +01:00
Dan Brown
1f4260f359 Updated version for release v0.16.2 2017-05-07 19:35:51 +01:00
Dan Brown
dc0bf8ad4e Merge branch 'master' into release 2017-05-07 19:35:34 +01:00
Dan Brown
102e326e6a Updated JS and version for release v0.16.1 2017-04-30 19:51:23 +01:00
Dan Brown
2b25bf6f3b Merge branch 'master' into release 2017-04-30 19:50:29 +01:00
Dan Brown
f93280696d Updated assets for release v0.16 2017-04-23 20:42:28 +01:00
Dan Brown
1787391b07 Merge branch 'master' into release 2017-04-23 20:41:45 +01:00
Dan Brown
a74a8ee483 Updated version for v0.15.3 2017-03-23 22:22:16 +00:00
Dan Brown
7fa5405cb7 Merge branch 'master' into release 2017-03-23 22:21:04 +00:00
Dan Brown
6725ddcc41 Updated version for release v0.15.2 2017-03-05 15:50:52 +00:00
Dan Brown
bce941db3f Merge branch 'master' into release 2017-03-05 15:49:47 +00:00
Dan Brown
6d926048ec Updated to version v0.15.1 2017-02-27 16:59:10 +00:00
Dan Brown
5335c973b4 Merge branch 'master' into release 2017-02-27 16:58:20 +00:00
Dan Brown
15c3e5c96e Updated assets for release v0.15 2017-02-27 14:58:02 +00:00
Dan Brown
a5d5904969 Merge branch 'master' into release 2017-02-27 14:57:38 +00:00
Dan Brown
598758b991 Updated version for v0.14.3 2017-02-05 21:23:27 +00:00
Dan Brown
9926e23bc8 Merge branch 'v0.14' into release 2017-02-05 21:21:54 +00:00
Dan Brown
5d3264bc63 Updated assets for release v0.14.2 2017-02-01 22:27:04 +00:00
Dan Brown
d71f819f95 Merge branch 'v0.14' into release 2017-02-01 22:22:38 +00:00
Dan Brown
ee13509760 Updated version number 2017-01-23 22:28:31 +00:00
Dan Brown
82d7bb1f32 Merge branch 'master' into release 2017-01-23 22:28:02 +00:00
Dan Brown
cdfda508d8 Updated assets for release v0.14 2017-01-22 12:36:10 +00:00
Dan Brown
da941e584f Merge branch 'master' into release ready for v0.14 2017-01-22 12:31:27 +00:00
Dan Brown
65874d7b96 Updated assets for release v0.13.1 2016-11-27 19:42:33 +00:00
Dan Brown
ac9b8f405c Merge fixes from master for release v0.13.1 2016-11-27 19:41:12 +00:00
Dan Brown
8d1419a12e Update assets and version for release v0.13 2016-11-13 12:29:52 +00:00
Dan Brown
04f7a7d301 Merge branch 'master' into release 2016-11-13 12:26:56 +00:00
Dan Brown
c10d2a1493 Updated assets for release v0.12.2 2016-10-30 13:19:19 +00:00
Dan Brown
97bbf79ffd Merge branch 'v0.12' into release 2016-10-30 13:18:23 +00:00
Dan Brown
f7b01ae53d Updated assets for release v0.12.1 2016-09-06 20:50:15 +01:00
Dan Brown
d704e1dbba Merge branch 'master' into release 2016-09-06 20:49:15 +01:00
Dan Brown
ef2ff5e093 Updated assets for release v0.12 2016-09-05 19:49:42 +01:00
Dan Brown
7caed3b0db Merge branch 'master' into release 2016-09-05 19:35:21 +01:00
Dan Brown
45641d0754 Updated assets for release v0.11.2 2016-08-21 14:56:29 +01:00
Dan Brown
4b1d08ba99 Merge branch 'v0.11' into release 2016-08-21 14:55:11 +01:00
Dan Brown
160fa99ba4 Updated assets for release v0.11.1 2016-08-14 12:40:55 +01:00
Dan Brown
d2a5ab49ed Merge branch 'v0.11' into release 2016-08-14 12:37:48 +01:00
Dan Brown
c6404d8917 Updated assets for release v0.11 2016-07-03 10:56:16 +01:00
Dan Brown
7113807f12 Merge branch 'master' into release 2016-07-03 10:52:04 +01:00
Dan Brown
be711215e8 Updated assets for release v0.10 2016-05-22 15:12:47 +01:00
Dan Brown
7e3b404240 Merge branch 'master' into release for version v0.10 2016-05-22 15:11:50 +01:00
Dan Brown
e86901ca20 Updated assets for release v0.9.3 2016-05-03 21:13:02 +01:00
Dan Brown
bdfa61c8b2 Merge branch 'v0.9' into release 2016-05-03 21:11:01 +01:00
Dan Brown
2cc36787f5 Updated assets for release 0.9.2 2016-04-15 19:57:02 +01:00
Dan Brown
448ac61b48 Merge branch 'master' into release 2016-04-15 19:52:59 +01:00
Dan Brown
753f6394f7 Merge branch 'master' into release 2016-04-12 20:09:14 +01:00
Dan Brown
b1faf65934 Updated assets for release 0.9.0 2016-04-09 15:49:02 +01:00
Dan Brown
09f478bd74 Merge branch 'master' into release 2016-04-09 15:47:14 +01:00
Dan Brown
a0497feddd Updated assets for release 0.8.2 2016-03-30 21:44:30 +01:00
Dan Brown
789693bde9 Merge branch 'v0.8' into release 2016-03-30 21:32:46 +01:00
Dan Brown
1fe933e4ea Merge branch 'master' into release 2016-03-13 15:38:06 +00:00
Dan Brown
724b4b5a70 Updated assets for release 0.8.0 2016-03-13 15:15:14 +00:00
Dan Brown
1778a56146 Merge branch 'master' into release 2016-03-13 15:13:23 +00:00
Dan Brown
744865fcb2 Updated assets for release 0.7.6 2016-03-06 13:28:44 +00:00
Dan Brown
7f8c8b448d Merged branch master into release 2016-03-06 13:26:29 +00:00
Dan Brown
a67c53826d Updated assets for release 0.7.5 2016-02-25 21:24:09 +00:00
Dan Brown
14b131e850 Merge branch 'master' into release 2016-02-25 21:23:06 +00:00
Dan Brown
9b55a52b85 Updated assets for release 0.7.4 2016-02-11 22:35:01 +00:00
Dan Brown
db1d10e80f Merge branch 'master' into release 2016-02-11 22:29:29 +00:00
Dan Brown
1be576966f Updated assets for release 0.7.3 2016-02-08 20:47:33 +00:00
Dan Brown
b97e792c5f Merge branch 'master' into release 2016-02-08 20:45:48 +00:00
Dan Brown
8dec674cc3 Merge branch 'master' into release 2016-02-02 07:35:20 +00:00
Dan Brown
f784c03746 Merge branch 'master' into release 2016-02-01 18:31:04 +00:00
Dan Brown
148e172fe8 Updated assets for release 0.7 2016-01-31 18:03:55 +00:00
Dan Brown
56ae86646f Merge branch 'master' into release 2016-01-31 18:01:25 +00:00
Dan Brown
1d2b6fdfa2 Add updated assets 2016-01-02 14:50:59 +00:00
Dan Brown
4fc75beed4 Merge branch 'master' into release 2016-01-02 14:49:05 +00:00
Dan Brown
3b3bc0c4bf Updated compiled assets 2015-12-31 17:26:22 +00:00
Dan Brown
910faab88e Merge branch 'master' into release 2015-12-31 17:22:03 +00:00
Dan Brown
f184d763ad Added build folder to release 2015-12-16 17:53:53 +00:00
Dan Brown
a91d42634d Merge branch 'master' into release 2015-12-16 17:29:34 +00:00
Dan Brown
f517ef3616 Added new asset structure 2015-12-16 17:27:53 +00:00
Dan Brown
e99507ddcf Merge branch 'master' into release 2015-12-16 17:21:21 +00:00
Dan Brown
d2cacf1945 Release update 2015-12-01 21:30:21 +00:00
Dan Brown
448ac1405b Merge branch 'master' into release 2015-12-01 21:15:08 +00:00
Dan Brown
6ad21ce885 Added built assets for release 2015-11-30 21:59:34 +00:00
361 changed files with 18207 additions and 9057 deletions

View File

@@ -1,11 +1,14 @@
# Environment
APP_ENV=production
APP_DEBUG=false
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# The below url has to be set if using social auth options
# or if you are not using BookStack at the root path of your domain.
# APP_URL=http://bookstack.dev
# Application URL
# Remove the hash below and set a URL if using BookStack behind
# a proxy, 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
# Database details
DB_HOST=localhost
@@ -13,84 +16,16 @@ DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Cache and session
CACHE_DRIVER=file
SESSION_DRIVER=file
# If using Memcached, comment the above and uncomment these
#CACHE_DRIVER=memcached
#SESSION_DRIVER=memcached
QUEUE_DRIVER=sync
# A different prefix is useful when multiple BookStack instances use the same caching server
CACHE_PREFIX=bookstack
# Memcached settings
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Storage
STORAGE_TYPE=local
# Amazon S3 Config
STORAGE_S3_KEY=false
STORAGE_S3_SECRET=false
STORAGE_S3_REGION=false
STORAGE_S3_BUCKET=false
# Storage URL
# Used to prefix image urls for when using custom domains/cdns
STORAGE_URL=false
# General auth
AUTH_METHOD=standard
# Social Authentication information. Defaults as off.
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
# Disable default services such as Gravatar and Draw.IO
DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
# LDAP Settings
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
# Do you want to sync LDAP groups to BookStack roles for a user
LDAP_USER_TO_GROUPS=false
# What is the LDAP attribute for group memberships
LDAP_GROUP_ATTRIBUTE="memberOf"
# Would you like to remove users from roles on BookStack if they do not match on LDAP
# If false, the ldap groups-roles sync will only add users to roles
LDAP_REMOVE_FROM_GROUPS=false
# Set this option to disable LDAPS Certificate Verification
LDAP_TLS_INSECURE=false
# Mail settings
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM=null
MAIL_FROM_NAME=null
# A full list of options can be found in the '.env.example.complete' file.

227
.env.example.complete Normal file
View File

@@ -0,0 +1,227 @@
# Full list of environment variables that can be used with BookStack.
# Selectively copy these to your '.env' file as required.
# Each option is shown with it's default value.
# Do not copy this whole file to use as your '.env' file.
# Application environment
# Can be 'production', 'development', 'testing' or 'demo'
APP_ENV=production
# Enable debug mode
# Shows advanced debug information and errors.
# CAN EXPOSE OTHER VARIABLES, LEAVE DISABLED
APP_DEBUG=false
# Application key
# Used for encryption where needed.
# Run `php artisan key:generate` to generate a valid key.
APP_KEY=SomeRandomString
# Application URL
# 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
# Application default language
# The default language choice to show.
# May be overridden by user-preference or visitor browser settings.
APP_LANG=en
# Auto-detect language for public visitors.
# Uses browser-sent headers to infer a language.
# APP_LANG will be used if such a header is not provided.
APP_AUTO_LANG_PUBLIC=true
# Application timezone
# Used where dates are displayed such as on exported content.
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
APP_TIMEZONE=UTC
# Database details
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=database_database
DB_USERNAME=database_username
DB_PASSWORD=database_user_password
# Mail system to use
# Can be 'smtp', 'mail' or 'sendmail'
MAIL_DRIVER=smtp
# Mail sending options
MAIL_FROM=mail@bookstackapp.com
MAIL_FROM_NAME=BookStack
# SMTP mail options
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
# Cache & Session driver to use
# Can be 'file', 'database', 'memcached' or 'redis'
CACHE_DRIVER=file
SESSION_DRIVER=file
# Session configuration
SESSION_LIFETIME=120
SESSION_COOKIE_NAME=bookstack_session
SESSION_SECURE_COOKIE=false
# Cache key prefix
# Can be used to prevent conflicts multiple BookStack instances use the same store.
CACHE_PREFIX=bookstack
# Memcached server configuration
# If using a UNIX socket path for the host, set the port to 0
# This follows the following format: HOST:PORT:WEIGHT
# For multiple servers separate with a comma
MEMCACHED_SERVERS=127.0.0.1:11211:100
# Redis server configuration
# This follows the following format: HOST:PORT:DATABASE
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
# For multiple servers separate with a comma. These will be clustered.
REDIS_SERVERS=127.0.0.1:6379:0
# Queue driver to use
# Queue not really currently used but may be configurable in the future.
# Would advise not to change this for now.
QUEUE_DRIVER=sync
# Storage system to use
# Can be 'local', 'local_secure' or 's3'
STORAGE_TYPE=local
# Amazon S3 storage configuration
STORAGE_S3_KEY=your-s3-key
STORAGE_S3_SECRET=your-s3-secret
STORAGE_S3_BUCKET=s3-bucket-name
STORAGE_S3_REGION=s3-bucket-region
# S3 endpoint to use for storage calls
# Only set this if using a non-Amazon s3-compatible service such as Minio
STORAGE_S3_ENDPOINT=https://my-custom-s3-compatible.service.com:8001
# Storage URL prefix
# Used as a base for any generated image urls.
# An s3-format URL will be generated if not set.
STORAGE_URL=false
# Authentication method to use
# Can be 'standard' or 'ldap'
AUTH_METHOD=standard
# Social authentication configuration
# All disabled by default.
# Refer to https://www.bookstackapp.com/docs/admin/third-party-auth/
AZURE_APP_ID=false
AZURE_APP_SECRET=false
AZURE_TENANT=false
AZURE_AUTO_REGISTER=false
AZURE_AUTO_CONFIRM_EMAIL=false
DISCORD_APP_ID=false
DISCORD_APP_SECRET=false
DISCORD_AUTO_REGISTER=false
DISCORD_AUTO_CONFIRM_EMAIL=false
FACEBOOK_APP_ID=false
FACEBOOK_APP_SECRET=false
FACEBOOK_AUTO_REGISTER=false
FACEBOOK_AUTO_CONFIRM_EMAIL=false
GITHUB_APP_ID=false
GITHUB_APP_SECRET=false
GITHUB_AUTO_REGISTER=false
GITHUB_AUTO_CONFIRM_EMAIL=false
GITLAB_APP_ID=false
GITLAB_APP_SECRET=false
GITLAB_BASE_URI=false
GITLAB_AUTO_REGISTER=false
GITLAB_AUTO_CONFIRM_EMAIL=false
GOOGLE_APP_ID=false
GOOGLE_APP_SECRET=false
GOOGLE_SELECT_ACCOUNT=false
GOOGLE_AUTO_REGISTER=false
GOOGLE_AUTO_CONFIRM_EMAIL=false
OKTA_BASE_URL=false
OKTA_APP_ID=false
OKTA_APP_SECRET=false
OKTA_AUTO_REGISTER=false
OKTA_AUTO_CONFIRM_EMAIL=false
SLACK_APP_ID=false
SLACK_APP_SECRET=false
SLACK_AUTO_REGISTER=false
SLACK_AUTO_CONFIRM_EMAIL=false
TWITCH_APP_ID=false
TWITCH_APP_SECRET=false
TWITCH_AUTO_REGISTER=false
TWITCH_AUTO_CONFIRM_EMAIL=false
TWITTER_APP_ID=false
TWITTER_APP_SECRET=false
TWITTER_AUTO_REGISTER=false
TWITTER_AUTO_CONFIRM_EMAIL=false
# LDAP authentication configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_SERVER=false
LDAP_BASE_DN=false
LDAP_DN=false
LDAP_PASS=false
LDAP_USER_FILTER=false
LDAP_VERSION=false
LDAP_TLS_INSECURE=false
LDAP_EMAIL_ATTRIBUTE=mail
LDAP_DISPLAY_NAME_ATTRIBUTE=cn
LDAP_FOLLOW_REFERRALS=true
# LDAP group sync configuration
# Refer to https://www.bookstackapp.com/docs/admin/ldap-auth/
LDAP_USER_TO_GROUPS=false
LDAP_GROUP_ATTRIBUTE="memberOf"
LDAP_REMOVE_FROM_GROUPS=false
# Disable default third-party services such as Gravatar and Draw.IO
# Service-specific options will override this option
DISABLE_EXTERNAL_SERVICES=false
# Use custom avatar service, Sets fetch URL
# Possible placeholders: ${hash} ${size} ${email}
# If set, Avatars will be fetched regardless of DISABLE_EXTERNAL_SERVICES option.
# Example: AVATAR_URL=https://seccdn.libravatar.org/avatar/${hash}?s=${size}&d=identicon
AVATAR_URL=
# Enable Draw.io integration
DRAWIO=true
# Default item listing view
# Used for public visitors and user's without a preference
# Can be 'list' or 'grid'
APP_VIEWS_BOOKS=list
APP_VIEWS_BOOKSHELVES=grid
# Page revision limit
# Number of page revisions to keep in the system before deleting old revisions.
# If set to 'false' a limit will not be enforced.
REVISION_LIMIT=50
# Allow <script> tags in page content
# Note, if set to 'true' the page editor may still escape scripts.
ALLOW_CONTENT_SCRIPTS=false
# Indicate if robots/crawlers should crawl your instance.
# Can be 'true', 'false' or 'null'.
# The behaviour of the default 'null' option will depend on the 'app-public' admin setting.
# Contents of the robots.txt file can be overridden, making this option obsolete.
ALLOW_ROBOTS=null

6
.gitignore vendored
View File

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

View File

@@ -103,18 +103,22 @@ class ActivityService
* @param int $page
* @return array
*/
public function entityActivity($entity, $count = 20, $page = 0)
public function entityActivity($entity, $count = 20, $page = 1)
{
if ($entity->isA('book')) {
$query = $this->activity->where('book_id', '=', $entity->id);
} else {
$query = $this->activity->where('entity_type', '=', get_class($entity))
$query = $this->activity->where('entity_type', '=', $entity->getMorphClass())
->where('entity_id', '=', $entity->id);
}
$activity = $this->permissionService
->filterRestrictedEntityRelations($query, 'activities', 'entity_id', 'entity_type')
->orderBy('created_at', 'desc')->with(['entity', 'user.avatar'])->skip($count * $page)->take($count)->get();
->orderBy('created_at', 'desc')
->with(['entity', 'user.avatar'])
->skip($count * ($page - 1))
->take($count)
->get();
return $this->filterSimilar($activity);
}

View File

@@ -2,21 +2,26 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Entities\EntityProvider;
use Illuminate\Support\Collection;
class ViewService
{
protected $view;
protected $permissionService;
protected $entityProvider;
/**
* ViewService constructor.
* @param \BookStack\Actions\View $view
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param EntityProvider $entityProvider
*/
public function __construct(View $view, PermissionService $permissionService)
public function __construct(View $view, PermissionService $permissionService, EntityProvider $entityProvider)
{
$this->view = $view;
$this->permissionService = $permissionService;
$this->entityProvider = $entityProvider;
}
/**
@@ -50,23 +55,21 @@ class ViewService
* Get the entities with the most views.
* @param int $count
* @param int $page
* @param Entity|false|array $filterModel
* @param string|array $filterModels
* @param string $action - used for permission checking
* @return
* @return Collection
*/
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
public function getPopular(int $count = 10, int $page = 0, $filterModels = null, string $action = 'view')
{
// TODO - Standardise input filter
$skipCount = $count * $page;
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
$query = $this->permissionService
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
->groupBy('viewable_id', 'viewable_type')
->orderBy('view_count', 'desc');
if ($filterModel && is_array($filterModel)) {
$query->whereIn('viewable_type', $filterModel);
} else if ($filterModel) {
$query->where('viewable_type', '=', $filterModel->getMorphClass());
if ($filterModels) {
$query->whereIn('viewable_type', $this->entityProvider->getMorphClasses($filterModels));
}
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');

View File

@@ -80,24 +80,44 @@ class LdapService
public function getUserDetails($userName)
{
$emailAttr = $this->config['email_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr]);
$displayNameAttr = $this->config['display_name_attribute'];
$user = $this->getUserWithAttributes($userName, ['cn', 'uid', 'dn', $emailAttr, $displayNameAttr]);
if ($user === null) {
return null;
}
$userCn = $this->getUserResponseProperty($user, 'cn', null);
return [
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
'name' => $user['cn'][0],
'uid' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
'dn' => $user['dn'],
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
'email' => $this->getUserResponseProperty($user, $emailAttr, null),
];
}
/**
* Get a property from an LDAP user response fetch.
* Handles properties potentially being part of an array.
* @param array $userDetails
* @param string $propertyKey
* @param $defaultValue
* @return mixed
*/
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
{
if (isset($userDetails[$propertyKey])) {
return (is_array($userDetails[$propertyKey]) ? $userDetails[$propertyKey][0] : $userDetails[$propertyKey]);
}
return $defaultValue;
}
/**
* @param Authenticatable $user
* @param string $username
* @param string $password
* @param string $username
* @param string $password
* @return bool
* @throws LdapException
*/
@@ -162,25 +182,14 @@ class LdapService
throw new LdapException(trans('errors.ldap_extension_not_installed'));
}
// Get port from server string and protocol if specified.
$ldapServer = explode(':', $this->config['server']);
$hasProtocol = preg_match('/^ldaps{0,1}\:\/\//', $this->config['server']) === 1;
if (!$hasProtocol) {
array_unshift($ldapServer, '');
}
$hostName = $ldapServer[0] . ($hasProtocol?':':'') . $ldapServer[1];
$defaultPort = $ldapServer[0] === 'ldaps' ? 636 : 389;
/*
* Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
* the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not
* per handle.
*/
if($this->config['tls_insecure']) {
$this->ldap->setOption(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
// Check if TLS_INSECURE is set. The handle is set to NULL due to the nature of
// the LDAP_OPT_X_TLS_REQUIRE_CERT option. It can only be set globally and not per handle.
if ($this->config['tls_insecure']) {
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
}
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
$serverDetails = $this->parseServerString($this->config['server']);
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
if ($ldapConnection === false) {
throw new LdapException(trans('errors.ldap_cannot_connect'));
@@ -195,6 +204,27 @@ class LdapService
return $this->ldapConnection;
}
/**
* Parse a LDAP server string and return the host and port for
* a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
* @param $serverString
* @return array
*/
protected function parseServerString($serverString)
{
$serverNameParts = explode(':', $serverString);
// If we have a protocol just return the full string since PHP will ignore a separate port.
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
return ['host' => $serverString, 'port' => 389];
}
// Otherwise, extract the port out
$hostName = $serverNameParts[0];
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
return ['host' => $hostName, 'port' => $ldapPort];
}
/**
* Build a filter string by injecting common variables.
* @param string $filterString
@@ -299,10 +329,10 @@ class LdapService
$count = 0;
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
$count = (int)$userGroupSearchResponse[$groupsAttr]['count'];
}
for ($i=0; $i<$count; $i++) {
for ($i = 0; $i < $count; $i++) {
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
if (!in_array($dnComponents[0], $ldapGroups)) {
$ldapGroups[] = $dnComponents[0];

View File

@@ -190,10 +190,10 @@ class PermissionService
{
return $this->entityProvider->book->newQuery()
->select(['id', 'restricted', 'created_by'])->with(['chapters' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
$query->select(['id', 'restricted', 'created_by', 'book_id']);
}, 'pages' => function ($query) {
$query->select(['id', 'restricted', 'created_by', 'book_id', 'chapter_id']);
}]);
}
/**
@@ -556,6 +556,39 @@ class PermissionService
return $q;
}
/**
* Checks if a user has the given permission for any items in the system.
* Can be passed an entity instance to filter on a specific type.
* @param string $permission
* @param string $entityClass
* @return bool
*/
public function checkUserHasPermissionOnAnything(string $permission, string $entityClass = null)
{
$userRoleIds = $this->currentUser()->roles()->select('id')->pluck('id')->toArray();
$userId = $this->currentUser()->id;
$permissionQuery = $this->db->table('joint_permissions')
->where('action', '=', $permission)
->whereIn('role_id', $userRoleIds)
->where(function ($query) use ($userId) {
$query->where('has_permission', '=', 1)
->orWhere(function ($query2) use ($userId) {
$query2->where('has_permission_own', '=', 1)
->where('created_by', '=', $userId);
});
});
if (!is_null($entityClass)) {
$entityInstance = app()->make($entityClass);
$permissionQuery = $permissionQuery->where('entity_type', '=', $entityInstance->getMorphClass());
}
$hasPermission = $permissionQuery->count() > 0;
$this->clean();
return $hasPermission;
}
/**
* Check if an entity has restrictions set on itself or its
* parent tree.
@@ -612,13 +645,13 @@ class PermissionService
$entities = $this->entityProvider;
$pageSelect = $this->db->table('pages')->selectRaw($entities->page->entityRawQuery($fetchPageContent))
->where('book_id', '=', $book_id)->where(function ($query) use ($filterDrafts) {
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$query->where('draft', '=', 0);
if (!$filterDrafts) {
$query->orWhere(function ($query) {
$query->where('draft', '=', 1)->where('created_by', '=', $this->currentUser()->id);
});
}
});
$chapterSelect = $this->db->table('chapters')->selectRaw($entities->chapter->entityRawQuery())->where('book_id', '=', $book_id);
$query = $this->db->query()->select('*')->from($this->db->raw("({$pageSelect->toSql()} UNION {$chapterSelect->toSql()}) AS U"))
->mergeBindings($pageSelect)->mergeBindings($chapterSelect);
@@ -671,7 +704,7 @@ class PermissionService
* @param string $entityIdColumn
* @param string $entityTypeColumn
* @param string $action
* @return mixed
* @return QueryBuilder
*/
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
{
@@ -699,18 +732,21 @@ class PermissionService
}
/**
* Filters pages that are a direct relation to another item.
* Add conditions to a query to filter the selection to related entities
* where permissions are granted.
* @param $entityType
* @param $query
* @param $tableName
* @param $entityIdColumn
* @return mixed
*/
public function filterRelatedPages($query, $tableName, $entityIdColumn)
public function filterRelatedEntity($entityType, $query, $tableName, $entityIdColumn)
{
$this->currentAction = 'view';
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
$pageMorphClass = $this->entityProvider->page->getMorphClass();
$pageMorphClass = $this->entityProvider->get($entityType)->getMorphClass();
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
@@ -728,7 +764,9 @@ class PermissionService
});
})->orWhere($tableDetails['entityIdColumn'], '=', 0);
});
$this->clean();
return $q;
}

View File

@@ -1,6 +1,7 @@
<?php namespace BookStack\Auth;
use BookStack\Auth\Permissions\JointPermission;
use BookStack\Auth\Permissions\RolePermission;
use BookStack\Model;
class Role extends Model
@@ -13,7 +14,7 @@ class Role extends Model
*/
public function users()
{
return $this->belongsToMany(User::class);
return $this->belongsToMany(User::class)->orderBy('name', 'asc');
}
/**
@@ -30,7 +31,7 @@ class Role extends Model
*/
public function permissions()
{
return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id');
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
}
/**
@@ -51,18 +52,18 @@ class Role extends Model
/**
* Add a permission to this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function attachPermission(Permissions\RolePermission $permission)
public function attachPermission(RolePermission $permission)
{
$this->permissions()->attach($permission->id);
}
/**
* Detach a single permission from this role.
* @param \BookStack\Auth\Permissions\RolePermission $permission
* @param RolePermission $permission
*/
public function detachPermission(Permissions\RolePermission $permission)
public function detachPermission(RolePermission $permission)
{
$this->permissions()->detach($permission->id);
}

View File

@@ -24,7 +24,7 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
* The attributes that are mass assignable.
* @var array
*/
protected $fillable = ['name', 'email', 'image_id'];
protected $fillable = ['name', 'email'];
/**
* The attributes excluded from the model's JSON form.
@@ -216,12 +216,12 @@ class User extends Model implements AuthenticatableContract, CanResetPasswordCon
*/
public function getShortName($chars = 8)
{
if (strlen($this->name) <= $chars) {
if (mb_strlen($this->name) <= $chars) {
return $this->name;
}
$splitName = explode(' ', $this->name);
if (strlen($splitName[0]) <= $chars) {
if (mb_strlen($splitName[0]) <= $chars) {
return $splitName[0];
}

View File

@@ -6,6 +6,7 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\Image;
use Exception;
use Illuminate\Database\Eloquent\Builder;
use Images;
class UserRepo
@@ -48,7 +49,7 @@ class UserRepo
/**
* Get all the users with their permissions.
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsers()
{
@@ -59,7 +60,7 @@ class UserRepo
* Get all the users with their permissions in a paginated format.
* @param int $count
* @param $sortData
* @return \Illuminate\Database\Eloquent\Builder|static
* @return Builder|static
*/
public function getAllUsersPaginatedAndSorted($count, $sortData)
{
@@ -197,7 +198,7 @@ class UserRepo
$user->delete();
// Delete user profile images
$profileImages = $images = Image::where('type', '=', 'user')->where('created_by', '=', $user->id)->get();
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
foreach ($profileImages as $image) {
Images::destroy($image);
}
@@ -223,16 +224,15 @@ class UserRepo
*/
public function getRecentlyCreated(User $user, $count = 20)
{
$createdByUserQuery = function (Builder $query) use ($user) {
$query->where('created_by', '=', $user->id);
};
return [
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
}),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, function ($query) use ($user) {
$query->where('created_by', '=', $user->id);
})
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
];
}
@@ -247,6 +247,7 @@ class UserRepo
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
];
}
@@ -256,7 +257,7 @@ class UserRepo
*/
public function getAllRoles()
{
return $this->role->all();
return $this->role->newQuery()->orderBy('name', 'asc')->get();
}
/**

View File

@@ -49,7 +49,7 @@ class CreateAdmin extends Command
if (empty($email)) {
$email = $this->ask('Please specify an email address for the new admin user');
}
if (strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
return $this->error('Invalid email address provided');
}
@@ -61,7 +61,7 @@ class CreateAdmin extends Command
if (empty($name)) {
$name = $this->ask('Please specify an name for the new admin user');
}
if (strlen($name) < 2) {
if (mb_strlen($name) < 2) {
return $this->error('Invalid name provided');
}
@@ -69,7 +69,7 @@ class CreateAdmin extends Command
if (empty($password)) {
$password = $this->secret('Please specify a password for the new admin user');
}
if (strlen($password) < 5) {
if (mb_strlen($password) < 5) {
return $this->error('Invalid password provided, Must be at least 5 characters');
}

View File

@@ -38,7 +38,7 @@ class Book extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@@ -69,6 +69,15 @@ class Book extends Entity
return $this->hasMany(Page::class);
}
/**
* Get the direct child pages of this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function directPages()
{
return $this->pages()->where('chapter_id', '=', '0');
}
/**
* Get all chapters within this book.
* @return \Illuminate\Database\Eloquent\Relations\HasMany
@@ -92,10 +101,10 @@ class Book extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**

View File

@@ -26,7 +26,9 @@ class Bookshelf extends Entity
*/
public function books()
{
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')
->withPivot('order')
->orderBy('order', 'asc');
}
/**
@@ -50,7 +52,8 @@ class Bookshelf extends Entity
*/
public function getBookCover($width = 440, $height = 250)
{
$default = baseUrl('/book_default_cover.png');
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
if (!$this->image_id) {
return $default;
}
@@ -64,7 +67,7 @@ class Bookshelf extends Entity
}
/**
* Get the cover image of the book
* Get the cover image of the shelf
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function cover()
@@ -77,10 +80,10 @@ class Bookshelf extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**
@@ -91,4 +94,14 @@ class Bookshelf extends Entity
{
return "'BookStack\\\\BookShelf' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text,'' as html, '0' as book_id, '0' as priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this shelf contains the given book.
* @param Book $book
* @return bool
*/
public function contains(Book $book)
{
return $this->books()->where('id', '=', $book->id)->count() > 0;
}
}

View File

@@ -0,0 +1,34 @@
<?php namespace BookStack\Entities;
use Illuminate\View\View;
class BreadcrumbsViewComposer
{
protected $entityContextManager;
/**
* BreadcrumbsViewComposer constructor.
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityContextManager $entityContextManager)
{
$this->entityContextManager = $entityContextManager;
}
/**
* Modify data when the view is composed.
* @param View $view
*/
public function compose(View $view)
{
$crumbs = $view->getData()['crumbs'];
if (array_first($crumbs) instanceof Book) {
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
if ($shelf) {
array_unshift($crumbs, $shelf);
$view->with('crumbs', $crumbs);
}
}
}
}

View File

@@ -53,10 +53,10 @@ class Chapter extends Entity
* @param int $length
* @return string
*/
public function getExcerpt($length = 100)
public function getExcerpt(int $length = 100)
{
$description = $this->description;
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
$description = $this->text ?? $this->description;
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
}
/**
@@ -67,4 +67,13 @@ class Chapter extends Entity
{
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
}
/**
* Check if this chapter has any child pages.
* @return bool
*/
public function hasChildren()
{
return count($this->pages) > 0;
}
}

View File

@@ -102,6 +102,11 @@ class Entity extends Ownable
return $this->morphMany(View::class, 'viewable');
}
public function viewCountQuery()
{
return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
}
/**
* Get the Tag models that have been user assigned to this entity.
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
@@ -218,6 +223,20 @@ class Entity extends Ownable
return $this->{$this->textField};
}
/**
* Get an excerpt of this entity's descriptive content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt(int $length = 100)
{
$text = $this->getText();
if (mb_strlen($text) > $length) {
$text = mb_substr($text, 0, $length-3) . '...';
}
return trim($text);
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @return string

View File

@@ -0,0 +1,60 @@
<?php namespace BookStack\Entities;
use BookStack\Entities\Repos\EntityRepo;
use Illuminate\Session\Store;
class EntityContextManager
{
protected $session;
protected $entityRepo;
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
/**
* EntityContextManager constructor.
* @param Store $session
* @param EntityRepo $entityRepo
*/
public function __construct(Store $session, EntityRepo $entityRepo)
{
$this->session = $session;
$this->entityRepo = $entityRepo;
}
/**
* Get the current bookshelf context for the given book.
* @param Book $book
* @return Bookshelf|null
*/
public function getContextualShelfForBook(Book $book)
{
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
if (is_int($contextBookshelfId)) {
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
if ($shelf && $shelf->contains($book)) {
return $shelf;
}
}
return null;
}
/**
* Store the current contextual shelf ID.
* @param int $shelfId
*/
public function setShelfContext(int $shelfId)
{
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
}
/**
* Clear the session stored shelf context id.
*/
public function clearShelfContext()
{
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
}
}

View File

@@ -85,5 +85,22 @@ class EntityProvider
return $this->all()[$type];
}
/**
* Get the morph classes, as an array, for a single or multiple types.
* @param string|array $types
* @return array<string>
*/
public function getMorphClasses($types)
{
if (is_string($types)) {
$types = [$types];
}
}
$morphClasses = [];
foreach ($types as $type) {
$model = $this->get($type);
$morphClasses[] = $model->getMorphClass();
}
return $morphClasses;
}
}

View File

@@ -102,17 +102,6 @@ class Page extends Entity
return baseUrl('/books/' . urlencode($bookSlug) . $midText . $idComponent);
}
/**
* Get an excerpt of this page's content to the specified length.
* @param int $length
* @return mixed
*/
public function getExcerpt($length = 100)
{
$text = strlen($this->text) > $length ? substr($this->text, 0, $length-3) . '...' : $this->text;
return mb_convert_encoding($text, 'UTF-8');
}
/**
* Return a generalised, common raw query that can be 'unioned' across entities.
* @param bool $withContent

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Entities\Repos;
use Activity;
use BookStack\Actions\TagRepo;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
@@ -15,8 +16,13 @@ use BookStack\Exceptions\NotFoundException;
use BookStack\Exceptions\NotifyException;
use BookStack\Uploads\AttachmentService;
use DOMDocument;
use DOMNode;
use DOMXPath;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Throwable;
class EntityRepo
{
@@ -101,7 +107,7 @@ class EntityRepo
* @param integer $id
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
{
@@ -119,7 +125,7 @@ class EntityRepo
* @param []int $ids
* @param bool $allowDrafts
* @param bool $ignorePermissions
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
* @return Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
*/
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
{
@@ -137,7 +143,7 @@ class EntityRepo
* @param string $type
* @param string $slug
* @param string|bool $bookSlug
* @return \BookStack\Entities\Entity
* @return Entity
* @throws NotFoundException
*/
public function getBySlug($type, $slug, $bookSlug = false)
@@ -179,11 +185,38 @@ class EntityRepo
* Get all entities in a paginated format
* @param $type
* @param int $count
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
* @param string $sort
* @param string $order
* @param null|callable $queryAddition
* @return LengthAwarePaginator
*/
public function getAllPaginated($type, $count = 10)
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
{
return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
$query = $this->entityQuery($type);
$query = $this->addSortToQuery($query, $sort, $order);
if ($queryAddition) {
$queryAddition($query);
}
return $query->paginate($count);
}
/**
* Add sorting operations to an entity query.
* @param Builder $query
* @param string $sort
* @param string $order
* @return Builder
*/
protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
{
$order = ($order === 'asc') ? 'asc' : 'desc';
$propertySorts = ['name', 'created_at', 'updated_at'];
if (in_array($sort, $propertySorts)) {
return $query->orderBy($sort, $order);
}
return $query;
}
/**
@@ -265,15 +298,14 @@ class EntityRepo
/**
* Get the most popular entities base on all views.
* @param string|bool $type
* @param string $type
* @param int $count
* @param int $page
* @return mixed
*/
public function getPopular($type, $count = 10, $page = 0)
public function getPopular(string $type, int $count = 10, int $page = 0)
{
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
return $this->viewService->getPopular($count, $page, $filter);
return $this->viewService->getPopular($count, $page, $type);
}
/**
@@ -305,7 +337,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param \BookStack\Entities\Bookshelf $bookshelf
* @param Bookshelf $bookshelf
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getBookshelfChildren(Bookshelf $bookshelf)
@@ -313,11 +345,23 @@ class EntityRepo
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
}
/**
* Get the direct children of a book.
* @param Book $book
* @return \Illuminate\Database\Eloquent\Collection
*/
public function getBookDirectChildren(Book $book)
{
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
}
/**
* Get all child objects of a book.
* Returns a sorted collection of Pages and Chapters.
* Loads the book slug onto child elements to prevent access database access for getting the slug.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @param bool $filterDrafts
* @param bool $renderPages
* @return mixed
@@ -367,7 +411,7 @@ class EntityRepo
/**
* Get the child items for a chapter sorted by priority but
* with draft items floated to the top.
* @param \BookStack\Entities\Chapter $chapter
* @param Chapter $chapter
* @return \Illuminate\Database\Eloquent\Collection|static[]
*/
public function getChapterChildren(Chapter $chapter)
@@ -379,7 +423,7 @@ class EntityRepo
/**
* Get the next sequential priority for a new child element in the given book.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @return int
*/
public function getNewBookPriority(Book $book)
@@ -390,7 +434,7 @@ class EntityRepo
/**
* Get a new priority for a new page to be added to the given chapter.
* @param \BookStack\Entities\Chapter $chapter
* @param Chapter $chapter
* @return int
*/
public function getNewChapterPriority(Chapter $chapter)
@@ -439,8 +483,8 @@ class EntityRepo
/**
* Updates entity restrictions from a request
* @param Request $request
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
* @param Entity $entity
* @throws Throwable
*/
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
{
@@ -470,7 +514,7 @@ class EntityRepo
* @param string $type
* @param array $input
* @param bool|Book $book
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function createFromInput($type, $input = [], $book = false)
{
@@ -494,9 +538,9 @@ class EntityRepo
* Update entity details from request input.
* Used for books and chapters
* @param string $type
* @param \BookStack\Entities\Entity $entityModel
* @param Entity $entityModel
* @param array $input
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function updateFromInput($type, Entity $entityModel, $input = [])
{
@@ -519,7 +563,7 @@ class EntityRepo
/**
* Sync the books assigned to a shelf from a comma-separated list
* of book IDs.
* @param \BookStack\Entities\Bookshelf $shelf
* @param Bookshelf $shelf
* @param string $books
*/
public function updateShelfBooks(Bookshelf $shelf, string $books)
@@ -538,13 +582,28 @@ class EntityRepo
$shelf->books()->sync($syncData);
}
/**
* Append a Book to a BookShelf.
* @param Bookshelf $shelf
* @param Book $book
*/
public function appendBookToShelf(Bookshelf $shelf, Book $book)
{
if ($shelf->contains($book)) {
return;
}
$maxOrder = $shelf->books()->max('order');
$shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
}
/**
* Change the book that an entity belongs to.
* @param string $type
* @param integer $newBookId
* @param Entity $entity
* @param bool $rebuildPermissions
* @return \BookStack\Entities\Entity
* @return Entity
*/
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
{
@@ -599,24 +658,48 @@ class EntityRepo
}
/**
* Render the page for viewing, Parsing and performing features such as page transclusion.
* Render the page for viewing
* @param Page $page
* @param bool $ignorePermissions
* @return mixed|string
* @param bool $blankIncludes
* @return string
*/
public function renderPage(Page $page, $ignorePermissions = false)
public function renderPage(Page $page, bool $blankIncludes = false) : string
{
$content = $page->html;
if (!config('app.allow_content_scripts')) {
$content = $this->escapeScripts($content);
}
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $content, $matches);
if (count($matches[0]) === 0) {
return $content;
if ($blankIncludes) {
$content = $this->blankPageIncludes($content);
} else {
$content = $this->parsePageIncludes($content);
}
return $content;
}
/**
* Remove any page include tags within the given HTML.
* @param string $html
* @return string
*/
protected function blankPageIncludes(string $html) : string
{
return preg_replace("/{{@\s?([0-9].*?)}}/", '', $html);
}
/**
* Parse any include tags "{{@<page_id>#section}}" to be part of the page.
* @param string $html
* @return mixed|string
*/
protected function parsePageIncludes(string $html) : string
{
$matches = [];
preg_match_all("/{{@\s?([0-9].*?)}}/", $html, $matches);
$topLevelTags = ['table', 'ul', 'ol'];
foreach ($matches[1] as $index => $includeId) {
$splitInclude = explode('#', $includeId, 2);
@@ -625,22 +708,23 @@ class EntityRepo
continue;
}
$matchedPage = $this->getById('page', $pageId, false, $ignorePermissions);
$matchedPage = $this->getById('page', $pageId);
if ($matchedPage === null) {
$content = str_replace($matches[0][$index], '', $content);
$html = str_replace($matches[0][$index], '', $html);
continue;
}
if (count($splitInclude) === 1) {
$content = str_replace($matches[0][$index], $matchedPage->html, $content);
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
continue;
}
$doc = new DOMDocument();
libxml_use_internal_errors(true);
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
$matchingElem = $doc->getElementById($splitInclude[1]);
if ($matchingElem === null) {
$content = str_replace($matches[0][$index], '', $content);
$html = str_replace($matches[0][$index], '', $html);
continue;
}
$innerContent = '';
@@ -652,29 +736,49 @@ class EntityRepo
$innerContent .= $doc->saveHTML($childNode);
}
}
$content = str_replace($matches[0][$index], trim($innerContent), $content);
libxml_clear_errors();
$html = str_replace($matches[0][$index], trim($innerContent), $html);
}
return $content;
return $html;
}
/**
* Escape script tags within HTML content.
* @param string $html
* @return mixed
* @return string
*/
protected function escapeScripts(string $html)
protected function escapeScripts(string $html) : string
{
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
$matches = [];
preg_match_all($scriptSearchRegex, $html, $matches);
if (count($matches) === 0) {
if ($html == '') {
return $html;
}
foreach ($matches[0] as $match) {
$html = str_replace($match, htmlentities($match), $html);
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
$xPath = new DOMXPath($doc);
// Remove standard script tags
$scriptElems = $xPath->query('//script');
foreach ($scriptElems as $scriptElem) {
$scriptElem->parentNode->removeChild($scriptElem);
}
// Remove 'on*' attributes
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
foreach ($onAttributes as $attr) {
/** @var \DOMAttr $attr*/
$attrName = $attr->nodeName;
$attr->parentNode->removeAttribute($attrName);
}
$html = '';
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
foreach ($topElems as $child) {
$html .= $doc->saveHTML($child);
}
return $html;
}
@@ -685,7 +789,7 @@ class EntityRepo
*/
public function searchForImage($imageString)
{
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
foreach ($pages as $page) {
$page->url = $page->getUrl();
$page->html = '';
@@ -696,8 +800,8 @@ class EntityRepo
/**
* Destroy a bookshelf instance
* @param \BookStack\Entities\Bookshelf $shelf
* @throws \Throwable
* @param Bookshelf $shelf
* @throws Throwable
*/
public function destroyBookshelf(Bookshelf $shelf)
{
@@ -707,9 +811,9 @@ class EntityRepo
/**
* Destroy the provided book and all its child entities.
* @param \BookStack\Entities\Book $book
* @param Book $book
* @throws NotifyException
* @throws \Throwable
* @throws Throwable
*/
public function destroyBook(Book $book)
{
@@ -725,8 +829,8 @@ class EntityRepo
/**
* Destroy a chapter and its relations.
* @param \BookStack\Entities\Chapter $chapter
* @throws \Throwable
* @param Chapter $chapter
* @throws Throwable
*/
public function destroyChapter(Chapter $chapter)
{
@@ -744,14 +848,17 @@ class EntityRepo
* Destroy a given page along with its dependencies.
* @param Page $page
* @throws NotifyException
* @throws \Throwable
* @throws Throwable
*/
public function destroyPage(Page $page)
{
// Check if set as custom homepage
// Check if set as custom homepage & remove setting if not used or throw error if active
$customHome = setting('app-homepage', '0:');
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
if (setting('app-homepage-type') === 'page') {
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
}
setting()->remove('app-homepage');
}
$this->destroyEntityCommonRelations($page);
@@ -767,12 +874,12 @@ class EntityRepo
/**
* Destroy or handle the common relations connected to an entity.
* @param \BookStack\Entities\Entity $entity
* @throws \Throwable
* @param Entity $entity
* @throws Throwable
*/
protected function destroyEntityCommonRelations(Entity $entity)
{
\Activity::removeEntity($entity);
Activity::removeEntity($entity);
$entity->views()->delete();
$entity->permissions()->delete();
$entity->tags()->delete();
@@ -784,9 +891,9 @@ class EntityRepo
/**
* Copy the permissions of a bookshelf to all child books.
* Returns the number of books that had permissions updated.
* @param \BookStack\Entities\Bookshelf $bookshelf
* @param Bookshelf $bookshelf
* @return int
* @throws \Throwable
* @throws Throwable
*/
public function copyBookshelfPermissions(Bookshelf $bookshelf)
{

View File

@@ -7,6 +7,7 @@ use BookStack\Entities\Page;
use BookStack\Entities\PageRevision;
use Carbon\Carbon;
use DOMDocument;
use DOMElement;
use DOMXPath;
class PageRepo extends EntityRepo
@@ -129,8 +130,7 @@ class PageRepo extends EntityRepo
}
/**
* Formats a page's html to be tagged correctly
* within the system.
* Formats a page's html to be tagged correctly within the system.
* @param string $htmlText
* @return string
*/
@@ -139,6 +139,7 @@ class PageRepo extends EntityRepo
if ($htmlText == '') {
return $htmlText;
}
libxml_use_internal_errors(true);
$doc = new DOMDocument();
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
@@ -147,37 +148,17 @@ class PageRepo extends EntityRepo
$body = $container->childNodes->item(0);
$childNodes = $body->childNodes;
// Ensure no duplicate ids are used
$idArray = [];
// Set ids on top-level nodes
$idMap = [];
foreach ($childNodes as $index => $childNode) {
/** @var \DOMElement $childNode */
if (get_class($childNode) !== 'DOMElement') {
continue;
}
$this->setUniqueId($childNode, $idMap);
}
// Overwrite id if not a BookStack custom id
if ($childNode->hasAttribute('id')) {
$id = $childNode->getAttribute('id');
if (strpos($id, 'bkmrk') === 0 && array_search($id, $idArray) === false) {
$idArray[] = $id;
continue;
};
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . substr(strtolower(preg_replace('/\s+/', '-', trim($childNode->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (in_array($newId, $idArray)) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$childNode->setAttribute('id', $newId);
$idArray[] = $newId;
// Ensure no duplicate ids within child items
$xPath = new DOMXPath($doc);
$idElems = $xPath->query('//body//*//*[@id]');
foreach ($idElems as $domElem) {
$this->setUniqueId($domElem, $idMap);
}
// Generate inner html as a string
@@ -189,14 +170,49 @@ class PageRepo extends EntityRepo
return $html;
}
/**
* Set a unique id on the given DOMElement.
* A map for existing ID's should be passed in to check for current existence.
* @param DOMElement $element
* @param array $idMap
*/
protected function setUniqueId($element, array &$idMap)
{
if (get_class($element) !== 'DOMElement') {
return;
}
// Overwrite id if not a BookStack custom id
$existingId = $element->getAttribute('id');
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
$idMap[$existingId] = true;
return;
}
// Create an unique id for the element
// Uses the content as a basis to ensure output is the same every time
// the same content is passed through.
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
$newId = urlencode($contentId);
$loopIndex = 0;
while (isset($idMap[$newId])) {
$newId = urlencode($contentId . '-' . $loopIndex);
$loopIndex++;
}
$element->setAttribute('id', $newId);
$idMap[$newId] = true;
}
/**
* Get the plain text version of a page's content.
* @param \BookStack\Entities\Page $page
* @return string
*/
public function pageToPlainText(Page $page)
protected function pageToPlainText(Page $page) : string
{
$html = $this->renderPage($page);
$html = $this->renderPage($page, true);
return strip_tags($html);
}
@@ -406,25 +422,29 @@ class PageRepo extends EntityRepo
return [];
}
$tree = collect([]);
foreach ($headers as $header) {
$text = $header->nodeValue;
$tree->push([
$tree = collect($headers)->map(function($header) {
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
if (mb_strlen($text) > 30) {
$text = mb_substr($text, 0, 27) . '...';
}
return [
'nodeName' => strtolower($header->nodeName),
'level' => intval(str_replace('h', '', $header->nodeName)),
'link' => '#' . $header->getAttribute('id'),
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
]);
}
'text' => $text,
];
})->filter(function($header) {
return mb_strlen($header['text']) > 0;
});
// Normalise headers if only smaller headers have been used
if (count($tree) > 0) {
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
}
$minLevel = $tree->pluck('level')->min();
$tree = $tree->map(function ($header) use ($minLevel) {
$header['level'] -= ($minLevel - 2);
return $header;
});
return $tree->toArray();
}
@@ -505,4 +525,4 @@ class PageRepo extends EntityRepo
return $this->publishPageDraft($copyPage, $pageData);
}
}
}

View File

@@ -2,4 +2,6 @@
use Exception;
class HttpFetchException extends Exception {}
class HttpFetchException extends Exception
{
}

View File

@@ -1,3 +1,5 @@
<?php namespace BookStack\Exceptions;
class UserUpdateException extends NotifyException {}
class UserUpdateException extends NotifyException
{
}

View File

@@ -128,7 +128,7 @@ class LoginController extends Controller
]);
}
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
}
/**

View File

@@ -70,7 +70,7 @@ class RegisterController extends Controller
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|max:255',
'name' => 'required|min:2|max:255',
'email' => 'required|email|max:255|unique:users',
'password' => 'required|min:6',
]);
@@ -142,7 +142,7 @@ class RegisterController extends Controller
if ($registrationRestrict) {
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
}
@@ -176,7 +176,7 @@ class RegisterController extends Controller
*/
public function getRegisterConfirmation()
{
return view('auth/register-confirm');
return view('auth.register-confirm');
}
/**
@@ -204,7 +204,7 @@ class RegisterController extends Controller
*/
public function showAwaitingConfirmation()
{
return view('auth/user-unconfirmed');
return view('auth.user-unconfirmed');
}
/**

View File

@@ -3,8 +3,10 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Book;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -15,18 +17,29 @@ class BookController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
* @param EntityRepo $entityRepo
* @param \BookStack\Auth\UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param UserRepo $userRepo
* @param ExportService $exportService
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
{
public function __construct(
EntityRepo $entityRepo,
UserRepo $userRepo,
ExportService $exportService,
EntityContextManager $entityContextManager,
ImageRepo $imageRepo
) {
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -36,67 +49,117 @@ class BookController extends Controller
*/
public function index()
{
$books = $this->entityRepo->getAllPaginated('book', 18);
$view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
$sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
$order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
$popular = $this->entityRepo->getPopular('book', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.books'));
return view('books/index', [
return view('books.index', [
'books' => $books,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'booksViewType' => $booksViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
/**
* Show the form for creating a new book.
* @param string $shelfSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function create()
public function create(string $shelfSlug = null)
{
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$this->checkPermission('book-create-all');
$this->setPageTitle(trans('entities.books_create'));
return view('books/create');
return view('books.create', [
'bookshelf' => $bookshelf
]);
}
/**
* Store a newly created book in storage.
*
* @param Request $request
* @param Request $request
* @param string $shelfSlug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
public function store(Request $request, string $shelfSlug = null)
{
$this->checkPermission('book-create-all');
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$bookshelf = null;
if ($shelfSlug !== null) {
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
}
$book = $this->entityRepo->createFromInput('book', $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_create', $book->id);
if ($bookshelf) {
$this->entityRepo->appendBookToShelf($bookshelf, $book);
Activity::add($bookshelf, 'bookshelf_update');
}
return redirect($book->getUrl());
}
/**
* Display the specified book.
* @param $slug
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
*/
public function show($slug)
public function show($slug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-view', $book);
$bookChildren = $this->entityRepo->getBookChildren($book);
Views::add($book);
if ($request->has('shelf')) {
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
}
$this->setPageTitle($book->getShortName());
return view('books/show', [
return view('books.show', [
'book' => $book,
'current' => $book,
'bookChildren' => $bookChildren,
'activity' => Activity::entityActivity($book, 20, 0)
'activity' => Activity::entityActivity($book, 20, 1)
]);
}
@@ -110,25 +173,32 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
return view('books/edit', ['book' => $book, 'current' => $book]);
return view('books.edit', ['book' => $book, 'current' => $book]);
}
/**
* Update the specified book in storage.
* @param Request $request
* @param Request $request
* @param $slug
* @return Response
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \BookStack\Exceptions\NotFoundException
*/
public function update(Request $request, $slug)
public function update(Request $request, string $slug)
{
$book = $this->entityRepo->getBySlug('book', $slug);
$this->checkOwnablePermission('book-update', $book);
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000'
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
$this->bookUpdateActions($book, $request);
Activity::add($book, 'book_update', $book->id);
return redirect($book->getUrl());
}
@@ -142,22 +212,24 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
return view('books/delete', ['book' => $book, 'current' => $book]);
return view('books.delete', ['book' => $book, 'current' => $book]);
}
/**
* Shows the view which allows pages to be re-ordered and sorted.
* @param string $bookSlug
* @return \Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function sort($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-update', $book);
$bookChildren = $this->entityRepo->getBookChildren($book, true);
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
}
/**
@@ -170,7 +242,7 @@ class BookController extends Controller
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$bookChildren = $this->entityRepo->getBookChildren($book);
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
}
/**
@@ -254,7 +326,12 @@ class BookController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('book-delete', $book);
Activity::addMessage('book_delete', 0, $book->name);
if ($book->cover) {
$this->imageRepo->destroyImage($book->cover);
}
$this->entityRepo->destroyBook($book);
return redirect('/books');
}
@@ -263,12 +340,12 @@ class BookController extends Controller
* @param $bookSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug)
public function showPermissions($bookSlug)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
$roles = $this->userRepo->getRestrictableRoles();
return view('books/restrictions', [
return view('books.permissions', [
'book' => $book,
'roles' => $roles
]);
@@ -277,11 +354,12 @@ class BookController extends Controller
/**
* Set the restrictions for this book.
* @param $bookSlug
* @param $bookSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, Request $request)
public function permissions($bookSlug, Request $request)
{
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $book);
@@ -325,4 +403,29 @@ class BookController extends Controller
$textContent = $this->exportService->bookToPlainText($book);
return $this->downloadResponse($textContent, $bookSlug . '.txt');
}
/**
* Common actions to run on book update.
* Handles updating the cover image.
* @param Book $book
* @param Request $request
* @throws \BookStack\Exceptions\ImageUploadException
*/
protected function bookUpdateActions(Book $book, Request $request)
{
// Update the cover image if in request
if ($request->has('image')) {
$this->imageRepo->destroyImage($book->cover);
$newImage = $request->file('image');
$image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
$book->image_id = $image->id;
$book->save();
}
if ($request->has('image_reset')) {
$this->imageRepo->destroyImage($book->cover);
$book->image_id = 0;
$book->save();
}
}
}

View File

@@ -3,8 +3,9 @@
use Activity;
use BookStack\Auth\UserRepo;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\ExportService;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Views;
@@ -14,19 +15,22 @@ class BookshelfController extends Controller
protected $entityRepo;
protected $userRepo;
protected $exportService;
protected $entityContextManager;
protected $imageRepo;
/**
* BookController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param UserRepo $userRepo
* @param \BookStack\Entities\ExportService $exportService
* @param EntityContextManager $entityContextManager
* @param ImageRepo $imageRepo
*/
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
{
$this->entityRepo = $entityRepo;
$this->userRepo = $userRepo;
$this->exportService = $exportService;
$this->entityContextManager = $entityContextManager;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -36,19 +40,35 @@ class BookshelfController extends Controller
*/
public function index()
{
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
$order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$this->entityContextManager->clearShelfContext();
$this->setPageTitle(trans('entities.shelves'));
return view('shelves/index', [
return view('shelves.index', [
'shelves' => $shelves,
'recents' => $recents,
'popular' => $popular,
'new' => $new,
'shelvesViewType' => $shelvesViewType
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
@@ -61,13 +81,14 @@ class BookshelfController extends Controller
$this->checkPermission('bookshelf-create-all');
$books = $this->entityRepo->getAll('book', false, 'update');
$this->setPageTitle(trans('entities.shelves_create'));
return view('shelves/create', ['books' => $books]);
return view('shelves.create', ['books' => $books]);
}
/**
* Store a newly created bookshelf in storage.
* @param Request $request
* @param Request $request
* @return Response
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function store(Request $request)
{
@@ -75,13 +96,14 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
Activity::add($bookshelf, 'bookshelf_create');
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
$this->shelfUpdateActions($shelf, $request);
return redirect($bookshelf->getUrl());
Activity::add($shelf, 'bookshelf_create');
return redirect($shelf->getUrl());
}
@@ -93,17 +115,20 @@ class BookshelfController extends Controller
*/
public function show(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('book-view', $bookshelf);
/** @var Bookshelf $shelf */
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('book-view', $shelf);
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
Views::add($bookshelf);
$books = $this->entityRepo->getBookshelfChildren($shelf);
Views::add($shelf);
$this->entityContextManager->setShelfContext($shelf->id);
$this->setPageTitle($bookshelf->getShortName());
return view('shelves/show', [
'shelf' => $bookshelf,
$this->setPageTitle($shelf->getShortName());
return view('shelves.show', [
'shelf' => $shelf,
'books' => $books,
'activity' => Activity::entityActivity($bookshelf, 20, 0)
'activity' => Activity::entityActivity($shelf, 20, 1)
]);
}
@@ -115,19 +140,19 @@ class BookshelfController extends Controller
*/
public function edit(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-update', $shelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
$shelfBookIds = $shelfBooks->pluck('id');
$books = $this->entityRepo->getAll('book', false, 'update');
$books = $books->filter(function ($book) use ($shelfBookIds) {
return !$shelfBookIds->contains($book->id);
});
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/edit', [
'shelf' => $bookshelf,
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
return view('shelves.edit', [
'shelf' => $shelf,
'books' => $books,
'shelfBooks' => $shelfBooks,
]);
@@ -136,10 +161,11 @@ class BookshelfController extends Controller
/**
* Update the specified bookshelf in storage.
* @param Request $request
* @param Request $request
* @param string $slug
* @return Response
* @throws \BookStack\Exceptions\NotFoundException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function update(Request $request, string $slug)
{
@@ -148,10 +174,12 @@ class BookshelfController extends Controller
$this->validate($request, [
'name' => 'required|string|max:255',
'description' => 'string|max:1000',
'image' => $this->imageRepo->getImageValidationRules(),
]);
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
$this->shelfUpdateActions($shelf, $request);
Activity::add($shelf, 'bookshelf_update');
return redirect($shelf->getUrl());
@@ -166,11 +194,11 @@ class BookshelfController extends Controller
*/
public function showDelete(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $shelf);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
return view('shelves/delete', ['shelf' => $bookshelf]);
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
return view('shelves.delete', ['shelf' => $shelf]);
}
/**
@@ -182,46 +210,52 @@ class BookshelfController extends Controller
*/
public function destroy(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
Activity::addMessage('bookshelf_delete', 0, $bookshelf->name);
$this->entityRepo->destroyBookshelf($bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
$this->checkOwnablePermission('bookshelf-delete', $shelf);
Activity::addMessage('bookshelf_delete', 0, $shelf->name);
if ($shelf->cover) {
$this->imageRepo->destroyImage($shelf->cover);
}
$this->entityRepo->destroyBookshelf($shelf);
return redirect('/shelves');
}
/**
* Show the Restrictions view.
* @param $slug
* Show the permissions view.
* @param string $slug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict(string $slug)
public function showPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$roles = $this->userRepo->getRestrictableRoles();
return view('shelves.restrictions', [
'shelf' => $bookshelf,
return view('shelves.permissions', [
'shelf' => $shelf,
'roles' => $roles
]);
}
/**
* Set the restrictions for this bookshelf.
* @param $slug
* Set the permissions for this bookshelf.
* @param string $slug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict(string $slug, Request $request)
public function permissions(string $slug, Request $request)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
session()->flash('success', trans('entities.shelves_permissions_updated'));
return redirect($bookshelf->getUrl());
return redirect($shelf->getUrl());
}
/**
@@ -232,11 +266,38 @@ class BookshelfController extends Controller
*/
public function copyPermissions(string $slug)
{
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
$this->checkOwnablePermission('restrictions-manage', $shelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
return redirect($bookshelf->getUrl());
return redirect($shelf->getUrl());
}
/**
* Common actions to run on bookshelf update.
* @param Bookshelf $shelf
* @param Request $request
* @throws \BookStack\Exceptions\ImageUploadException
*/
protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
{
// Update the books that the shelf references
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
// Update the cover image if in request
if ($request->has('image')) {
$newImage = $request->file('image');
$this->imageRepo->destroyImage($shelf->cover);
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
$shelf->image_id = $image->id;
$shelf->save();
}
if ($request->has('image_reset')) {
$this->imageRepo->destroyImage($shelf->cover);
$shelf->image_id = 0;
$shelf->save();
}
}
}

View File

@@ -39,7 +39,7 @@ class ChapterController extends Controller
$book = $this->entityRepo->getBySlug('book', $bookSlug);
$this->checkOwnablePermission('chapter-create', $book);
$this->setPageTitle(trans('entities.chapters_create'));
return view('chapters/create', ['book' => $book, 'current' => $book]);
return view('chapters.create', ['book' => $book, 'current' => $book]);
}
/**
@@ -78,7 +78,7 @@ class ChapterController extends Controller
Views::add($chapter);
$this->setPageTitle($chapter->getShortName());
$pages = $this->entityRepo->getChapterChildren($chapter);
return view('chapters/show', [
return view('chapters.show', [
'book' => $chapter->book,
'chapter' => $chapter,
'current' => $chapter,
@@ -98,7 +98,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -130,7 +130,7 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-delete', $chapter);
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
}
/**
@@ -161,7 +161,8 @@ class ChapterController extends Controller
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
$this->checkOwnablePermission('chapter-update', $chapter);
return view('chapters/move', [
$this->checkOwnablePermission('chapter-delete', $chapter);
return view('chapters.move', [
'chapter' => $chapter,
'book' => $chapter->book
]);
@@ -179,6 +180,7 @@ class ChapterController extends Controller
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('chapter-update', $chapter);
$this->checkOwnablePermission('chapter-delete', $chapter);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -212,13 +214,14 @@ class ChapterController extends Controller
* @param $bookSlug
* @param $chapterSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws \BookStack\Exceptions\NotFoundException
*/
public function showRestrict($bookSlug, $chapterSlug)
public function showPermissions($bookSlug, $chapterSlug)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);
$roles = $this->userRepo->getRestrictableRoles();
return view('chapters/restrictions', [
return view('chapters.permissions', [
'chapter' => $chapter,
'roles' => $roles
]);
@@ -230,8 +233,10 @@ class ChapterController extends Controller
* @param $chapterSlug
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws \BookStack\Exceptions\NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $chapterSlug, Request $request)
public function permissions($bookSlug, $chapterSlug, Request $request)
{
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $chapter);

View File

@@ -54,7 +54,7 @@ class CommentController extends Controller
$this->checkPermission('comment-create-all');
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
Activity::add($page, 'commented_on', $page->book->id);
return view('comments/comment', ['comment' => $comment]);
return view('comments.comment', ['comment' => $comment]);
}
/**
@@ -75,7 +75,7 @@ class CommentController extends Controller
$this->checkOwnablePermission('comment-update', $comment);
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
return view('comments/comment', ['comment' => $comment]);
return view('comments.comment', ['comment' => $comment]);
}
/**

View File

@@ -123,6 +123,20 @@ abstract class Controller extends BaseController
return true;
}
/**
* Check if the current user has a permission or bypass if the provided user
* id matches the current user.
* @param string $permissionName
* @param int $userId
* @return bool
*/
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
{
return $this->checkPermissionOr($permissionName, function () use ($userId) {
return $userId === $this->currentUser->id;
});
}
/**
* Send back a json error message.
* @param string $messageText

View File

@@ -19,7 +19,6 @@ class HomeController extends Controller
parent::__construct();
}
/**
* Display the homepage.
* @return Response
@@ -45,17 +44,39 @@ class HomeController extends Controller
'draftPages' => $draftPages,
];
// Add required list ordering & sorting for books & shelves views.
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
$key = $homepageOption;
$view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
$sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
$order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
$sortOptions = [
'name' => trans('common.sort_name'),
'created_at' => trans('common.sort_created_at'),
'updated_at' => trans('common.sort_updated_at'),
];
$commonData = array_merge($commonData, [
'view' => $view,
'sort' => $sort,
'order' => $order,
'sortOptions' => $sortOptions,
]);
}
if ($homepageOption === 'bookshelves') {
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
$shelvesViewType = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
$data = array_merge($commonData, ['shelves' => $shelves, 'shelvesViewType' => $shelvesViewType]);
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
foreach ($shelves as $shelf) {
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
}
$data = array_merge($commonData, ['shelves' => $shelves]);
return view('common.home-shelves', $data);
}
if ($homepageOption === 'books') {
$books = $this->entityRepo->getAllPaginated('book', 18);
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
$data = array_merge($commonData, ['books' => $books, 'booksViewType' => $booksViewType]);
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
$data = array_merge($commonData, ['books' => $books]);
return view('common.home-book', $data);
}
@@ -105,7 +126,7 @@ class HomeController extends Controller
*/
public function customHeadContent()
{
return view('partials/custom-head-content');
return view('partials.custom-head-content');
}
/**
@@ -120,7 +141,7 @@ class HomeController extends Controller
$allowRobots = $sitePublic;
}
return response()
->view('common/robots', ['allowRobots' => $allowRobots])
->view('common.robots', ['allowRobots' => $allowRobots])
->header('Content-Type', 'text/plain');
}
@@ -129,6 +150,6 @@ class HomeController extends Controller
*/
public function getNotFound()
{
return response()->view('errors/404', [], 404);
return response()->view('errors.404', [], 404);
}
}

View File

@@ -1,247 +0,0 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Get all images for a specific type, Paginated
* @param string $type
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAllByType($type, $page = 0)
{
$imgData = $this->imageRepo->getPaginatedByType($type, $page);
return response()->json($imgData);
}
/**
* Search through images within a particular type.
* @param $type
* @param int $page
* @param Request $request
* @return mixed
*/
public function searchByType(Request $request, $type, $page = 0)
{
$this->validate($request, [
'term' => 'required|string'
]);
$searchTerm = $request->get('term');
$imgData = $this->imageRepo->searchPaginatedByType($type, $searchTerm, $page, 24);
return response()->json($imgData);
}
/**
* Get all images for a user.
* @param int $page
* @return \Illuminate\Http\JsonResponse
*/
public function getAllForUserType($page = 0)
{
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
return response()->json($imgData);
}
/**
* Get gallery images with a specific filter such as book or page
* @param $filter
* @param int $page
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function getGalleryFiltered(Request $request, $filter, $page = 0)
{
$this->validate($request, [
'page_id' => 'required|integer'
]);
$validFilters = collect(['page', 'book']);
if (!$validFilters->contains($filter)) {
return response('Invalid filter', 500);
}
$pageId = $request->get('page_id');
$imgData = $this->imageRepo->getGalleryFiltered(strtolower($filter), $pageId, $page, 24);
return response()->json($imgData);
}
/**
* Handles image uploads for use on pages.
* @param string $type
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function uploadByType($type, Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => 'is_image'
]);
if (!$this->imageRepo->isValidType($type)) {
return $this->jsonError(trans('errors.image_upload_type_error'));
}
$imageUpload = $request->file('file');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Upload a drawing to the system.
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
*/
public function uploadDrawing(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getBase64Image($id)
{
$image = $this->imageRepo->getById($id);
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
/**
* Generate a sized thumbnail for an image.
* @param $id
* @param $width
* @param $height
* @param $crop
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function getThumbnail($id, $width, $height, $crop)
{
$this->checkPermission('image-create-all');
$image = $this->imageRepo->getById($id);
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
return response()->json(['url' => $thumbnailUrl]);
}
/**
* Update image details
* @param integer $imageId
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($imageId, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($imageId);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($id)
{
$image = $this->imageRepo->getById($id);
$this->checkOwnablePermission('image-delete', $image);
$this->imageRepo->destroyImage($image);
return response()->json(trans('components.images_deleted'));
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class DrawioImageController extends Controller
{
protected $imageRepo;
/**
* DrawioImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* 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)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->validate($request, [
'image' => 'required|string',
'uploaded_to' => 'required|integer'
]);
$this->checkPermission('image-create-all');
$imageBase64Data = $request->get('image');
try {
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
/**
* Get the content of an image based64 encoded.
* @param $id
* @return \Illuminate\Http\JsonResponse|mixed
*/
public function getAsBase64($id)
{
$image = $this->imageRepo->getById($id);
$page = $image->getPage();
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
return $this->jsonError("Image data could not be found");
}
$imageData = $this->imageRepo->getImageData($image);
if ($imageData === null) {
return $this->jsonError("Image data could not be found");
}
return response()->json([
'content' => base64_encode($imageData)
]);
}
}

View File

@@ -0,0 +1,64 @@
<?php
namespace BookStack\Http\Controllers\Images;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use BookStack\Http\Controllers\Controller;
class GalleryImageController extends Controller
{
protected $imageRepo;
/**
* GalleryImageController constructor.
* @param ImageRepo $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* 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)
{
$page = $request->get('page', 1);
$searchTerm = $request->get('search', null);
$uploadedToFilter = $request->get('uploaded_to', null);
$parentTypeFilter = $request->get('filter_type', null);
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
return response()->json($imgData);
}
/**
* Store a new gallery image in the system.
* @param Request $request
* @return Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function create(Request $request)
{
$this->checkPermission('image-create-all');
$this->validate($request, [
'file' => $this->imageRepo->getImageValidationRules()
]);
try {
$imageUpload = $request->file('file');
$uploadedTo = $request->get('uploaded_to', 0);
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
} catch (ImageUploadException $e) {
return response($e->getMessage(), 500);
}
return response()->json($image);
}
}

View File

@@ -0,0 +1,115 @@
<?php namespace BookStack\Http\Controllers\Images;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Exceptions\ImageUploadException;
use BookStack\Http\Controllers\Controller;
use BookStack\Repos\PageRepo;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageRepo;
use Illuminate\Filesystem\Filesystem as File;
use Illuminate\Http\Request;
class ImageController extends Controller
{
protected $image;
protected $file;
protected $imageRepo;
/**
* ImageController constructor.
* @param Image $image
* @param File $file
* @param ImageRepo $imageRepo
*/
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
{
$this->image = $image;
$this->file = $file;
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Provide an image file from storage.
* @param string $path
* @return mixed
*/
public function showImage(string $path)
{
$path = storage_path('uploads/images/' . $path);
if (!file_exists($path)) {
abort(404);
}
return response()->file($path);
}
/**
* Update image details
* @param integer $id
* @param Request $request
* @return \Illuminate\Http\JsonResponse
* @throws ImageUploadException
* @throws \Exception
*/
public function update($id, Request $request)
{
$this->validate($request, [
'name' => 'required|min:2|string'
]);
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$this->checkOwnablePermission('image-update', $image);
$image = $this->imageRepo->updateImageDetails($image, $request->all());
return response()->json($image);
}
/**
* Show the usage of an image on pages.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param $id
* @return \Illuminate\Http\JsonResponse
*/
public function usage(EntityRepo $entityRepo, $id)
{
$image = $this->imageRepo->getById($id);
$this->checkImagePermission($image);
$pageSearch = $entityRepo->searchForImage($image->url);
return response()->json($pageSearch);
}
/**
* Deletes an image and all thumbnail/image files
* @param int $id
* @return \Illuminate\Http\JsonResponse
* @throws \Exception
*/
public function destroy($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'));
}
/**
* Check related page permission and ensure type is drawio or gallery.
* @param Image $image
*/
protected function checkImagePermission(Image $image)
{
if ($image->type !== 'drawio' && $image->type !== 'gallery') {
$this->showPermissionError();
}
$relatedPage = $image->getPage();
if ($relatedPage) {
$this->checkOwnablePermission('page-view', $relatedPage);
}
}
}

View File

@@ -61,7 +61,7 @@ class PageController extends Controller
// Otherwise show the edit view if they're a guest
$this->setPageTitle(trans('entities.pages_new'));
return view('pages/guest-create', ['parent' => $parent]);
return view('pages.guest-create', ['parent' => $parent]);
}
/**
@@ -110,7 +110,7 @@ class PageController extends Controller
$this->setPageTitle(trans('entities.pages_edit_draft'));
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $draft,
'book' => $draft->book,
'isDraft' => true,
@@ -184,7 +184,7 @@ class PageController extends Controller
Views::add($page);
$this->setPageTitle($page->getShortName());
return view('pages/show', [
return view('pages.show', [
'page' => $page,'book' => $page->book,
'current' => $page,
'sidebarTree' => $sidebarTree,
@@ -239,7 +239,7 @@ class PageController extends Controller
}
$draftsEnabled = $this->signedIn;
return view('pages/edit', [
return view('pages.edit', [
'page' => $page,
'book' => $page->book,
'current' => $page,
@@ -317,7 +317,7 @@ class PageController extends Controller
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-delete', $page);
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
@@ -333,7 +333,7 @@ class PageController extends Controller
$page = $this->pageRepo->getById('page', $pageId, true);
$this->checkOwnablePermission('page-update', $page);
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
}
/**
@@ -377,12 +377,13 @@ class PageController extends Controller
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\View\View
* @throws NotFoundException
*/
public function showRevisions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
return view('pages.revisions', ['page' => $page, 'current' => $page]);
}
/**
@@ -403,9 +404,10 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => null,
'revision' => $revision
]);
}
@@ -432,7 +434,7 @@ class PageController extends Controller
$page->fill($revision->toArray());
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
return view('pages/revision', [
return view('pages.revision', [
'page' => $page,
'book' => $page->book,
'diff' => $diff,
@@ -482,12 +484,12 @@ class PageController extends Controller
// Check if its the latest revision, cannot delete latest revision.
if (intval($currentRevision->id) === intval($revId)) {
session()->flash('error', trans('entities.revision_cannot_delete_latest'));
return response()->view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
return response()->view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page], 400);
}
$revision->delete();
session()->flash('success', trans('entities.revision_delete_success'));
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
return view('pages.revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
}
/**
@@ -532,49 +534,20 @@ class PageController extends Controller
return $this->downloadResponse($pageText, $pageSlug . '.txt');
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyCreated()
{
$pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
return view('pages/detailed-listing', [
'title' => trans('entities.recently_created_pages'),
'pages' => $pages
]);
}
/**
* Show a listing of recently created pages
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRecentlyUpdated()
{
// TODO - Still exist?
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
return view('pages/detailed-listing', [
return view('pages.detailed-listing', [
'title' => trans('entities.recently_updated_pages'),
'pages' => $pages
]);
}
/**
* Show the Restrictions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
*/
public function showRestrict($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages/restrictions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Show the view to choose a new parent to move a page into.
* @param string $bookSlug
@@ -586,7 +559,8 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
return view('pages/move', [
$this->checkOwnablePermission('page-delete', $page);
return view('pages.move', [
'book' => $page->book,
'page' => $page
]);
@@ -604,6 +578,7 @@ class PageController extends Controller
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-delete', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -641,9 +616,9 @@ class PageController extends Controller
public function showCopy($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
session()->flashInput(['name' => $page->name]);
return view('pages/copy', [
return view('pages.copy', [
'book' => $page->book,
'page' => $page
]);
@@ -660,7 +635,7 @@ class PageController extends Controller
public function copy($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('page-update', $page);
$this->checkOwnablePermission('page-view', $page);
$entitySelection = $request->get('entity_selection', null);
if ($entitySelection === null || $entitySelection === '') {
@@ -688,6 +663,24 @@ class PageController extends Controller
return redirect($pageCopy->getUrl());
}
/**
* Show the Permissions view.
* @param string $bookSlug
* @param string $pageSlug
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @throws NotFoundException
*/
public function showPermissions($bookSlug, $pageSlug)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);
$roles = $this->userRepo->getRestrictableRoles();
return view('pages.permissions', [
'page' => $page,
'roles' => $roles
]);
}
/**
* Set the permissions for this page.
* @param string $bookSlug
@@ -695,8 +688,9 @@ class PageController extends Controller
* @param Request $request
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
* @throws NotFoundException
* @throws \Throwable
*/
public function restrict($bookSlug, $pageSlug, Request $request)
public function permissions($bookSlug, $pageSlug, Request $request)
{
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
$this->checkOwnablePermission('restrictions-manage', $page);

View File

@@ -26,7 +26,7 @@ class PermissionController extends Controller
{
$this->checkPermission('user-roles-manage');
$roles = $this->permissionsRepo->getAllRoles();
return view('settings/roles/index', ['roles' => $roles]);
return view('settings.roles.index', ['roles' => $roles]);
}
/**
@@ -36,7 +36,7 @@ class PermissionController extends Controller
public function createRole()
{
$this->checkPermission('user-roles-manage');
return view('settings/roles/create');
return view('settings.roles.create');
}
/**
@@ -70,7 +70,7 @@ class PermissionController extends Controller
if ($role->hidden) {
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
}
return view('settings/roles/edit', ['role' => $role]);
return view('settings.roles.edit', ['role' => $role]);
}
/**
@@ -106,7 +106,7 @@ class PermissionController extends Controller
$roles = $this->permissionsRepo->getAllRolesExcept($role);
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
$roles->prepend($blankRole);
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
}
/**

View File

@@ -1,34 +1,45 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Actions\ViewService;
use BookStack\Entities\EntityContextManager;
use BookStack\Entities\Repos\EntityRepo;
use BookStack\Entities\SearchService;
use BookStack\Exceptions\NotFoundException;
use Illuminate\Contracts\View\Factory;
use Illuminate\Http\Request;
use Illuminate\View\View;
class SearchController extends Controller
{
protected $entityRepo;
protected $viewService;
protected $searchService;
protected $entityContextManager;
/**
* SearchController constructor.
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
* @param EntityRepo $entityRepo
* @param ViewService $viewService
* @param SearchService $searchService
* @param EntityContextManager $entityContextManager
*/
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
{
public function __construct(
EntityRepo $entityRepo,
ViewService $viewService,
SearchService $searchService,
EntityContextManager $entityContextManager
) {
$this->entityRepo = $entityRepo;
$this->viewService = $viewService;
$this->searchService = $searchService;
$this->entityContextManager = $entityContextManager;
parent::__construct();
}
/**
* Searches all entities.
* @param Request $request
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function search(Request $request)
@@ -41,7 +52,7 @@ class SearchController extends Controller
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
return view('search/all', [
return view('search.all', [
'entities' => $results['results'],
'totalResults' => $results['total'],
'searchTerm' => $searchTerm,
@@ -55,28 +66,28 @@ class SearchController extends Controller
* Searches all entities within a book.
* @param Request $request
* @param integer $bookId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchBook(Request $request, $bookId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchBook($bookId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
* Searches all entities within a chapter.
* @param Request $request
* @param integer $chapterId
* @return \Illuminate\View\View
* @return View
* @internal param string $searchTerm
*/
public function searchChapter(Request $request, $chapterId)
{
$term = $request->get('term', '');
$results = $this->searchService->searchChapter($chapterId, $term);
return view('partials/entity-list', ['entities' => $results]);
return view('partials.entity-list', ['entities' => $results]);
}
/**
@@ -87,21 +98,64 @@ class SearchController extends Controller
*/
public function searchEntitiesAjax(Request $request)
{
$entityTypes = $request->filled('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
$searchTerm = $request->get('term', false);
$permission = $request->get('permission', 'view');
// Search for entities otherwise show most popular
if ($searchTerm !== false) {
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
} else {
$entityNames = $entityTypes->map(function ($type) {
return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
})->toArray();
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
}
return view('search/entity-ajax-list', ['entities' => $entities]);
return view('search.entity-ajax-list', ['entities' => $entities]);
}
/**
* Search siblings items in the system.
* @param Request $request
* @return Factory|View|mixed
*/
public function searchSiblings(Request $request)
{
$type = $request->get('entity_type', null);
$id = $request->get('entity_id', null);
$entity = $this->entityRepo->getById($type, $id);
if (!$entity) {
return $this->jsonError(trans('errors.entity_not_found'), 404);
}
$entities = [];
// Page in chapter
if ($entity->isA('page') && $entity->chapter) {
$entities = $this->entityRepo->getChapterChildren($entity->chapter);
}
// Page in book or chapter
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
}
// Book
// Gets just the books in a shelf if shelf is in context
if ($entity->isA('book')) {
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
if ($contextShelf) {
$entities = $this->entityRepo->getBookshelfChildren($contextShelf);
} else {
$entities = $this->entityRepo->getAll('book');
}
}
// Shelve
if ($entity->isA('bookshelf')) {
$entities = $this->entityRepo->getAll('bookshelf');
}
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
}
}

View File

@@ -1,5 +1,7 @@
<?php namespace BookStack\Http\Controllers;
use BookStack\Auth\User;
use BookStack\Uploads\ImageRepo;
use BookStack\Uploads\ImageService;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -7,6 +9,19 @@ use Setting;
class SettingController extends Controller
{
protected $imageRepo;
/**
* SettingController constructor.
* @param $imageRepo
*/
public function __construct(ImageRepo $imageRepo)
{
$this->imageRepo = $imageRepo;
parent::__construct();
}
/**
* Display a listing of the settings.
* @return Response
@@ -19,7 +34,10 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/index', ['version' => $version]);
return view('settings.index', [
'version' => $version,
'guestUser' => User::getDefault()
]);
}
/**
@@ -31,6 +49,9 @@ class SettingController extends Controller
{
$this->preventAccessForDemoUsers();
$this->checkPermission('settings-manage');
$this->validate($request, [
'app_logo' => $this->imageRepo->getImageValidationRules(),
]);
// Cycles through posted settings and update them
foreach ($request->all() as $name => $value) {
@@ -38,7 +59,21 @@ class SettingController extends Controller
continue;
}
$key = str_replace('setting-', '', trim($name));
Setting::put($key, $value);
setting()->put($key, $value);
}
// Update logo image if set
if ($request->has('app_logo')) {
$logoFile = $request->file('app_logo');
$this->imageRepo->destroyByType('system');
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
setting()->put('app-logo', $image->url);
}
// Clear logo image if requested
if ($request->get('app_logo_reset', null)) {
$this->imageRepo->destroyByType('system');
setting()->remove('app-logo');
}
session()->flash('success', trans('settings.settings_save_success'));
@@ -57,7 +92,7 @@ class SettingController extends Controller
// Get application version
$version = trim(file_get_contents(base_path('version')));
return view('settings/maintenance', ['version' => $version]);
return view('settings.maintenance', ['version' => $version]);
}
/**

View File

@@ -4,6 +4,7 @@ use BookStack\Auth\Access\SocialAuthService;
use BookStack\Auth\User;
use BookStack\Auth\UserRepo;
use BookStack\Exceptions\UserUpdateException;
use BookStack\Uploads\ImageRepo;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
@@ -12,16 +13,19 @@ class UserController extends Controller
protected $user;
protected $userRepo;
protected $imageRepo;
/**
* UserController constructor.
* @param User $user
* @param User $user
* @param UserRepo $userRepo
* @param ImageRepo $imageRepo
*/
public function __construct(User $user, UserRepo $userRepo)
public function __construct(User $user, UserRepo $userRepo, ImageRepo $imageRepo)
{
$this->user = $user;
$this->userRepo = $userRepo;
$this->imageRepo = $imageRepo;
parent::__construct();
}
@@ -41,7 +45,7 @@ class UserController extends Controller
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
$this->setPageTitle(trans('settings.users'));
$users->appends($listDetails);
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
}
/**
@@ -53,7 +57,7 @@ class UserController extends Controller
$this->checkPermission('users-manage');
$authMethod = config('auth.method');
$roles = $this->userRepo->getAllRoles();
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
}
/**
@@ -107,9 +111,7 @@ class UserController extends Controller
*/
public function edit($id, SocialAuthService $socialAuthService)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->user->findOrFail($id);
@@ -118,29 +120,29 @@ class UserController extends Controller
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
$this->setPageTitle(trans('settings.user_profile'));
$roles = $this->userRepo->getAllRoles();
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
return view('users.edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
}
/**
* Update the specified user in storage.
* @param Request $request
* @param int $id
* @param Request $request
* @param int $id
* @return Response
* @throws UserUpdateException
* @throws \BookStack\Exceptions\ImageUploadException
*/
public function update(Request $request, $id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$this->validate($request, [
'name' => 'min:2',
'email' => 'min:2|email|unique:users,email,' . $id,
'password' => 'min:5|required_with:password_confirm',
'password-confirm' => 'same:password|required_with:password',
'setting' => 'array'
'setting' => 'array',
'profile_image' => $this->imageRepo->getImageValidationRules(),
]);
$user = $this->userRepo->getById($id);
@@ -170,10 +172,23 @@ class UserController extends Controller
}
}
// Save profile image if in request
if ($request->has('profile_image')) {
$imageUpload = $request->file('profile_image');
$this->imageRepo->destroyImage($user->avatar);
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
$user->image_id = $image->id;
}
// Delete the profile image if set to
if ($request->has('profile_image_reset')) {
$this->imageRepo->destroyImage($user->avatar);
}
$user->save();
session()->flash('success', trans('settings.users_edit_success'));
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
return redirect($redirectUrl);
}
@@ -184,13 +199,11 @@ class UserController extends Controller
*/
public function delete($id)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
return view('users/delete', ['user' => $user]);
return view('users.delete', ['user' => $user]);
}
/**
@@ -202,9 +215,7 @@ class UserController extends Controller
public function destroy($id)
{
$this->preventAccessForDemoUsers();
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$this->checkPermissionOrCurrentUser('users-manage', $id);
$user = $this->userRepo->getById($id);
@@ -232,10 +243,12 @@ class UserController extends Controller
public function showProfilePage($id)
{
$user = $this->userRepo->getById($id);
$userActivity = $this->userRepo->getActivity($user);
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
$assetCounts = $this->userRepo->getAssetCounts($user);
return view('users/profile', [
return view('users.profile', [
'user' => $user,
'activity' => $userActivity,
'recentlyCreated' => $recentlyCreated,
@@ -251,19 +264,7 @@ class UserController extends Controller
*/
public function switchBookView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $id;
});
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'books_view_type', $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return $this->switchViewType($id, $request, 'books');
}
/**
@@ -274,18 +275,97 @@ class UserController extends Controller
*/
public function switchShelfView($id, Request $request)
{
$this->checkPermissionOr('users-manage', function () use ($id) {
return $this->currentUser->id == $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
*/
protected function switchViewType($userId, Request $request, string $listName)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$viewType = $request->get('view_type');
if (!in_array($viewType, ['grid', 'list'])) {
$viewType = 'list';
}
$user = $this->userRepo->getById($id);
setting()->putUser($user, 'bookshelves_view_type', $viewType);
$user = $this->userRepo->getById($userId);
$key = $listName . '_view_type';
setting()->putUser($user, $key, $viewType);
return redirect()->back(302, [], "/settings/users/$id");
return redirect()->back(302, [], "/settings/users/$userId");
}
/**
* Change the stored sort type for a particular view.
* @param string $id
* @param string $type
* @param Request $request
* @return \Illuminate\Http\RedirectResponse
*/
public function changeSort(string $id, string $type, Request $request)
{
$validSortTypes = ['books', 'bookshelves'];
if (!in_array($type, $validSortTypes)) {
return redirect()->back(500);
}
return $this->changeListSort($id, $request, $type);
}
/**
* Update the stored section expansion preference for the given user.
* @param string $id
* @param string $key
* @param Request $request
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
*/
public function updateExpansionPreference(string $id, string $key, Request $request)
{
$this->checkPermissionOrCurrentUser('users-manage', $id);
$keyWhitelist = ['home-details'];
if (!in_array($key, $keyWhitelist)) {
return response("Invalid key", 500);
}
$newState = $request->get('expand', 'false');
$user = $this->user->findOrFail($id);
setting()->putUser($user, 'section_expansion#' . $key, $newState);
return response("", 204);
}
/**
* 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)
{
$this->checkPermissionOrCurrentUser('users-manage', $userId);
$sort = $request->get('sort');
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
$sort = 'name';
}
$order = $request->get('order');
if (!in_array($order, ['asc', 'desc'])) {
$order = 'asc';
}
$user = $this->user->findOrFail($userId);
$sortKey = $listName . '_sort';
$orderKey = $listName . '_sort_order';
setting()->putUser($user, $sortKey, $sort);
setting()->putUser($user, $orderKey, $order);
return redirect()->back(302, [], "/settings/users/$userId");
}
}

View File

@@ -37,7 +37,7 @@ class Authenticate
}
}
if ($this->auth->guest() && !setting('app-public')) {
if (!hasAppAccess()) {
if ($request->ajax()) {
return response('Unauthorized.', 401);
} else {

View File

@@ -7,8 +7,40 @@ use Illuminate\Http\Request;
class Localization
{
/**
* Array of right-to-left locales
* @var array
*/
protected $rtlLocales = ['ar'];
/**
* Map of BookStack locale names to best-estimate system locale names.
* @var array
*/
protected $localeMap = [
'ar' => 'ar',
'de' => 'de_DE',
'de_informal' => 'de_DE',
'en' => 'en_GB',
'es' => 'es_ES',
'es_AR' => 'es_AR',
'fr' => 'fr_FR',
'it' => 'it_IT',
'ja' => 'ja',
'kr' => 'ko_KR',
'nl' => 'nl_NL',
'pl' => 'pl_PL',
'pt_BR' => 'pt_BR',
'pt_BR' => 'pt_BR',
'ru' => 'ru',
'sk' => 'sk_SK',
'sv' => 'sv_SE',
'uk' => 'uk_UA',
'uk' => 'uk_UA',
'zh_CN' => 'zh_CN',
'zh_TW' => 'zh_TW',
];
/**
* Handle an incoming request.
*
@@ -19,6 +51,7 @@ class Localization
public function handle($request, Closure $next)
{
$defaultLang = config('app.locale');
config()->set('app.default_locale', $defaultLang);
if (user()->isDefault() && config('app.auto_detect_locale')) {
$locale = $this->autoDetectLocale($request, $defaultLang);
@@ -33,6 +66,7 @@ class Localization
app()->setLocale($locale);
Carbon::setLocale($locale);
$this->setSystemDateLocale($locale);
return $next($request);
}
@@ -53,4 +87,18 @@ class Localization
}
return $default;
}
/**
* Set the system date locale for localized date formatting.
* Will try both the standard locale name and the UTF8 variant.
* @param string $locale
*/
protected function setSystemDateLocale(string $locale)
{
$systemLocale = $this->localeMap[$locale] ?? $locale;
$set = setlocale(LC_TIME, $systemLocale);
if ($set === false) {
setlocale(LC_TIME, $systemLocale . '.utf8');
}
}
}

View File

@@ -31,5 +31,4 @@ class MailNotification extends Notification implements ShouldQueue
'text' => 'vendor.notifications.email-plain'
]);
}
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Notifications;
class ResetPassword extends MailNotification
{
/**

View File

@@ -3,11 +3,14 @@
use Blade;
use BookStack\Entities\Book;
use BookStack\Entities\Bookshelf;
use BookStack\Entities\BreadcrumbsViewComposer;
use BookStack\Entities\Chapter;
use BookStack\Entities\Page;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Schema;
use Validator;
@@ -22,9 +25,14 @@ class AppServiceProvider extends ServiceProvider
public function boot()
{
// Custom validation methods
Validator::extend('is_image', function ($attribute, $value, $parameters, $validator) {
$imageMimes = ['image/png', 'image/bmp', 'image/gif', 'image/jpeg', 'image/jpg', 'image/tiff', 'image/webp'];
return in_array($value->getMimeType(), $imageMimes);
Validator::extend('image_extension', function ($attribute, $value, $parameters, $validator) {
$validImageExtensions = ['png', 'jpg', 'jpeg', 'bmp', 'gif', 'tiff', 'webp'];
return in_array(strtolower($value->getClientOriginalExtension()), $validImageExtensions);
});
Validator::extend('no_double_extension', function ($attribute, $value, $parameters, $validator) {
$uploadName = $value->getClientOriginalName();
return substr_count($uploadName, '.') < 2;
});
// Custom blade view directives
@@ -42,6 +50,9 @@ class AppServiceProvider extends ServiceProvider
'BookStack\\Chapter' => Chapter::class,
'BookStack\\Page' => Page::class,
]);
// View Composers
View::composer('partials.breadcrumbs', BreadcrumbsViewComposer::class);
}
/**

View File

@@ -2,20 +2,11 @@
namespace BookStack\Providers;
use BookStack\Actions\Activity;
use BookStack\Actions\ActivityService;
use BookStack\Actions\View;
use BookStack\Actions\ViewService;
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Settings\Setting;
use BookStack\Settings\SettingService;
use BookStack\Uploads\HttpFetcher;
use BookStack\Uploads\Image;
use BookStack\Uploads\ImageService;
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Contracts\Filesystem\Factory;
use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
class CustomFacadeProvider extends ServiceProvider
{
@@ -37,34 +28,19 @@ class CustomFacadeProvider extends ServiceProvider
public function register()
{
$this->app->bind('activity', function () {
return new ActivityService(
$this->app->make(Activity::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ActivityService::class);
});
$this->app->bind('views', function () {
return new ViewService(
$this->app->make(View::class),
$this->app->make(PermissionService::class)
);
return $this->app->make(ViewService::class);
});
$this->app->bind('setting', function () {
return new SettingService(
$this->app->make(Setting::class),
$this->app->make(Repository::class)
);
return $this->app->make(SettingService::class);
});
$this->app->bind('images', function () {
return new ImageService(
$this->app->make(Image::class),
$this->app->make(ImageManager::class),
$this->app->make(Factory::class),
$this->app->make(Repository::class),
$this->app->make(HttpFetcher::class)
);
return $this->app->make(ImageService::class);
});
}
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Providers;
use BookStack\Translation\Translator;
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
@@ -29,4 +28,4 @@ class TranslationServiceProvider extends \Illuminate\Translation\TranslationServ
return $trans;
});
}
}
}

View File

@@ -4,10 +4,8 @@ use Illuminate\Contracts\Cache\Repository as Cache;
/**
* Class SettingService
*
* The settings are a simple key-value database store.
*
* @package BookStack\Services
* For non-authenticated users, user settings are stored via the session instead.
*/
class SettingService
{
@@ -41,6 +39,7 @@ class SettingService
if ($default === false) {
$default = config('setting-defaults.' . $key, false);
}
if (isset($this->localCache[$key])) {
return $this->localCache[$key];
}
@@ -51,6 +50,19 @@ class SettingService
return $formatted;
}
/**
* Get a value from the session instead of the main store option.
* @param $key
* @param bool $default
* @return mixed
*/
protected function getFromSession($key, $default = false)
{
$value = session()->get($key, $default);
$formatted = $this->formatValue($value, $default);
return $formatted;
}
/**
* Get a user-specific setting from the database or cache.
* @param \BookStack\Auth\User $user
@@ -60,9 +72,23 @@ class SettingService
*/
public function getUser($user, $key, $default = false)
{
if ($user->isDefault()) {
return $this->getFromSession($key, $default);
}
return $this->get($this->userKey($user->id, $key), $default);
}
/**
* Get a value for the current logged-in user.
* @param $key
* @param bool $default
* @return bool|string
*/
public function getForCurrentUser($key, $default = false)
{
return $this->getUser(user(), $key, $default);
}
/**
* Gets a setting value from the cache or database.
* Looks at the system defaults if not cached or in database.
@@ -179,6 +205,9 @@ class SettingService
*/
public function putUser($user, $key, $value)
{
if ($user->isDefault()) {
return session()->put($key, $value);
}
return $this->put($this->userKey($user->id, $key), $value);
}

View File

@@ -1,6 +1,5 @@
<?php namespace BookStack\Translation;
class Translator extends \Illuminate\Translation\Translator
{
@@ -70,5 +69,4 @@ class Translator extends \Illuminate\Translation\Translator
{
return $this->baseLocaleMap[$locale] ?? null;
}
}
}

View File

@@ -44,7 +44,7 @@ class AttachmentService extends UploadService
public function saveNewUpload(UploadedFile $uploadedFile, $page_id)
{
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachmentPath = $this->putFileInStorage($uploadedFile);
$largestExistingOrder = Attachment::where('uploaded_to', '=', $page_id)->max('order');
$attachment = Attachment::forceCreate([
@@ -75,7 +75,7 @@ class AttachmentService extends UploadService
}
$attachmentName = $uploadedFile->getClientOriginalName();
$attachmentPath = $this->putFileInStorage($attachmentName, $uploadedFile);
$attachmentPath = $this->putFileInStorage($uploadedFile);
$attachment->name = $attachmentName;
$attachment->path = $attachmentPath;
@@ -174,19 +174,18 @@ class AttachmentService extends UploadService
/**
* Store a file in storage with the given filename
* @param $attachmentName
* @param UploadedFile $uploadedFile
* @return string
* @throws FileUploadException
*/
protected function putFileInStorage($attachmentName, UploadedFile $uploadedFile)
protected function putFileInStorage(UploadedFile $uploadedFile)
{
$attachmentData = file_get_contents($uploadedFile->getRealPath());
$storage = $this->getStorage();
$basePath = 'uploads/files/' . Date('Y-m-M') . '/';
$uploadFileName = $attachmentName;
$uploadFileName = str_random(16) . '.' . $uploadedFile->getClientOriginalExtension();
while ($storage->exists($basePath . $uploadFileName)) {
$uploadFileName = str_random(3) . $uploadFileName;
}

View File

@@ -30,5 +30,4 @@ class HttpFetcher
return $data;
}
}
}

View File

@@ -1,5 +1,6 @@
<?php namespace BookStack\Uploads;
use BookStack\Entities\Page;
use BookStack\Ownable;
use Images;
@@ -20,4 +21,14 @@ class Image extends Ownable
{
return Images::getThumbnail($this, $width, $height, $keepRatio);
}
/**
* Get the page this image has been uploaded to.
* Only applicable to gallery or drawio image types.
* @return Page|null
*/
public function getPage()
{
return $this->belongsTo(Page::class, 'uploaded_to')->first();
}
}

View File

@@ -2,6 +2,7 @@
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Page;
use Illuminate\Database\Eloquent\Builder;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageRepo
@@ -19,8 +20,12 @@ class ImageRepo
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
* @param \BookStack\Entities\Page $page
*/
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
{
public function __construct(
Image $image,
ImageService $imageService,
PermissionService $permissionService,
Page $page
) {
$this->image = $image;
$this->imageService = $imageService;
$this->restrictionService = $permissionService;
@@ -31,7 +36,7 @@ class ImageRepo
/**
* Get an image with the given id.
* @param $id
* @return mixed
* @return Image
*/
public function getById($id)
{
@@ -44,95 +49,113 @@ class ImageRepo
* @param $query
* @param int $page
* @param int $pageSize
* @param bool $filterOnPage
* @return array
*/
private function returnPaginated($query, $page = 0, $pageSize = 24)
private function returnPaginated($query, $page = 1, $pageSize = 24)
{
$images = $this->restrictionService->filterRelatedPages($query, 'images', 'uploaded_to');
$images = $images->orderBy('created_at', 'desc')->skip($pageSize * $page)->take($pageSize + 1)->get();
$images = $query->orderBy('created_at', 'desc')->skip($pageSize * ($page - 1))->take($pageSize + 1)->get();
$hasMore = count($images) > $pageSize;
$returnImages = $images->take(24);
$returnImages = $images->take($pageSize);
$returnImages->each(function ($image) {
$this->loadThumbs($image);
});
return [
'images' => $returnImages,
'hasMore' => $hasMore
'has_more' => $hasMore
];
}
/**
* Gets a load images paginated, filtered by image type.
* Fetch a list of images in a paginated format, filtered by image type.
* Can be filtered by uploaded to and also by name.
* @param string $type
* @param int $page
* @param int $pageSize
* @param bool|int $userFilter
* @param int $uploadedTo
* @param string|null $search
* @param callable|null $whereClause
* @return array
*/
public function getPaginatedByType($type, $page = 0, $pageSize = 24, $userFilter = false)
{
$images = $this->image->where('type', '=', strtolower($type));
public function getPaginatedByType(
string $type,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null,
callable $whereClause = null
) {
$imageQuery = $this->image->newQuery()->where('type', '=', strtolower($type));
if ($userFilter !== false) {
$images = $images->where('created_by', '=', $userFilter);
if ($uploadedTo !== null) {
$imageQuery = $imageQuery->where('uploaded_to', '=', $uploadedTo);
}
return $this->returnPaginated($images, $page, $pageSize);
if ($search !== null) {
$imageQuery = $imageQuery->where('name', 'LIKE', '%' . $search . '%');
}
// Filter by page access
$imageQuery = $this->restrictionService->filterRelatedEntity('page', $imageQuery, 'images', 'uploaded_to');
if ($whereClause !== null) {
$imageQuery = $imageQuery->where($whereClause);
}
return $this->returnPaginated($imageQuery, $page, $pageSize);
}
/**
* Search for images by query, of a particular type.
* Get paginated gallery images within a specific page or book.
* @param string $type
* @param string $filterType
* @param int $page
* @param int $pageSize
* @param string $searchTerm
* @param int|null $uploadedTo
* @param string|null $search
* @return array
*/
public function searchPaginatedByType($type, $searchTerm, $page = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', strtolower($type))->where('name', 'LIKE', '%' . $searchTerm . '%');
return $this->returnPaginated($images, $page, $pageSize);
}
public function getEntityFiltered(
string $type,
string $filterType = null,
int $page = 0,
int $pageSize = 24,
int $uploadedTo = null,
string $search = null
) {
$contextPage = $this->page->findOrFail($uploadedTo);
$parentFilter = null;
/**
* Get gallery images with a particular filter criteria such as
* being within the current book or page.
* @param $filter
* @param $pageId
* @param int $pageNum
* @param int $pageSize
* @return array
*/
public function getGalleryFiltered($filter, $pageId, $pageNum = 0, $pageSize = 24)
{
$images = $this->image->where('type', '=', 'gallery');
$page = $this->page->findOrFail($pageId);
if ($filter === 'page') {
$images = $images->where('uploaded_to', '=', $page->id);
} elseif ($filter === 'book') {
$validPageIds = $page->book->pages->pluck('id')->toArray();
$images = $images->whereIn('uploaded_to', $validPageIds);
if ($filterType === 'book' || $filterType === 'page') {
$parentFilter = function (Builder $query) use ($filterType, $contextPage) {
if ($filterType === 'page') {
$query->where('uploaded_to', '=', $contextPage->id);
} elseif ($filterType === 'book') {
$validPageIds = $contextPage->book->pages()->get(['id'])->pluck('id')->toArray();
$query->whereIn('uploaded_to', $validPageIds);
}
};
}
return $this->returnPaginated($images, $pageNum, $pageSize);
return $this->getPaginatedByType($type, $page, $pageSize, null, $search, $parentFilter);
}
/**
* Save a new image into storage and return the new image.
* @param UploadedFile $uploadFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return Image
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0)
public function saveNew(UploadedFile $uploadFile, $type, $uploadedTo = 0, int $resizeWidth = null, int $resizeHeight = null, bool $keepRatio = true)
{
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo);
$image = $this->imageService->saveNewFromUpload($uploadFile, $type, $uploadedTo, $resizeWidth, $resizeHeight, $keepRatio);
$this->loadThumbs($image);
return $image;
}
@@ -175,12 +198,27 @@ class ImageRepo
* @return bool
* @throws \Exception
*/
public function destroyImage(Image $image)
public function destroyImage(Image $image = null)
{
$this->imageService->destroy($image);
if ($image) {
$this->imageService->destroy($image);
}
return true;
}
/**
* Destroy all images of a certain type.
* @param string $imageType
* @throws \Exception
*/
public function destroyByType(string $imageType)
{
$images = $this->image->where('type', '=', $imageType)->get();
foreach ($images as $image) {
$this->destroyImage($image);
}
}
/**
* Load thumbnails onto an image object.
@@ -191,8 +229,8 @@ class ImageRepo
protected function loadThumbs(Image $image)
{
$image->thumbs = [
'gallery' => $this->getThumbnail($image, 150, 150),
'display' => $this->getThumbnail($image, 840, 0, true)
'gallery' => $this->getThumbnail($image, 150, 150, false),
'display' => $this->getThumbnail($image, 1680, null, true)
];
}
@@ -208,7 +246,7 @@ class ImageRepo
* @throws \BookStack\Exceptions\ImageUploadException
* @throws \Exception
*/
public function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
protected function getThumbnail(Image $image, $width = 220, $height = 220, $keepRatio = false)
{
try {
return $this->imageService->getThumbnail($image, $width, $height, $keepRatio);
@@ -232,13 +270,11 @@ class ImageRepo
}
/**
* Check if the provided image type is valid.
* @param $type
* @return bool
* Get the validation rules for image files.
* @return string
*/
public function isValidType($type)
public function getImageValidationRules()
{
$validTypes = ['gallery', 'cover', 'system', 'user'];
return in_array($type, $validTypes);
return 'image_extension|no_double_extension|mimes:jpeg,png,gif,bmp,webp,tiff';
}
}

View File

@@ -9,6 +9,7 @@ use Illuminate\Contracts\Cache\Repository as Cache;
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
use Intervention\Image\Exception\NotSupportedException;
use Intervention\Image\ImageManager;
use phpDocumentor\Reflection\Types\Integer;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class ImageService extends UploadService
@@ -57,15 +58,29 @@ class ImageService extends UploadService
/**
* Saves a new image from an upload.
* @param UploadedFile $uploadedFile
* @param string $type
* @param string $type
* @param int $uploadedTo
* @param int|null $resizeWidth
* @param int|null $resizeHeight
* @param bool $keepRatio
* @return mixed
* @throws ImageUploadException
*/
public function saveNewFromUpload(UploadedFile $uploadedFile, $type, $uploadedTo = 0)
{
public function saveNewFromUpload(
UploadedFile $uploadedFile,
string $type,
int $uploadedTo = 0,
int $resizeWidth = null,
int $resizeHeight = null,
bool $keepRatio = true
) {
$imageName = $uploadedFile->getClientOriginalName();
$imageData = file_get_contents($uploadedFile->getRealPath());
if ($resizeWidth !== null || $resizeHeight !== null) {
$imageData = $this->resizeImage($imageData, $resizeWidth, $resizeHeight, $keepRatio);
}
return $this->saveNew($imageName, $imageData, $type, $uploadedTo);
}
@@ -122,7 +137,7 @@ class ImageService extends UploadService
$secureUploads = setting('app-secure-images');
$imageName = str_replace(' ', '-', $imageName);
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m-M') . '/';
$imagePath = '/uploads/images/' . $type . '/' . Date('Y-m') . '/';
while ($storage->exists($imagePath . $imageName)) {
$imageName = str_random(3) . $imageName;
@@ -201,8 +216,28 @@ class ImageService extends UploadService
return $this->getPublicUrl($thumbFilePath);
}
$thumbData = $this->resizeImage($storage->get($imagePath), $width, $height, $keepRatio);
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
}
/**
* Resize image data.
* @param string $imageData
* @param int $width
* @param int $height
* @param bool $keepRatio
* @return string
* @throws ImageUploadException
*/
protected function resizeImage(string $imageData, $width = 220, $height = null, bool $keepRatio = true)
{
try {
$thumb = $this->imageTool->make($storage->get($imagePath));
$thumb = $this->imageTool->make($imageData);
} catch (Exception $e) {
if ($e instanceof \ErrorException || $e instanceof NotSupportedException) {
throw new ImageUploadException(trans('errors.cannot_create_thumbs'));
@@ -211,20 +246,14 @@ class ImageService extends UploadService
}
if ($keepRatio) {
$thumb->resize($width, null, function ($constraint) {
$thumb->resize($width, $height, function ($constraint) {
$constraint->aspectRatio();
$constraint->upsize();
});
} else {
$thumb->fit($width, $height);
}
$thumbData = (string)$thumb->encode();
$storage->put($thumbFilePath, $thumbData);
$storage->setVisibility($thumbFilePath, 'public');
$this->cache->put('images-' . $image->id . '-' . $thumbFilePath, $thumbFilePath, 60 * 72);
return $this->getPublicUrl($thumbFilePath);
return (string)$thumb->encode();
}
/**
@@ -306,6 +335,7 @@ class ImageService extends UploadService
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
$image->created_by = $user->id;
$image->updated_by = $user->id;
$image->uploaded_to = $user->id;
$image->save();
return $image;

View File

@@ -1,5 +1,7 @@
<?php
use BookStack\Auth\Permissions\PermissionService;
use BookStack\Entities\Entity;
use BookStack\Ownable;
/**
@@ -41,30 +43,52 @@ function user()
* Check if current user is a signed in user.
* @return bool
*/
function signedInUser()
function signedInUser() : bool
{
return auth()->user() && !auth()->user()->isDefault();
}
/**
* Check if the current user has general access.
* @return bool
*/
function hasAppAccess() : bool
{
return !auth()->guest() || setting('app-public');
}
/**
* Check if the current user has a permission.
* If an ownable element is passed in the jointPermissions are checked against
* that particular item.
* @param $permission
* @param string $permission
* @param Ownable $ownable
* @return mixed
*/
function userCan($permission, Ownable $ownable = null)
function userCan(string $permission, Ownable $ownable = null)
{
if ($ownable === null) {
return user() && user()->can($permission);
}
// Check permission on ownable item
$permissionService = app(\BookStack\Auth\Permissions\PermissionService::class);
$permissionService = app(PermissionService::class);
return $permissionService->checkOwnableUserAccess($ownable, $permission);
}
/**
* Check if the current user has the given permission
* on any item in the system.
* @param string $permission
* @param string|null $entityClass
* @return bool
*/
function userCanOnAny(string $permission, string $entityClass = null)
{
$permissionService = app(PermissionService::class);
return $permissionService->checkUserHasPermissionOnAnything($permission, $entityClass);
}
/**
* Helper to access system settings.
* @param $key
@@ -99,10 +123,11 @@ function baseUrl($path, $forceAppDomain = false)
// Remove non-specified domain if forced and we have a domain
if ($isFullUrl && $forceAppDomain) {
if (!empty($base) && strpos($path, $base) === 0) {
$path = trim(substr($path, strlen($base) - 1));
$path = mb_substr($path, mb_strlen($base));
} else {
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
$explodedPath = explode('/', $path);
$path = implode('/', array_splice($explodedPath, 3));
}
// Return normal url path if not specified in config
@@ -110,7 +135,7 @@ function baseUrl($path, $forceAppDomain = false)
return url($path);
}
return $base . '/' . $path;
return $base . '/' . ltrim($path, '/');
}
/**

View File

@@ -16,7 +16,7 @@
"laravel/framework": "~5.5.44",
"fideloper/proxy": "~3.3",
"intervention/image": "^2.4",
"laravel/socialite": "^3.0",
"laravel/socialite": "3.0.x-dev",
"league/flysystem-aws-s3-v3": "^1.0",
"barryvdh/laravel-dompdf": "^0.8.1",
"predis/predis": "^1.1",

93
composer.lock generated
View File

@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "06219a5c2419ca23ec2924eb31f4ed16",
"content-hash": "0946a07729a7a1bfef9bac185a870afd",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.82.3",
"version": "3.86.2",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7"
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"reference": "a0353c24b18d2ba0f5bb7ca8a478b4ce0b8153f7",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"reference": "50224232ac7a4e2a6fa4ebbe0281e5b7503acf76",
"shasum": ""
},
"require": {
@@ -87,7 +87,7 @@
"s3",
"sdk"
],
"time": "2018-12-21T22:21:50+00:00"
"time": "2019-01-18T21:10:44+00:00"
},
{
"name": "barryvdh/laravel-dompdf",
@@ -1457,16 +1457,16 @@
},
{
"name": "laravel/socialite",
"version": "v3.2.0",
"version": "3.0.x-dev",
"source": {
"type": "git",
"url": "https://github.com/laravel/socialite.git",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481"
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/socialite/zipball/7194c0cd9fb2ce449669252b8ec316b85b7de481",
"reference": "7194c0cd9fb2ce449669252b8ec316b85b7de481",
"url": "https://api.github.com/repos/laravel/socialite/zipball/79316f36641f1916a50ab14d368acdf1d97e46de",
"reference": "79316f36641f1916a50ab14d368acdf1d97e46de",
"shasum": ""
},
"require": {
@@ -1516,7 +1516,7 @@
"laravel",
"oauth"
],
"time": "2018-10-18T03:39:04+00:00"
"time": "2018-12-21T14:06:32+00:00"
},
{
"name": "league/flysystem",
@@ -1891,16 +1891,16 @@
},
{
"name": "nesbot/carbon",
"version": "1.36.1",
"version": "1.36.2",
"source": {
"type": "git",
"url": "https://github.com/briannesbitt/Carbon.git",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983"
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"reference": "63da8cdf89d7a5efe43aabc794365f6e7b7b8983",
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"reference": "cd324b98bc30290f233dd0e75e6ce49f7ab2a6c9",
"shasum": ""
},
"require": {
@@ -1945,7 +1945,7 @@
"datetime",
"time"
],
"time": "2018-11-22T18:23:02+00:00"
"time": "2018-12-28T10:07:33+00:00"
},
{
"name": "paragonie/random_compat",
@@ -2555,20 +2555,20 @@
},
{
"name": "socialiteproviders/manager",
"version": "v3.3.1",
"version": "v3.3.4",
"source": {
"type": "git",
"url": "https://github.com/SocialiteProviders/Manager.git",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7"
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"reference": "1de3f3d874392da6f1a4c0bf30d843e9cd903ea7",
"url": "https://api.github.com/repos/SocialiteProviders/Manager/zipball/58b72a667da292a1d0a0b1e6e9aeda4053617030",
"reference": "58b72a667da292a1d0a0b1e6e9aeda4053617030",
"shasum": ""
},
"require": {
"laravel/socialite": "~3.0",
"laravel/socialite": "~3.0|~4.0",
"php": "^5.6 || ^7.0"
},
"require-dev": {
@@ -2585,8 +2585,7 @@
},
"autoload": {
"psr-4": {
"SocialiteProviders\\Manager\\": "src/",
"SocialiteProviders\\Manager\\Test\\": "tests/"
"SocialiteProviders\\Manager\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -2597,10 +2596,14 @@
{
"name": "Andy Wendt",
"email": "andy@awendt.com"
},
{
"name": "Anton Komarev",
"email": "a.komarev@cybercog.su"
}
],
"description": "Easily add new or override built-in providers in Laravel Socialite.",
"time": "2017-11-20T08:42:57+00:00"
"time": "2019-01-16T07:58:54+00:00"
},
{
"name": "socialiteproviders/microsoft-azure",
@@ -3664,16 +3667,16 @@
},
{
"name": "vlucas/phpdotenv",
"version": "v2.5.1",
"version": "v2.5.2",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e"
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"reference": "8abb4f9aa89ddea9d52112c65bbe8d0125e2fa8e",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/cfd5dc225767ca154853752abc93aeec040fcf36",
"reference": "cfd5dc225767ca154853752abc93aeec040fcf36",
"shasum": ""
},
"require": {
@@ -3710,7 +3713,7 @@
"env",
"environment"
],
"time": "2018-07-29T20:33:41+00:00"
"time": "2018-10-30T17:29:25+00:00"
}
],
"packages-dev": [
@@ -4423,23 +4426,23 @@
},
{
"name": "justinrainbow/json-schema",
"version": "5.2.7",
"version": "5.2.8",
"source": {
"type": "git",
"url": "https://github.com/justinrainbow/json-schema.git",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23"
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/8560d4314577199ba51bf2032f02cd1315587c23",
"reference": "8560d4314577199ba51bf2032f02cd1315587c23",
"url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"reference": "dcb6e1006bb5fd1e392b4daa68932880f37550d4",
"shasum": ""
},
"require": {
"php": ">=5.3.3"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.1",
"friendsofphp/php-cs-fixer": "~2.2.20",
"json-schema/json-schema-test-suite": "1.2.0",
"phpunit/phpunit": "^4.8.35"
},
@@ -4485,7 +4488,7 @@
"json",
"schema"
],
"time": "2018-02-14T22:26:30+00:00"
"time": "2019-01-14T23:55:14+00:00"
},
{
"name": "laravel/browser-kit-testing",
@@ -6265,20 +6268,21 @@
},
{
"name": "webmozart/assert",
"version": "1.3.0",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/webmozart/assert.git",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a"
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/webmozart/assert/zipball/0df1908962e7a3071564e857d86874dad1ef204a",
"reference": "0df1908962e7a3071564e857d86874dad1ef204a",
"url": "https://api.github.com/repos/webmozart/assert/zipball/83e253c8e0be5b0257b881e1827274667c5c17a9",
"reference": "83e253c8e0be5b0257b881e1827274667c5c17a9",
"shasum": ""
},
"require": {
"php": "^5.3.3 || ^7.0"
"php": "^5.3.3 || ^7.0",
"symfony/polyfill-ctype": "^1.8"
},
"require-dev": {
"phpunit/phpunit": "^4.6",
@@ -6311,12 +6315,14 @@
"check",
"validate"
],
"time": "2018-01-29T19:49:41+00:00"
"time": "2018-12-25T11:19:39+00:00"
}
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"stability-flags": {
"laravel/socialite": 20
},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
@@ -6326,7 +6332,8 @@
"ext-dom": "*",
"ext-xml": "*",
"ext-mbstring": "*",
"ext-gd": "*"
"ext-gd": "*",
"ext-curl": "*"
},
"platform-dev": [],
"platform-overrides": {

View File

@@ -22,7 +22,8 @@ return [
// Set the default view type for various lists. Can be overridden by user preferences.
// These will be used for public viewers and users that have not set a preference.
'views' => [
'books' => env('APP_VIEWS_BOOKS', 'list')
'books' => env('APP_VIEWS_BOOKS', 'list'),
'bookshelves' => env('APP_VIEWS_BOOKSHELVES', 'grid'),
],
// The number of revisions to keep in the database.
@@ -45,13 +46,13 @@ return [
'url' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
// Application timezone for back-end date functions.
'timezone' => 'UTC',
'timezone' => env('APP_TIMEZONE', 'UTC'),
// Default locale to use
'locale' => env('APP_LANG', 'en'),
// Locales available
'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
'locales' => ['en', 'ar', 'de', 'de_informal', 'es', 'es_AR', 'fr', 'nl', 'pt_BR', 'sk', 'cs', 'sv', 'kr', 'ja', 'pl', 'it', 'ru', 'uk', 'zh_CN', 'zh_TW'],
// Application Fallback Locale
'fallback_locale' => 'en',

View File

@@ -8,23 +8,39 @@
* Do not edit this file unless you're happy to maintain any changes yourself.
*/
// REDIS - Split out configuration into an array
// REDIS
// Split out configuration into an array
if (env('REDIS_SERVERS', false)) {
$redisServerKeys = ['host', 'port', 'database'];
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
$redisConfig = [
'cluster' => env('REDIS_CLUSTER', false)
];
$redisConfig = [];
$cluster = count($redisServers) > 1;
if ($cluster) {
$redisConfig['clusters'] = ['default' => []];
}
foreach ($redisServers as $index => $redisServer) {
$redisServerName = ($index === 0) ? 'default' : 'redis-server-' . $index;
$redisServerDetails = explode(':', $redisServer);
if (count($redisServerDetails) < 2) $redisServerDetails[] = '6379';
if (count($redisServerDetails) < 3) $redisServerDetails[] = '0';
$redisConfig[$redisServerName] = array_combine($redisServerKeys, $redisServerDetails);
$serverConfig = [];
$configIndex = 0;
foreach ($redisDefaults as $configKey => $configDefault) {
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
$configIndex++;
}
if ($cluster) {
$redisConfig['clusters']['default'][] = $serverConfig;
} else {
$redisConfig['default'] = $serverConfig;
}
}
}
// MYSQL - Split out port from host if set
// MYSQL
// Split out port from host if set
$mysql_host = env('DB_HOST', 'localhost');
$mysql_host_exploded = explode(':', $mysql_host);
$mysql_port = env('DB_PORT', 3306);

View File

@@ -49,6 +49,8 @@ return [
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
'region' => env('STORAGE_S3_REGION', 'your-region'),
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
],
'rackspace' => [

View File

@@ -141,6 +141,7 @@ return [
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
'version' => env('LDAP_VERSION', false),
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),

View File

@@ -14,7 +14,6 @@ return [
// Options: file, cookie, database, redis, memcached, array
'driver' => env('SESSION_DRIVER', 'file'),
// Session lifetime, in minutes
'lifetime' => env('SESSION_LIFETIME', 120),

2991
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,17 +13,17 @@
"@babel/core": "^7.1.6",
"@babel/polyfill": "^7.0.0",
"@babel/preset-env": "^7.1.6",
"autoprefixer": "^8.6.5",
"autoprefixer": "^9.4.7",
"babel-loader": "^8.0.4",
"css-loader": "^0.28.11",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"css-loader": "^2.1.0",
"livereload": "^0.7.0",
"mini-css-extract-plugin": "^0.5.0",
"node-sass": "^4.10.0",
"npm-run-all": "^4.1.5",
"postcss-loader": "^2.1.6",
"postcss-loader": "^3.0.0",
"sass-loader": "^7.1.0",
"style-loader": "^0.21.0",
"uglifyjs-webpack-plugin": "^1.3.0",
"style-loader": "^0.23.1",
"uglifyjs-webpack-plugin": "^2.1.1",
"webpack": "^4.26.1",
"webpack-cli": "^3.1.2"
},

67
public/dist/app.js vendored Normal file

File diff suppressed because one or more lines are too long

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

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,32 @@
header {
display: none; }
body {
font-size: 12px; }
.page-content {
margin: 0 auto; }
.flex-fill {
display: block; }
.flex.sidebar + .flex.content {
border-left: none; }
.print-hidden {
display: none; }
.print-full-width {
width: 100%;
float: none;
display: block; }
h2 {
font-size: 2em;
line-height: 1;
margin-top: 0.6em;
margin-bottom: 0.3em; }
.comments-container {
display: none; }

4487
public/dist/styles.css vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
!function(){"use strict";var n=function(t){var e=t,r=function(){return e};return{get:r,set:function(t){e=t},clone:function(){return n(r())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),a=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),r=function(t){return t.fire("RestoreDraft")},i=function(t){return t.fire("StoreDraft")},s=function(t){return t.fire("RemoveDraft")},e=function(t,e){return((t=/^(\d+)([ms]?)$/.exec(""+(t||e)))[2]?{s:1e3,m:6e4}[t[2]]:1)*parseInt(t,10)},u=function(t){return t.getParam("autosave_ask_before_unload",!0)},f=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,document.location.pathname)).replace(/\{query\}/g,document.location.search)).replace(/\{hash\}/g,document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t){return e(t.settings.autosave_interval,"30s")},l=function(t){return e(t.settings.autosave_retention,"20m")},m=function(t,e){var r=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+r+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+r+">|<br>$","i").test(e)},v=function(t){var e=parseInt(a.getItem(f(t)+"time"),10)||0;return!((new Date).getTime()-e>l(t)&&(d(t,!1),1))},d=function(t,e){var r=f(t);a.removeItem(r+"draft"),a.removeItem(r+"time"),!1!==e&&s(t)},D=function(t){var e=f(t);!m(t)&&t.isDirty()&&(a.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),a.setItem(e+"time",(new Date).getTime().toString()),i(t))},g=function(t){var e=f(t);v(t)&&(t.setContent(a.getItem(e+"draft"),{format:"raw"}),r(t))},y={isEmpty:m,hasDraft:v,removeDraft:d,storeDraft:D,restoreDraft:g,startStoreDraft:function(t,e){var r=c(t);e.get()||(setInterval(function(){t.removed||D(t)},r),e.set(!0))},restoreLastDraft:function(t){t.undoManager.transact(function(){g(t),d(t)}),t.focus()}},p=function(e,r){return function(){var t=Array.prototype.slice.call(arguments);return e.apply(null,[r].concat(t))}},h=function(t){return{hasDraft:p(y.hasDraft,t),storeDraft:p(y.storeDraft,t),restoreDraft:p(y.restoreDraft,t),removeDraft:p(y.removeDraft,t),isEmpty:p(y.isEmpty,t)}},_=tinymce.util.Tools.resolve("tinymce.EditorManager");_._beforeUnloadHandler=function(){var e;return o.each(_.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&u(t)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var b=function(t){window.onbeforeunload=_._beforeUnloadHandler},I=function(r,n){return function(t){var e=t.control;e.disabled(!y.hasDraft(r)),r.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!y.hasDraft(r))}),y.startStoreDraft(r,n)}},w=function(t,e){t.addButton("restoredraft",{title:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e)}),t.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){y.restoreLastDraft(t)},onPostRender:I(t,e),context:"file"})};t.add("autosave",function(t){var e=n(!1);return b(t),w(t,e),h(t)})}();
!function(a){"use strict";var i=function(t){var e=t,n=function(){return e};return{get:n,set:function(t){e=t},clone:function(){return i(n())}}},t=tinymce.util.Tools.resolve("tinymce.PluginManager"),r=tinymce.util.Tools.resolve("tinymce.util.LocalStorage"),o=tinymce.util.Tools.resolve("tinymce.util.Tools"),u=function(t,e){var n=t||e,r=/^(\d+)([ms]?)$/.exec(""+n);return(r[2]?{s:1e3,m:6e4}[r[2]]:1)*parseInt(n,10)},s=function(t){var e=t.getParam("autosave_prefix","tinymce-autosave-{path}{query}{hash}-{id}-");return e=(e=(e=(e=e.replace(/\{path\}/g,a.document.location.pathname)).replace(/\{query\}/g,a.document.location.search)).replace(/\{hash\}/g,a.document.location.hash)).replace(/\{id\}/g,t.id)},c=function(t,e){var n=t.settings.forced_root_block;return""===(e=o.trim(void 0===e?t.getBody().innerHTML:e))||new RegExp("^<"+n+"[^>]*>((\xa0|&nbsp;|[ \t]|<br[^>]*>)+?|)</"+n+">|<br>$","i").test(e)},f=function(t){var e=parseInt(r.getItem(s(t)+"time"),10)||0;return!((new Date).getTime()-e>u(t.settings.autosave_retention,"20m")&&(l(t,!1),1))},l=function(t,e){var n=s(t);r.removeItem(n+"draft"),r.removeItem(n+"time"),!1!==e&&t.fire("RemoveDraft")},m=function(t){var e=s(t);!c(t)&&t.isDirty()&&(r.setItem(e+"draft",t.getContent({format:"raw",no_events:!0})),r.setItem(e+"time",(new Date).getTime().toString()),t.fire("StoreDraft"))},v=function(t){var e=s(t);f(t)&&(t.setContent(r.getItem(e+"draft"),{format:"raw"}),t.fire("RestoreDraft"))},d=function(t,e){var n=u(t.settings.autosave_interval,"30s");e.get()||(setInterval(function(){t.removed||m(t)},n),e.set(!0))},g=function(t){t.undoManager.transact(function(){v(t),l(t)}),t.focus()};function y(r){for(var o=[],t=1;t<arguments.length;t++)o[t-1]=arguments[t];return function(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];var n=o.concat(t);return r.apply(null,n)}}var p=tinymce.util.Tools.resolve("tinymce.EditorManager");p._beforeUnloadHandler=function(){var e;return o.each(p.get(),function(t){t.plugins.autosave&&t.plugins.autosave.storeDraft(),!e&&t.isDirty()&&t.getParam("autosave_ask_before_unload",!0)&&(e=t.translate("You have unsaved changes are you sure you want to navigate away?"))}),e};var h=function(n,r){return function(t){var e=t.control;e.disabled(!f(n)),n.on("StoreDraft RestoreDraft RemoveDraft",function(){e.disabled(!f(n))}),d(n,r)}};t.add("autosave",function(t){var e,n,r,o=i(!1);return a.window.onbeforeunload=p._beforeUnloadHandler,n=o,(e=t).addButton("restoredraft",{title:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n)}),e.addMenuItem("restoredraft",{text:"Restore last draft",onclick:function(){g(e)},onPostRender:h(e,n),context:"file"}),t.on("init",function(){t.getParam("autosave_restore_when_empty",!1)&&t.dom.isEmpty(t.getBody())&&v(t)}),{hasDraft:y(f,r=t),storeDraft:y(m,r),restoreDraft:y(v,r),removeDraft:y(l,r),isEmpty:y(c,r)}})}(window);

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),m=function(e,n){e.fire("FullscreenStateChanged",{state:n})},g=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=document.body,u=document.documentElement,d=n.get(),a=function(){var e,n,t,i;g.setStyle(l,"height",(t=window,i=document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){g.unbind(window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),g.removeClass(s,"mce-fullscreen"),g.removeClass(u,"mce-fullscreen"),g.removeClass(r,"mce-fullscreen"),o=d.scrollPos,window.scrollTo(o.x,o.y),g.unbind(window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),m(e,!1);else{var f={scrollPos:(c=g.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",g.addClass(s,"mce-fullscreen"),g.addClass(u,"mce-fullscreen"),g.addClass(r,"mce-fullscreen"),g.bind(window,"resize",a),e.on("remove",h),a(),n.set(f),m(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}();
!function(m){"use strict";var i=function(e){var n=e,t=function(){return n};return{get:t,set:function(e){n=e},clone:function(){return i(t())}}},e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=function(e){return{isFullscreen:function(){return null!==e.get()}}},n=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),g=function(e,n){e.fire("FullscreenStateChanged",{state:n})},w=n.DOM,r=function(e,n){var t,r,l,i,o,c,s=m.document.body,u=m.document.documentElement,d=n.get(),a=function(){var e,n,t,i;w.setStyle(l,"height",(t=m.window,i=m.document.body,i.offsetWidth&&(e=i.offsetWidth,n=i.offsetHeight),t.innerWidth&&t.innerHeight&&(e=t.innerWidth,n=t.innerHeight),{w:e,h:n}).h-(r.clientHeight-l.clientHeight))},h=function(){w.unbind(m.window,"resize",a)};if(t=(r=e.getContainer()).style,i=(l=e.getContentAreaContainer().firstChild).style,d)i.width=d.iframeWidth,i.height=d.iframeHeight,d.containerWidth&&(t.width=d.containerWidth),d.containerHeight&&(t.height=d.containerHeight),w.removeClass(s,"mce-fullscreen"),w.removeClass(u,"mce-fullscreen"),w.removeClass(r,"mce-fullscreen"),o=d.scrollPos,m.window.scrollTo(o.x,o.y),w.unbind(m.window,"resize",d.resizeHandler),e.off("remove",d.removeHandler),n.set(null),g(e,!1);else{var f={scrollPos:(c=w.getViewPort(),{x:c.x,y:c.y}),containerWidth:t.width,containerHeight:t.height,iframeWidth:i.width,iframeHeight:i.height,resizeHandler:a,removeHandler:h};i.width=i.height="100%",t.width=t.height="",w.addClass(s,"mce-fullscreen"),w.addClass(u,"mce-fullscreen"),w.addClass(r,"mce-fullscreen"),w.bind(m.window,"resize",a),e.on("remove",h),a(),n.set(f),g(e,!0)}},l=function(e,n){e.addCommand("mceFullScreen",function(){r(e,n)})},o=function(t){return function(e){var n=e.control;t.on("FullscreenStateChanged",function(e){n.active(e.state)})}},c=function(e){e.addMenuItem("fullscreen",{text:"Fullscreen",shortcut:"Ctrl+Shift+F",selectable:!0,cmd:"mceFullScreen",onPostRender:o(e),context:"view"}),e.addButton("fullscreen",{active:!1,tooltip:"Fullscreen",cmd:"mceFullScreen",onPostRender:o(e)})};e.add("fullscreen",function(e){var n=i(null);return e.settings.inline||(l(e,n),c(e),e.addShortcut("Ctrl+Shift+F","","mceFullScreen")),t(n)})}(window);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1 +1 @@
!function(){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),c=tinymce.util.Tools.resolve("tinymce.EditorManager"),s=tinymce.util.Tools.resolve("tinymce.Env"),a=tinymce.util.Tools.resolve("tinymce.util.Delay"),y=tinymce.util.Tools.resolve("tinymce.util.Tools"),f=tinymce.util.Tools.resolve("tinymce.util.VK"),d=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},m=t.DOM,n=function(e){e.keyCode!==f.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==f.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=y.explode(d(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):m.get(e[0]):":next"===e[1]?u(1):m.get(e[1]))){var t=c.get(o.id||o.name);o.id&&t?t.focus():a.setTimeout(function(){s.webkit||window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&c.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=m.select(":input:enabled,*[tabindex]:not(iframe)"),y.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&m.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),s.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}();
!function(c){"use strict";var e=tinymce.util.Tools.resolve("tinymce.PluginManager"),t=tinymce.util.Tools.resolve("tinymce.dom.DOMUtils"),s=tinymce.util.Tools.resolve("tinymce.EditorManager"),a=tinymce.util.Tools.resolve("tinymce.Env"),y=tinymce.util.Tools.resolve("tinymce.util.Delay"),f=tinymce.util.Tools.resolve("tinymce.util.Tools"),d=tinymce.util.Tools.resolve("tinymce.util.VK"),m=function(e){return e.getParam("tab_focus",e.getParam("tabfocus_elements",":prev,:next"))},v=t.DOM,n=function(e){e.keyCode!==d.TAB||e.ctrlKey||e.altKey||e.metaKey||e.preventDefault()},i=function(r){function e(n){var i,o,e,l;if(!(n.keyCode!==d.TAB||n.ctrlKey||n.altKey||n.metaKey||n.isDefaultPrevented())&&(1===(e=f.explode(m(r))).length&&(e[1]=e[0],e[0]=":prev"),o=n.shiftKey?":prev"===e[0]?u(-1):v.get(e[0]):":next"===e[1]?u(1):v.get(e[1]))){var t=s.get(o.id||o.name);o.id&&t?t.focus():y.setTimeout(function(){a.webkit||c.window.focus(),o.focus()},10),n.preventDefault()}function u(e){function t(t){return/INPUT|TEXTAREA|BUTTON/.test(t.tagName)&&s.get(n.id)&&-1!==t.tabIndex&&function e(t){return"BODY"===t.nodeName||"hidden"!==t.type&&"none"!==t.style.display&&"hidden"!==t.style.visibility&&e(t.parentNode)}(t)}if(o=v.select(":input:enabled,*[tabindex]:not(iframe)"),f.each(o,function(e,t){if(e.id===r.id)return i=t,!1}),0<e){for(l=i+1;l<o.length;l++)if(t(o[l]))return o[l]}else for(l=i-1;0<=l;l--)if(t(o[l]))return o[l];return null}}r.on("init",function(){r.inline&&v.setAttrib(r.getBody(),"tabIndex",null),r.on("keyup",n),a.gecko?r.on("keypress keydown",e):r.on("keydown",e)})};e.add("tabfocus",function(e){i(e)})}(window);

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -87,6 +87,7 @@
<glyph unicode="&#xe906;" glyph-name="reload" d="M889.68 793.68c-93.608 102.216-228.154 166.32-377.68 166.32-282.77 0-512-229.23-512-512h96c0 229.75 186.25 416 416 416 123.020 0 233.542-53.418 309.696-138.306l-149.696-149.694h352v352l-134.32-134.32zM928 448c0-229.75-186.25-416-416-416-123.020 0-233.542 53.418-309.694 138.306l149.694 149.694h-352v-352l134.32 134.32c93.608-102.216 228.154-166.32 377.68-166.32 282.77 0 512 229.23 512 512h-96z" />
<glyph unicode="&#xe907;" glyph-name="translate" d="M553.6 304l-118.4 118.4c80 89.6 137.6 195.2 172.8 304h137.6v92.8h-326.4v92.8h-92.8v-92.8h-326.4v-92.8h518.4c-32-89.6-80-176-147.2-249.6-44.8 48-80 99.2-108.8 156.8h-92.8c35.2-76.8 80-147.2 137.6-211.2l-236.8-233.6 67.2-67.2 233.6 233.6 144-144c3.2 0 38.4 92.8 38.4 92.8zM816 540.8h-92.8l-208-560h92.8l51.2 140.8h220.8l51.2-140.8h92.8l-208 560zM691.2 214.4l76.8 201.6 76.8-201.6h-153.6z" />
<glyph unicode="&#xe908;" glyph-name="drag" d="M576 896h128v-128h-128v128zM576 640h128v-128h-128v128zM320 640h128v-128h-128v128zM576 384h128v-128h-128v128zM320 384h128v-128h-128v128zM320 128h128v-128h-128v128zM576 128h128v-128h-128v128zM320 896h128v-128h-128v128z" />
<glyph unicode="&#xe909;" glyph-name="format-painter" d="M768 746.667v42.667c0 23.467-19.2 42.667-42.667 42.667h-512c-23.467 0-42.667-19.2-42.667-42.667v-170.667c0-23.467 19.2-42.667 42.667-42.667h512c23.467 0 42.667 19.2 42.667 42.667v42.667h42.667v-170.667h-426.667v-384c0-23.467 19.2-42.667 42.667-42.667h85.333c23.467 0 42.667 19.2 42.667 42.667v298.667h341.333v341.333h-128z" />
<glyph unicode="&#xe90b;" glyph-name="home" d="M1024 369.556l-512 397.426-512-397.428v162.038l512 397.426 512-397.428zM896 384v-384h-256v256h-256v-256h-256v384l384 288z" />
<glyph unicode="&#xe911;" glyph-name="books" d="M576.234 670.73l242.712 81.432 203.584-606.784-242.712-81.432zM0 64h256v704h-256v-704zM64 640h128v-64h-128v64zM320 64h256v704h-256v-704zM384 640h128v-64h-128v64z" />
<glyph unicode="&#xe914;" glyph-name="upload" d="M839.432 760.57c27.492-27.492 50.554-78.672 55.552-120.57h-318.984v318.984c41.898-4.998 93.076-28.060 120.568-55.552l142.864-142.862zM512 576v384h-368c-44 0-80-36-80-80v-864c0-44 36-80 80-80h672c44 0 80 36 80 80v560h-384zM576 192v-192h-192v192h-160l256 256 256-256h-160z" />

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -9,8 +9,7 @@ A platform for storing and organising information and documentation. General inf
* [Installation Instructions](https://www.bookstackapp.com/docs/admin/installation)
* [Documentation](https://www.bookstackapp.com/docs)
* [Demo Instance](https://demo.bookstackapp.com)
* *Username: `admin@example.com`*
* *Password: `password`*
* [Admin Login](https://demo.bookstackapp.com/login?email=admin@example.com&password=password)
* [BookStack Blog](https://www.bookstackapp.com/blog)
## Project Definition
@@ -19,7 +18,30 @@ BookStack is an opinionated wiki system that provides a pleasant and simple out
BookStack is not designed as an extensible platform to be used for purposes that differ to the statement above.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. Put simply, At the end of the day this is free software developed and maintained by people donating their own free time.
In regards to development philosophy, BookStack has a relaxed, open & positive approach. At the end of the day this is free software developed and maintained by people donating their own free time.
## Road Map
Below is a high-level road map view for BookStack to provide a sense of direction of where the project is going. This can change at any point and does not reflect many features and improvements that will also be included as part of the journey along this road map. For more granular detail of what will be included in upcoming releases you can review the project milestones as defined in the "Release Process" section below.
- **Design Revamp** *[(In Progress)](https://github.com/BookStackApp/BookStack/pull/1153)*
- *A more organised modern design to clean things up, make BookStack more efficient to use and increase mobile usability.*
- **Platform REST API**
- *A REST API covering, at minimum, control of core content models (Books, Chapters, Pages) for automation and platform extension.*
- **Editor Alignment & Review**
- *Review the page editors with goal of achieving increased interoperability & feature parity while also considering collaborative editing potential.*
- **Permission System Review**
- *Improvement in how permissions are applied and a review of the efficiency of the permission & roles system.*
- **Installation & Deployment Process Revamp**
- *Creation of a streamlined & secure process for users to deploy & update BookStack with reduced development requirements (No git or composer requirement).*
## Release Versioning & Process
BookStack releases are each assigned a version number, such as "v0.25.2", in the format `v<phase>.<feature>.<patch>`. A change only in the `patch` number indicates a fairly minor release that mainly contains fixes and therefore is very unlikely to cause breakages upon update. A change in the `feature` number indicates a release which will generally bring new features in addition to fixes and enhancements. These releases have a small chance of introducing breaking changes upon update so it's worth checking for any notes in the [update guide](https://www.bookstackapp.com/docs/admin/updates/). A change in the `phase` indicates a much large change in BookStack that will likely incur breakages requiring manual intervention.
Each BookStack release will have a [milestone](https://github.com/BookStackApp/BookStack/milestones) created with issues & pull requests assigned to it to define what will be in that release. Milestones are built up then worked through until complete at which point, after some testing and documentation updates, the release will be deployed.
For feature releases, and some patch releases, the release will be accompanied by a post on the [BookStack blog](https://www.bookstackapp.com/blog/) which will provide additional detail on features, changes & updates otherwise the [GitHub release page](https://github.com/BookStackApp/BookStack/releases) will show a list of changes. You can sign up to be alerted to new BookStack blogs posts (once per week maximum) [at this link](http://eepurl.com/cmmq5j).
## Development & Testing
@@ -85,15 +107,24 @@ PHP code within BookStack is generally to [PSR-2](http://www.php-fig.org/psr/psr
### Pull Requests
Pull requests are very welcome. If the scope of your pull request is large it may be best to open the pull request early or create an issue for it to discuss how it will fit in to the project and plan out the merge.
Pull requests are welcome. Unless a small tweak or language update, It may be best to open the pull request early or create an issue for your intended change to discuss how it will fit in to the project and plan out the merge.
Pull requests should be created from the `master` branch and should be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
Pull requests should be created from the `master` branch since they will be merged back into `master` once done. Please do not build from or request a merge into the `release` branch as this is only for publishing releases.
If you are looking to alter CSS or JavaScript content please edit the source files found in `resources/assets`. Any CSS or JS files within `public` are built from these source files and therefore should not be edited directly.
## Website, Docs & Blog
The website project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
The website which contains the project docs & Blog can be found in the [BookStackApp/website](https://github.com/BookStackApp/website) repo.
## Security
Security information for administering a BookStack instance can be found on the [documentation site here](https://www.bookstackapp.com/docs/admin/security/).
If you'd like to be notified of new potential security concerns you can [sign-up to the BookStack security mailing list](http://eepurl.com/glIh8z).
If you would like to report a security concern in a more confidential manner than via a GitHub issue, You can directly email the lead maintainer [ssddanbrown](https://github.com/ssddanbrown). You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown). Alternatively you can send a DM via twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
## License
@@ -117,7 +148,6 @@ These are the great open-source projects used to help build BookStack:
* [clipboard.js](https://clipboardjs.com/)
* [TinyColorPicker](http://www.dematte.at/tinyColorPicker/index.html)
* [markdown-it](https://github.com/markdown-it/markdown-it) and [markdown-it-task-lists](https://github.com/revin/markdown-it-task-lists)
* [Moment.js](http://momentjs.com/)
* [BarryVD](https://github.com/barryvdh)
* [Debugbar](https://github.com/barryvdh/laravel-debugbar)
* [Dompdf](https://github.com/barryvdh/laravel-dompdf)

View File

@@ -1,4 +1 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
<path d="M0 0h24v24H0z" fill="none"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 13.3h-5.7V19h-2.6v-5.7H5v-2.6h5.7V5h2.6v5.7H19z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

Before

Width:  |  Height:  |  Size: 161 B

After

Width:  |  Height:  |  Size: 166 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M19.252 1.708H8.663a1.77 1.77 0 0 0-1.765 1.764v14.12c0 .97.794 1.764 1.765 1.764h10.59a1.77 1.77 0 0 0 1.764-1.765V3.472a1.77 1.77 0 0 0-1.765-1.764zM8.663 3.472h4.412v7.06L10.87 9.208l-2.206 1.324z"/><path d="M30.61 3.203h24v24h-24z" fill="none"/><path d="M2.966 6.61v14c0 1.1.9 2 2 2h14v-2h-14v-14z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

View File

@@ -1,2 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path fill="none" d="M0 0h24v24H0V0z"/><path d="M1.088 2.566h17.42v17.42H1.088z" fill="none"/><path d="M4 20.058h15.892V22H4z"/><path d="M2.902 1.477h17.42v17.42H2.903z" fill="none"/><g><path d="M6.658 3.643V18h-2.38V3.643zM11.326 3.643V18H8.947V3.643zM14.722 3.856l5.613 13.214-2.19.93-5.613-13.214z"/></g></svg>

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.86 4.118l-9.733 9.609-3.951-3.995-2.98 2.966 6.93 7.184L21.805 7.217z"/></svg>

After

Width:  |  Height:  |  Size: 151 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/><path d="M0 0h24v24H0z" fill="none"/></svg>

After

Width:  |  Height:  |  Size: 161 B

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