Compare commits
388 Commits
fix/video-
...
v0.26.3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f16129869 | ||
|
|
64a8037fdd | ||
|
|
c732970f6e | ||
|
|
7502ba1bc8 | ||
|
|
33a04697ef | ||
|
|
a602cdf401 | ||
|
|
5aa741cb60 | ||
|
|
3ad1b42a74 | ||
|
|
2b7362fa94 | ||
|
|
35f35bcba5 | ||
|
|
13c0386e84 | ||
|
|
78f5f44460 | ||
|
|
35e6635379 | ||
|
|
8ae35f645a | ||
|
|
5470a9e035 | ||
|
|
dbfe63ccf6 | ||
|
|
114f10d5ca | ||
|
|
5226ddd959 | ||
|
|
60013f776a | ||
|
|
ac0a070fc8 | ||
|
|
b05659d7a3 | ||
|
|
3af4648dc3 | ||
|
|
e1e1ea6099 | ||
|
|
0c3dc50cd9 | ||
|
|
0a0ceb382e | ||
|
|
896f88174a | ||
|
|
0ee9e5c4db | ||
|
|
112f73c91c | ||
|
|
b47dc046e0 | ||
|
|
215d84d705 | ||
|
|
c459d86b58 | ||
|
|
adf0d5fce2 | ||
|
|
cb355c8aad | ||
|
|
d0e351b942 | ||
|
|
e3d570e928 | ||
|
|
e00c170d85 | ||
|
|
e430dad38c | ||
|
|
b70a5c0cdb | ||
|
|
9443ae9f40 | ||
|
|
d62d2384cb | ||
|
|
a981dc41cb | ||
|
|
97ffbaa740 | ||
|
|
8051558d3a | ||
|
|
7ef059e254 | ||
|
|
4329fee2c9 | ||
|
|
b1cf5ab309 | ||
|
|
b67d9f4036 | ||
|
|
224d9e7a7d | ||
|
|
31419c3913 | ||
|
|
ba36d36597 | ||
|
|
ac7d6b8737 | ||
|
|
e52dab825a | ||
|
|
33f51d5b78 | ||
|
|
ae7529376b | ||
|
|
aefd4d423c | ||
|
|
bad44391d4 | ||
|
|
1880633be3 | ||
|
|
8273f8b16e | ||
|
|
0600f752eb | ||
|
|
342dc8948c | ||
|
|
3e24e44106 | ||
|
|
404d11d0eb | ||
|
|
07b889547d | ||
|
|
220c2a4102 | ||
|
|
e9914eb301 | ||
|
|
e47a7e0b97 | ||
|
|
989309fdff | ||
|
|
6797c91eeb | ||
|
|
b9ad3f9f65 | ||
|
|
7a8678e5f7 | ||
|
|
ba09dad1fe | ||
|
|
5910e00fb8 | ||
|
|
3f83c548f8 | ||
|
|
adc866cb3d | ||
|
|
ad542f0407 | ||
|
|
15786e2630 | ||
|
|
8c190324ac | ||
|
|
79f6dc00a3 | ||
|
|
cb832a2c10 | ||
|
|
a87ae16010 | ||
|
|
aeb1fc4d49 | ||
|
|
6428f32483 | ||
|
|
884e20cc5e | ||
|
|
fc761784a1 | ||
|
|
e0c229114f | ||
|
|
4e49d06182 | ||
|
|
2bb06463d5 | ||
|
|
4d6df37963 | ||
|
|
553d3ce861 | ||
|
|
0bc5ccba32 | ||
|
|
ed330f246c | ||
|
|
6c66a8935a | ||
|
|
c653618eab | ||
|
|
efc034bd8d | ||
|
|
c24764018a | ||
|
|
c8cf6731e2 | ||
|
|
d0db0f8e26 | ||
|
|
c380c10d54 | ||
|
|
95d4149d5e | ||
|
|
7f3f6e65b9 | ||
|
|
29f17fd154 | ||
|
|
84419005e7 | ||
|
|
d3cd369247 | ||
|
|
50a9c71de0 | ||
|
|
faa3a8b842 | ||
|
|
c836862d89 | ||
|
|
03073dd9e4 | ||
|
|
ee58bea8b7 | ||
|
|
9406b4d4c9 | ||
|
|
01be72d5e2 | ||
|
|
21e1123d12 | ||
|
|
8d358e4894 | ||
|
|
f797d2da20 | ||
|
|
d3cc261320 | ||
|
|
cc24d478aa | ||
|
|
07adfb2ff1 | ||
|
|
36481bb73f | ||
|
|
4d5e47a2d2 | ||
|
|
d66fab8bee | ||
|
|
2694bb8fab | ||
|
|
b12ae6d11b | ||
|
|
221a483b40 | ||
|
|
4a127c29a5 | ||
|
|
8c21b5345d | ||
|
|
0a06e2bce3 | ||
|
|
d9cde4123d | ||
|
|
7cda9b026e | ||
|
|
67ed4710b6 | ||
|
|
745a0bb98d | ||
|
|
aedff7dc6d | ||
|
|
17969c0bbf | ||
|
|
666ced9c3b | ||
|
|
37bf7f11e4 | ||
|
|
bda8aa414b | ||
|
|
60d175a9b9 | ||
|
|
42e908c7f0 | ||
|
|
53a26a365c | ||
|
|
4ee0fde0ac | ||
|
|
934512d09c | ||
|
|
9102c90986 | ||
|
|
9879a0d12c | ||
|
|
5d2e80bff3 | ||
|
|
83818234c8 | ||
|
|
5c00187138 | ||
|
|
193e2ffebe | ||
|
|
b2a5d07787 | ||
|
|
15cf9f8b32 | ||
|
|
f9adfee47a | ||
|
|
2e988277f0 | ||
|
|
4e8e28c35f | ||
|
|
4cbfb2bf13 | ||
|
|
c3e74219c4 | ||
|
|
13c9d7bc2d | ||
|
|
f5fe524e6c | ||
|
|
119b539586 | ||
|
|
29a5c180f0 | ||
|
|
37b91b6b0e | ||
|
|
b3a4d8af2a | ||
|
|
8b7bee7c67 | ||
|
|
837d2bc582 | ||
|
|
64cd499542 | ||
|
|
5f2d226f09 | ||
|
|
7906602291 | ||
|
|
6dafe773ff | ||
|
|
00703fa817 | ||
|
|
44c537de1a | ||
|
|
6bccf0e64a | ||
|
|
042a6f9760 | ||
|
|
04287745e4 | ||
|
|
5c9b528517 | ||
|
|
6be2d3f28c | ||
|
|
6d20bdc1fb | ||
|
|
502ea608bf | ||
|
|
55b07c7076 | ||
|
|
33e999909f | ||
|
|
6be95cd2ac | ||
|
|
f467185e86 | ||
|
|
646fd822c5 | ||
|
|
d96baf2d4a | ||
|
|
1c312906bc | ||
|
|
9126c87f2b | ||
|
|
579d98a908 | ||
|
|
98a4359198 | ||
|
|
6f710225b5 | ||
|
|
b273b9d6d0 | ||
|
|
e471d0c52a | ||
|
|
0a431e3223 | ||
|
|
035a0d8efb | ||
|
|
e70423c73f | ||
|
|
8445304fe9 | ||
|
|
f1e571a57c | ||
|
|
e9be2b7174 | ||
|
|
352cbbd074 | ||
|
|
b00b319e83 | ||
|
|
a112c11df8 | ||
|
|
058cc2cbd6 | ||
|
|
edd98c00e5 | ||
|
|
19bb11a1c9 | ||
|
|
9511d10ec8 | ||
|
|
3286f29a61 | ||
|
|
77d3bd31a6 | ||
|
|
56004abdf4 | ||
|
|
df6f6e2d77 | ||
|
|
ba1b3fc181 | ||
|
|
67d4fa0c65 | ||
|
|
49deab3a02 | ||
|
|
5325870271 | ||
|
|
138f5d5c4f | ||
|
|
880d4f35da | ||
|
|
20988962fe | ||
|
|
32603362a6 | ||
|
|
9dba9ca178 | ||
|
|
8a4a81629f | ||
|
|
5ef0992d5b | ||
|
|
25bc28a1be | ||
|
|
4c561c7fa0 | ||
|
|
12be7d0086 | ||
|
|
ba0af9214e | ||
|
|
36424a24b5 | ||
|
|
a70ee9664a | ||
|
|
156c0a88e9 | ||
|
|
0efed43389 | ||
|
|
163a57cf70 | ||
|
|
95b3e78573 | ||
|
|
63a345bc93 | ||
|
|
a3ccde8698 | ||
|
|
9700b7ccea | ||
|
|
54c428c375 | ||
|
|
ebe5d643f3 | ||
|
|
e66ddbc17b | ||
|
|
0e0a17cc30 | ||
|
|
ffceb4092e | ||
|
|
50e5527483 | ||
|
|
f63fd4beca | ||
|
|
d682a0157f | ||
|
|
70ad707c3c | ||
|
|
3062bf1876 | ||
|
|
a2087fe3ff | ||
|
|
19770d2792 | ||
|
|
99c6d70c51 | ||
|
|
0830521e60 | ||
|
|
2c48f4f7e8 | ||
|
|
7f95b51b00 | ||
|
|
ff0b9004bc | ||
|
|
8fd8652bbf | ||
|
|
e1474194db | ||
|
|
4c574c22a8 | ||
|
|
0b976d9f91 | ||
|
|
2a882a43ff | ||
|
|
aabd4c0412 | ||
|
|
d39fc84301 | ||
|
|
e093a172cb | ||
|
|
4b01f8934b | ||
|
|
3c796b1ae7 | ||
|
|
b7915cc7b0 | ||
|
|
54b36cd305 | ||
|
|
4df11701e7 | ||
|
|
4a872012c5 | ||
|
|
bc116b45b5 | ||
|
|
a059960b9e | ||
|
|
7770966fed | ||
|
|
d7adcf6c69 | ||
|
|
c356612612 | ||
|
|
0e395b1e21 | ||
|
|
89be30ff0e | ||
|
|
04a364dcc3 | ||
|
|
db83ac7eaa | ||
|
|
3ca9dddf61 | ||
|
|
bf74f53ca7 | ||
|
|
9d67efb4a4 | ||
|
|
3a39b9f440 | ||
|
|
27f7aab375 | ||
|
|
337da0c467 | ||
|
|
f56b3560c4 | ||
|
|
02dfe11ce6 | ||
|
|
83d06beb70 | ||
|
|
a8cfc059c8 | ||
|
|
1614b2bab0 | ||
|
|
4bdec0d214 | ||
|
|
6a7d7e7c2b | ||
|
|
30d4674657 | ||
|
|
9f961f95f8 | ||
|
|
bab99a26ec | ||
|
|
9a7fecd269 | ||
|
|
a8dc0d449b | ||
|
|
a0381f76bf | ||
|
|
6102f66daa | ||
|
|
c6134d162d | ||
|
|
2046f9b9de | ||
|
|
ac3ba594a4 | ||
|
|
22df25a480 | ||
|
|
8b30c7f02e | ||
|
|
757cdddc7c | ||
|
|
df95e99680 | ||
|
|
5a6d544db7 | ||
|
|
16117d329c | ||
|
|
e90da18ada | ||
|
|
a08d80e1cc | ||
|
|
6258175922 | ||
|
|
15736777a0 | ||
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
67d9e24d8f | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
753f6394f7 | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
1fe933e4ea | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
8dec674cc3 | ||
|
|
f784c03746 | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
f184d763ad | ||
|
|
a91d42634d | ||
|
|
f517ef3616 | ||
|
|
e99507ddcf | ||
|
|
d2cacf1945 | ||
|
|
448ac1405b | ||
|
|
6ad21ce885 |
97
.env.example
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Entities/BreadcrumbsViewComposer.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
60
app/Entities/EntityContextManager.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,6 @@
|
||||
|
||||
use Exception;
|
||||
|
||||
class HttpFetchException extends Exception {}
|
||||
class HttpFetchException extends Exception
|
||||
{
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserUpdateException extends NotifyException {}
|
||||
class UserUpdateException extends NotifyException
|
||||
{
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ class LoginController extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
}
|
||||
88
app/Http/Controllers/Images/DrawioImageController.php
Normal 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)
|
||||
]);
|
||||
}
|
||||
}
|
||||
64
app/Http/Controllers/Images/GalleryImageController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
115
app/Http/Controllers/Images/ImageController.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,7 +37,7 @@ class Authenticate
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->auth->guest() && !setting('app-public')) {
|
||||
if (!hasAppAccess()) {
|
||||
if ($request->ajax()) {
|
||||
return response('Unauthorized.', 401);
|
||||
} else {
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,5 +31,4 @@ class MailNotification extends Notification implements ShouldQueue
|
||||
'text' => 'vendor.notifications.email-plain'
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<?php namespace BookStack\Notifications;
|
||||
|
||||
|
||||
class ResetPassword extends MailNotification
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -30,5 +30,4 @@ class HttpFetcher
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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' => [
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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
12
package.json
@@ -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
2766
public/dist/export-styles.css
vendored
Normal file
32
public/dist/print-styles.css
vendored
Normal 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
@@ -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| |[ \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| |[ \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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -87,6 +87,7 @@
|
||||
<glyph unicode="" 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="" 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="" 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="" 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="" 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="" 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="" 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 |
4
public/libs/tinymce/tinymce.min.js
vendored
44
readme.md
@@ -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)
|
||||
|
||||
@@ -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 |
1
resources/assets/icons/books.svg
Normal 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 |
@@ -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 |
1
resources/assets/icons/check.svg
Normal 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 |
1
resources/assets/icons/chevron-right.svg
Normal 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 |