mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-13 03:13:58 +03:00
Compare commits
323 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
058cc2cbd6 | ||
|
|
edd98c00e5 | ||
|
|
19bb11a1c9 | ||
|
|
9511d10ec8 | ||
|
|
77d3bd31a6 | ||
|
|
56004abdf4 | ||
|
|
df6f6e2d77 | ||
|
|
ba1b3fc181 | ||
|
|
9dba9ca178 | ||
|
|
8a4a81629f | ||
|
|
5ef0992d5b | ||
|
|
25bc28a1be | ||
|
|
4c561c7fa0 | ||
|
|
12be7d0086 | ||
|
|
ba0af9214e | ||
|
|
36424a24b5 | ||
|
|
a70ee9664a | ||
|
|
156c0a88e9 | ||
|
|
95b3e78573 | ||
|
|
63a345bc93 | ||
|
|
a3ccde8698 | ||
|
|
9700b7ccea | ||
|
|
54c428c375 | ||
|
|
ebe5d643f3 | ||
|
|
e66ddbc17b | ||
|
|
0e0a17cc30 | ||
|
|
ffceb4092e | ||
|
|
50e5527483 | ||
|
|
f63fd4beca | ||
|
|
d682a0157f | ||
|
|
70ad707c3c | ||
|
|
3062bf1876 | ||
|
|
a2087fe3ff | ||
|
|
19770d2792 | ||
|
|
99c6d70c51 | ||
|
|
0830521e60 | ||
|
|
2317bf2350 | ||
|
|
2c48f4f7e8 | ||
|
|
456afdcd4c | ||
|
|
68017e2553 | ||
|
|
866187830a | ||
|
|
b56fc21aaf | ||
|
|
d673bf61c2 | ||
|
|
18b10153e5 | ||
|
|
7c8edf5673 | ||
|
|
f4ea5f1f55 | ||
|
|
f62843c861 | ||
|
|
5fe630b8d2 | ||
|
|
d910defbfd | ||
|
|
153adb055c | ||
|
|
26ec1cc3dc | ||
|
|
d476e30df0 | ||
|
|
37ab97af8c | ||
|
|
7fcd7a5d91 | ||
|
|
c67f76f776 | ||
|
|
9a444b4a04 | ||
|
|
106f32591d | ||
|
|
7f6929d716 | ||
|
|
651ae2f3be | ||
|
|
101a7b40b9 | ||
|
|
1930ed4d6a | ||
|
|
2753629dbe | ||
|
|
86a00a59d4 | ||
|
|
d6dd96e7fc | ||
|
|
1b1ddb6794 | ||
|
|
f65ff3a9a8 | ||
|
|
323bff7d6d | ||
|
|
0e3d507ec2 | ||
|
|
f943f0d401 | ||
|
|
1b01d65965 | ||
|
|
a2acd063f3 | ||
|
|
e9e3e8b6b1 | ||
|
|
e093a172cb | ||
|
|
4b01f8934b | ||
|
|
75ca430fd4 | ||
|
|
4cf43f67d6 | ||
|
|
86899864dd | ||
|
|
3c796b1ae7 | ||
|
|
b7915cc7b0 | ||
|
|
eac82c47a5 | ||
|
|
d3d3e2ad3e | ||
|
|
54b36cd305 | ||
|
|
f8396d3632 | ||
|
|
bc116b45b5 | ||
|
|
a059960b9e | ||
|
|
302b53562d | ||
|
|
7770966fed | ||
|
|
d7adcf6c69 | ||
|
|
ebd4c3327d | ||
|
|
d0c166c207 | ||
|
|
321b53c827 | ||
|
|
5e6c039b08 | ||
|
|
178b5af83a | ||
|
|
4be0c567cc | ||
|
|
0724ae3640 | ||
|
|
62a475e464 | ||
|
|
cfc1a2f045 | ||
|
|
b012f27ae3 | ||
|
|
58bec7287f | ||
|
|
9341ae4910 | ||
|
|
1328755f95 | ||
|
|
038b2418f7 | ||
|
|
e3230f8f21 | ||
|
|
fd37d95ffc | ||
|
|
3abde8bfe2 | ||
|
|
2ca8038df2 | ||
|
|
89de328439 | ||
|
|
c37e73b626 | ||
|
|
0283ab11b5 | ||
|
|
5b36ddb12f | ||
|
|
ffc1aa873e | ||
|
|
19b7093438 | ||
|
|
7799ba5c79 | ||
|
|
1c89fcd20a | ||
|
|
730cb78b45 | ||
|
|
8e7f703af7 | ||
|
|
6c14c09880 | ||
|
|
773ab9d7ff | ||
|
|
6c7d87c836 | ||
|
|
e8ab4fd91f | ||
|
|
5d1162fb64 | ||
|
|
216358c6e4 | ||
|
|
57d99130ee | ||
|
|
79afec9737 | ||
|
|
90929baa52 | ||
|
|
85f330c79a | ||
|
|
77d7f764f1 | ||
|
|
a76599bd2a | ||
|
|
4afc67a962 | ||
|
|
042d8b3274 | ||
|
|
1c0a196b9d | ||
|
|
a1fda37896 | ||
|
|
43758a7d60 | ||
|
|
c7d3db9751 | ||
|
|
fca7689e1a | ||
|
|
18bac4e673 | ||
|
|
ca2a9fbf1c | ||
|
|
cbebe7c8de | ||
|
|
0943221902 | ||
|
|
17ed1b7faf | ||
|
|
36d18f28ee | ||
|
|
495d18814a | ||
|
|
257a5a23ec | ||
|
|
919660678b | ||
|
|
19751ed1cb | ||
|
|
04a364dcc3 | ||
|
|
db83ac7eaa | ||
|
|
818c02ed44 | ||
|
|
3ca9dddf61 | ||
|
|
9abdab3991 | ||
|
|
bf74f53ca7 | ||
|
|
9d67efb4a4 | ||
|
|
43122f86e8 | ||
|
|
935337862c | ||
|
|
dd671524f8 | ||
|
|
1cac3d43d3 | ||
|
|
9243c635f2 | ||
|
|
93f820d9da | ||
|
|
b62afcad1f | ||
|
|
7b32aa163f | ||
|
|
eebfd8904e | ||
|
|
0d84a0b976 | ||
|
|
88ac636c00 | ||
|
|
9dc26a8c52 | ||
|
|
6543020fd2 | ||
|
|
be4f3d62cd | ||
|
|
da58c41ab6 | ||
|
|
10d08e641c | ||
|
|
3a39b9f440 | ||
|
|
27f7aab375 | ||
|
|
337da0c467 | ||
|
|
c4e4cebf14 | ||
|
|
3f58800ed1 | ||
|
|
0931ff38e9 | ||
|
|
07bc0612c0 | ||
|
|
6dec485b45 | ||
|
|
a441faf65c | ||
|
|
50ee1462ad | ||
|
|
1cb6ae39c8 | ||
|
|
c667c6e235 | ||
|
|
e3e484e561 | ||
|
|
73fa18a128 | ||
|
|
5c2e3f4e56 | ||
|
|
c47b578599 | ||
|
|
e60d11ee04 | ||
|
|
7ad8314bd7 | ||
|
|
131fcae4c7 | ||
|
|
c8d893fac7 | ||
|
|
b59e5942c8 | ||
|
|
8ff969dd17 | ||
|
|
6eead437d8 | ||
|
|
0b6f83837b | ||
|
|
81eb642f75 | ||
|
|
47b08888ba | ||
|
|
32e34f10ff | ||
|
|
f455b317ec | ||
|
|
08b967607f | ||
|
|
90883bb22b | ||
|
|
0c8b6b7324 | ||
|
|
81d3bdc168 | ||
|
|
54ca4487fa | ||
|
|
25da4d9a8b | ||
|
|
f0add69b61 | ||
|
|
714c7bbd3a | ||
|
|
e0b479efef | ||
|
|
9d09c4c7b0 | ||
|
|
b89411c108 | ||
|
|
c472b82ee3 | ||
|
|
f56b3560c4 | ||
|
|
d2f5313f92 | ||
|
|
572e75b783 | ||
|
|
02dfe11ce6 | ||
|
|
83d06beb70 | ||
|
|
d2a9b312e9 | ||
|
|
b224a2c8a0 | ||
|
|
fe6dfcedf9 | ||
|
|
a8cfc059c8 | ||
|
|
1614b2bab0 | ||
|
|
01260d95f3 | ||
|
|
d69ba6b47a | ||
|
|
098128aafb | ||
|
|
92c9837157 | ||
|
|
18e5f86ffa | ||
|
|
c860645a5a | ||
|
|
fcb93dc7c8 | ||
|
|
fcdb39e428 | ||
|
|
1b3e1863f4 | ||
|
|
fbc2175789 | ||
|
|
8099c431bb | ||
|
|
efbfe0f7af | ||
|
|
66402b474c | ||
|
|
c3986cedfc | ||
|
|
b5a2d3c1c4 | ||
|
|
a6862362c1 | ||
|
|
7d4ec0d633 | ||
|
|
85544c04a9 | ||
|
|
0a8ff6ffad | ||
|
|
04274078c4 | ||
|
|
aac9fbf236 | ||
|
|
08e290f3ea | ||
|
|
86602854ac | ||
|
|
f47f0e05d6 | ||
|
|
4bdec0d214 | ||
|
|
6a7d7e7c2b | ||
|
|
c83a51f7e2 | ||
|
|
b922c8029e | ||
|
|
653761e67d | ||
|
|
d59ff132ab | ||
|
|
e6e740b2a1 | ||
|
|
af6f4e6c8c | ||
|
|
69a0f8d502 | ||
|
|
6d35fb5237 | ||
|
|
6eb63a1e03 | ||
|
|
8774f1a320 | ||
|
|
6ca8ccd330 | ||
|
|
df88ffa159 | ||
|
|
dcbb8ad960 | ||
|
|
0f2ffa9545 | ||
|
|
6bae16f7e9 | ||
|
|
f5ca7ab1c8 | ||
|
|
7dd11decb8 | ||
|
|
79d0f707e6 | ||
|
|
369dc02e78 | ||
|
|
9d2e65b73d | ||
|
|
f421d83627 | ||
|
|
be2ca9d4bb | ||
|
|
17bca662a7 | ||
|
|
1776204870 | ||
|
|
985e214d94 | ||
|
|
2bcc159fd6 | ||
|
|
fb7c12438d | ||
|
|
b2cd363539 | ||
|
|
f668bee88b | ||
|
|
642f2760cc | ||
|
|
37aa8b05f8 | ||
|
|
d640cc1eee | ||
|
|
c2d6e98985 | ||
|
|
84b4fe6176 | ||
|
|
decdf5714b | ||
|
|
9da600caf9 | ||
|
|
45aee2a1c1 | ||
|
|
f5df5ac7d5 | ||
|
|
fb29f4119d | ||
|
|
93795b6eda | ||
|
|
f7b808a9e6 | ||
|
|
4948b443b6 | ||
|
|
448068e318 | ||
|
|
7d81a95156 | ||
|
|
a9bf2ed398 | ||
|
|
771f781e7f | ||
|
|
78be8535f7 | ||
|
|
6c4c1ccb58 | ||
|
|
562225a77b | ||
|
|
b936e1f403 | ||
|
|
b3cc3130f0 | ||
|
|
0363fc4ea1 | ||
|
|
134a96fa32 | ||
|
|
56f444a8a7 |
2
.browserslistrc
Normal file
2
.browserslistrc
Normal file
@@ -0,0 +1,2 @@
|
||||
>0.25%
|
||||
not op_mini all
|
||||
80
.env.example
80
.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,67 +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
|
||||
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
|
||||
|
||||
# External services such as Gravatar and Draw.IO
|
||||
DISABLE_EXTERNAL_SERVICES=false
|
||||
|
||||
# LDAP Settings
|
||||
LDAP_SERVER=false
|
||||
LDAP_BASE_DN=false
|
||||
LDAP_DN=false
|
||||
LDAP_PASS=false
|
||||
LDAP_USER_FILTER=false
|
||||
LDAP_VERSION=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.
|
||||
222
.env.example.complete
Normal file
222
.env.example.complete
Normal file
@@ -0,0 +1,222 @@
|
||||
# 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
|
||||
|
||||
# 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
|
||||
|
||||
21
.github/ISSUE_TEMPLATE.md
vendored
21
.github/ISSUE_TEMPLATE.md
vendored
@@ -1,21 +0,0 @@
|
||||
### For Feature Requests
|
||||
|
||||
Desired Feature:
|
||||
|
||||
### For Bug Reports
|
||||
|
||||
* BookStack Version *(Found in settings, Please don't put 'latest')*:
|
||||
* PHP Version:
|
||||
* MySQL Version:
|
||||
|
||||
##### Expected Behavior
|
||||
|
||||
|
||||
|
||||
##### Current Behavior
|
||||
|
||||
|
||||
|
||||
##### Steps to Reproduce
|
||||
|
||||
|
||||
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Steps To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Your Configuration (please complete the following information):**
|
||||
- Exact BookStack Version (Found in settings):
|
||||
- PHP Version:
|
||||
- Hosting Method (Nginx/Apache/Docker):
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
|
||||
---
|
||||
|
||||
**Describe the feature you'd like**
|
||||
A clear description of the feature you'd like implemented in BookStack.
|
||||
|
||||
**Describe the benefits this feature would bring to BookStack users**
|
||||
Explain the measurable benefits this feature would achieve.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
@@ -1,6 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace BookStack;
|
||||
namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
|
||||
/**
|
||||
* @property string key
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
use Session;
|
||||
|
||||
class ActivityService
|
||||
@@ -12,7 +12,7 @@ class ActivityService
|
||||
|
||||
/**
|
||||
* ActivityService constructor.
|
||||
* @param Activity $activity
|
||||
* @param \BookStack\Actions\Activity $activity
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(Activity $activity, PermissionService $permissionService)
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Ownable;
|
||||
|
||||
class Comment extends Ownable
|
||||
{
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Comment;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Entities\Entity;
|
||||
|
||||
/**
|
||||
* Class CommentRepo
|
||||
@@ -11,13 +10,13 @@ class CommentRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Comment $comment
|
||||
* @var \BookStack\Actions\Comment $comment
|
||||
*/
|
||||
protected $comment;
|
||||
|
||||
/**
|
||||
* CommentRepo constructor.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
*/
|
||||
public function __construct(Comment $comment)
|
||||
{
|
||||
@@ -27,7 +26,7 @@ class CommentRepo
|
||||
/**
|
||||
* Get a comment by ID.
|
||||
* @param $id
|
||||
* @return Comment|\Illuminate\Database\Eloquent\Model
|
||||
* @return \BookStack\Actions\Comment|\Illuminate\Database\Eloquent\Model
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
@@ -36,9 +35,9 @@ class CommentRepo
|
||||
|
||||
/**
|
||||
* Create a new comment on an entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param array $data
|
||||
* @return Comment
|
||||
* @return \BookStack\Actions\Comment
|
||||
*/
|
||||
public function create(Entity $entity, $data = [])
|
||||
{
|
||||
@@ -53,7 +52,7 @@ class CommentRepo
|
||||
|
||||
/**
|
||||
* Update an existing comment.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
* @param array $input
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -66,7 +65,7 @@ class CommentRepo
|
||||
|
||||
/**
|
||||
* Delete a comment from the system.
|
||||
* @param Comment $comment
|
||||
* @param \BookStack\Actions\Comment $comment
|
||||
* @return mixed
|
||||
*/
|
||||
public function delete($comment)
|
||||
@@ -76,7 +75,7 @@ class CommentRepo
|
||||
|
||||
/**
|
||||
* Get the next local ID relative to the linked entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return int
|
||||
*/
|
||||
protected function getNextLocalId(Entity $entity)
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
/**
|
||||
* Class Attribute
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Tag;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
|
||||
/**
|
||||
* Class TagRepo
|
||||
@@ -17,9 +16,9 @@ class TagRepo
|
||||
|
||||
/**
|
||||
* TagRepo constructor.
|
||||
* @param Tag $attr
|
||||
* @param Entity $ent
|
||||
* @param PermissionService $ps
|
||||
* @param \BookStack\Actions\Tag $attr
|
||||
* @param \BookStack\Entities\Entity $ent
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $ps
|
||||
*/
|
||||
public function __construct(Tag $attr, Entity $ent, PermissionService $ps)
|
||||
{
|
||||
@@ -107,7 +106,7 @@ class TagRepo
|
||||
|
||||
/**
|
||||
* Save an array of tags to an entity
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param array $tags
|
||||
* @return array|\Illuminate\Database\Eloquent\Collection
|
||||
*/
|
||||
@@ -128,7 +127,7 @@ class TagRepo
|
||||
/**
|
||||
* Create a new Tag instance from user input.
|
||||
* @param $input
|
||||
* @return Tag
|
||||
* @return \BookStack\Actions\Tag
|
||||
*/
|
||||
protected function newInstanceFromInput($input)
|
||||
{
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class View extends Model
|
||||
{
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Actions;
|
||||
|
||||
use BookStack\Entity;
|
||||
use BookStack\View;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
|
||||
class ViewService
|
||||
{
|
||||
@@ -10,8 +10,8 @@ class ViewService
|
||||
|
||||
/**
|
||||
* ViewService constructor.
|
||||
* @param View $view
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Actions\View $view
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(View $view, PermissionService $permissionService)
|
||||
{
|
||||
@@ -50,12 +50,13 @@ class ViewService
|
||||
* Get the entities with the most views.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|false|array $filterModel
|
||||
* @param Entity|false|array $filterModel
|
||||
* @param string $action - used for permission checking
|
||||
* @return
|
||||
*/
|
||||
public function getPopular($count = 10, $page = 0, $filterModel = false, $action = 'view')
|
||||
{
|
||||
// TODO - Standardise input filter
|
||||
$skipCount = $count * $page;
|
||||
$query = $this->permissionService->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type', $action)
|
||||
->select('*', 'viewable_id', 'viewable_type', \DB::raw('SUM(views) as view_count'))
|
||||
@@ -65,7 +66,7 @@ class ViewService
|
||||
if ($filterModel && is_array($filterModel)) {
|
||||
$query->whereIn('viewable_type', $filterModel);
|
||||
} else if ($filterModel) {
|
||||
$query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query->where('viewable_type', '=', $filterModel->getMorphClass());
|
||||
}
|
||||
|
||||
return $query->with('viewable')->skip($skipCount)->take($count)->get()->pluck('viewable');
|
||||
@@ -89,7 +90,7 @@ class ViewService
|
||||
->filterRestrictedEntityRelations($this->view, 'views', 'viewable_id', 'viewable_type');
|
||||
|
||||
if ($filterModel) {
|
||||
$query = $query->where('viewable_type', '=', get_class($filterModel));
|
||||
$query = $query->where('viewable_type', '=', $filterModel->getMorphClass());
|
||||
}
|
||||
$query = $query->where('user_id', '=', $user->id);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Notifications\ConfirmEmail;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use Carbon\Carbon;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\User;
|
||||
use BookStack\Notifications\ConfirmEmail;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Connection as Database;
|
||||
|
||||
class EmailConfirmationService
|
||||
@@ -16,7 +16,7 @@ class EmailConfirmationService
|
||||
/**
|
||||
* EmailConfirmationService constructor.
|
||||
* @param Database $db
|
||||
* @param UserRepo $users
|
||||
* @param \BookStack\Auth\UserRepo $users
|
||||
*/
|
||||
public function __construct(Database $db, UserRepo $users)
|
||||
{
|
||||
@@ -27,7 +27,7 @@ class EmailConfirmationService
|
||||
/**
|
||||
* Create new confirmation for a user,
|
||||
* Also removes any existing old ones.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
public function sendConfirmation(User $user)
|
||||
@@ -88,7 +88,7 @@ class EmailConfirmationService
|
||||
|
||||
/**
|
||||
* Delete all email confirmations that belong to a user.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function deleteConfirmationsByUser(User $user)
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
/**
|
||||
* Class Ldap
|
||||
@@ -92,4 +92,27 @@ class Ldap
|
||||
{
|
||||
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explode a LDAP dn string into an array of components.
|
||||
* @param string $dn
|
||||
* @param int $withAttrib
|
||||
* @return array
|
||||
*/
|
||||
public function explodeDn(string $dn, int $withAttrib)
|
||||
{
|
||||
return ldap_explode_dn($dn, $withAttrib);
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape a string for use in an LDAP filter.
|
||||
* @param string $value
|
||||
* @param string $ignore
|
||||
* @param int $flags
|
||||
* @return string
|
||||
*/
|
||||
public function escape(string $value, string $ignore = "", int $flags = 0)
|
||||
{
|
||||
return ldap_escape($value, $ignore, $flags);
|
||||
}
|
||||
}
|
||||
405
app/Auth/Access/LdapService.php
Normal file
405
app/Auth/Access/LdapService.php
Normal file
@@ -0,0 +1,405 @@
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Auth\Access;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
* Handles any app-specific LDAP tasks.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
protected $userRepo;
|
||||
protected $enabled;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
* @param Ldap $ldap
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(Access\Ldap $ldap, UserRepo $userRepo)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
$this->userRepo = $userRepo;
|
||||
$this->enabled = config('auth.method') === 'ldap';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if groups should be synced.
|
||||
* @return bool
|
||||
*/
|
||||
public function shouldSyncGroups()
|
||||
{
|
||||
return $this->enabled && $this->config['user_to_groups'] !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for attributes for a specific user on the ldap
|
||||
* @param string $userName
|
||||
* @param array $attributes
|
||||
* @return null|array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getUserWithAttributes($userName, $attributes)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, $attributes);
|
||||
if ($users['count'] === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $users[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
* @param $userName
|
||||
* @return array|null
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails($userName)
|
||||
{
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$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' => $this->getUserResponseProperty($user, 'uid', $user['dn']),
|
||||
'name' => $this->getUserResponseProperty($user, $displayNameAttr, $userCn),
|
||||
'dn' => $user['dn'],
|
||||
'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
|
||||
* @return bool
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
} catch (\ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
||||
return $ldapBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the system user to the LDAP connection using the given credentials
|
||||
* otherwise anonymous access is attempted.
|
||||
* @param $connection
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function bindSystemUser($connection)
|
||||
{
|
||||
$ldapDn = $this->config['dn'];
|
||||
$ldapPass = $this->config['pass'];
|
||||
|
||||
$isAnonymous = ($ldapDn === false || $ldapPass === false);
|
||||
if ($isAnonymous) {
|
||||
$ldapBind = $this->ldap->bind($connection);
|
||||
} else {
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) {
|
||||
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) {
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
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);
|
||||
}
|
||||
|
||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filter string by injecting common variables.
|
||||
* @param string $filterString
|
||||
* @param array $attrs
|
||||
* @return string
|
||||
*/
|
||||
protected function buildFilter($filterString, array $attrs)
|
||||
{
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $attrText) {
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $this->ldap->escape($attrText);
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the groups a user is a part of on ldap
|
||||
* @param string $userName
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserGroups($userName)
|
||||
{
|
||||
$groupsAttr = $this->config['group_attribute'];
|
||||
$user = $this->getUserWithAttributes($userName, [$groupsAttr]);
|
||||
|
||||
if ($user === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$userGroups = $this->groupFilter($user);
|
||||
$userGroups = $this->getGroupsRecursive($userGroups, []);
|
||||
return $userGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent groups of an array of groups
|
||||
* @param array $groupsArray
|
||||
* @param array $checked
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupsRecursive($groupsArray, $checked)
|
||||
{
|
||||
$groups_to_add = [];
|
||||
foreach ($groupsArray as $groupName) {
|
||||
if (in_array($groupName, $checked)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$groupsToAdd = $this->getGroupGroups($groupName);
|
||||
$groups_to_add = array_merge($groups_to_add, $groupsToAdd);
|
||||
$checked[] = $groupName;
|
||||
}
|
||||
$groupsArray = array_unique(array_merge($groupsArray, $groups_to_add), SORT_REGULAR);
|
||||
|
||||
if (!empty($groups_to_add)) {
|
||||
return $this->getGroupsRecursive($groupsArray, $checked);
|
||||
} else {
|
||||
return $groupsArray;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent groups of a single group
|
||||
* @param string $groupName
|
||||
* @return array
|
||||
* @throws LdapException
|
||||
*/
|
||||
private function getGroupGroups($groupName)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$groupsAttr = strtolower($this->config['group_attribute']);
|
||||
|
||||
$groupFilter = 'CN=' . $this->ldap->escape($groupName);
|
||||
$groups = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $groupFilter, [$groupsAttr]);
|
||||
if ($groups['count'] === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$groupGroups = $this->groupFilter($groups[0]);
|
||||
return $groupGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter out LDAP CN and DN language in a ldap search return
|
||||
* Gets the base CN (common name) of the string
|
||||
* @param array $userGroupSearchResponse
|
||||
* @return array
|
||||
*/
|
||||
protected function groupFilter(array $userGroupSearchResponse)
|
||||
{
|
||||
$groupsAttr = strtolower($this->config['group_attribute']);
|
||||
$ldapGroups = [];
|
||||
$count = 0;
|
||||
|
||||
if (isset($userGroupSearchResponse[$groupsAttr]['count'])) {
|
||||
$count = (int) $userGroupSearchResponse[$groupsAttr]['count'];
|
||||
}
|
||||
|
||||
for ($i=0; $i<$count; $i++) {
|
||||
$dnComponents = $this->ldap->explodeDn($userGroupSearchResponse[$groupsAttr][$i], 1);
|
||||
if (!in_array($dnComponents[0], $ldapGroups)) {
|
||||
$ldapGroups[] = $dnComponents[0];
|
||||
}
|
||||
}
|
||||
|
||||
return $ldapGroups;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the LDAP groups to the user roles for the current user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param string $username
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function syncGroups(User $user, string $username)
|
||||
{
|
||||
$userLdapGroups = $this->getUserGroups($username);
|
||||
|
||||
// Get the ids for the roles from the names
|
||||
$ldapGroupsAsRoles = $this->matchLdapGroupsToSystemsRoles($userLdapGroups);
|
||||
|
||||
// Sync groups
|
||||
if ($this->config['remove_from_groups']) {
|
||||
$user->roles()->sync($ldapGroupsAsRoles);
|
||||
$this->userRepo->attachDefaultRole($user);
|
||||
} else {
|
||||
$user->roles()->syncWithoutDetaching($ldapGroupsAsRoles);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Match an array of group names from LDAP to BookStack system roles.
|
||||
* Formats LDAP group names to be lower-case and hyphenated.
|
||||
* @param array $groupNames
|
||||
* @return \Illuminate\Support\Collection
|
||||
*/
|
||||
protected function matchLdapGroupsToSystemsRoles(array $groupNames)
|
||||
{
|
||||
foreach ($groupNames as $i => $groupName) {
|
||||
$groupNames[$i] = str_replace(' ', '-', trim(strtolower($groupName)));
|
||||
}
|
||||
|
||||
$roles = Role::query()->where(function (Builder $query) use ($groupNames) {
|
||||
$query->whereIn('name', $groupNames);
|
||||
foreach ($groupNames as $groupName) {
|
||||
$query->orWhere('external_auth_id', 'LIKE', '%' . $groupName . '%');
|
||||
}
|
||||
})->get();
|
||||
|
||||
$matchedRoles = $roles->filter(function (Role $role) use ($groupNames) {
|
||||
return $this->roleMatchesGroupNames($role, $groupNames);
|
||||
});
|
||||
|
||||
return $matchedRoles->pluck('id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check a role against an array of group names to see if it matches.
|
||||
* Checked against role 'external_auth_id' if set otherwise the name of the role.
|
||||
* @param \BookStack\Auth\Role $role
|
||||
* @param array $groupNames
|
||||
* @return bool
|
||||
*/
|
||||
protected function roleMatchesGroupNames(Role $role, array $groupNames)
|
||||
{
|
||||
if ($role->external_auth_id) {
|
||||
$externalAuthIds = explode(',', strtolower($role->external_auth_id));
|
||||
foreach ($externalAuthIds as $externalAuthId) {
|
||||
if (in_array(trim($externalAuthId), $groupNames)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$roleName = str_replace(' ', '-', trim(strtolower($role->display_name)));
|
||||
return in_array($roleName, $groupNames);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,12 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Access;
|
||||
|
||||
use BookStack\Http\Requests\Request;
|
||||
use GuzzleHttp\Exception\ClientException;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\SocialAccount;
|
||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
|
||||
class SocialAuthService
|
||||
{
|
||||
@@ -16,11 +15,11 @@ class SocialAuthService
|
||||
protected $socialite;
|
||||
protected $socialAccount;
|
||||
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch'];
|
||||
protected $validSocialDrivers = ['google', 'github', 'facebook', 'slack', 'twitter', 'azure', 'okta', 'gitlab', 'twitch', 'discord'];
|
||||
|
||||
/**
|
||||
* SocialAuthService constructor.
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
* @param Socialite $socialite
|
||||
* @param SocialAccount $socialAccount
|
||||
*/
|
||||
@@ -41,7 +40,7 @@ class SocialAuthService
|
||||
public function startLogIn($socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->socialite->driver($driver)->redirect();
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -53,23 +52,18 @@ class SocialAuthService
|
||||
public function startRegister($socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
return $this->socialite->driver($driver)->redirect();
|
||||
return $this->getSocialDriver($driver)->redirect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the social registration process on callback.
|
||||
* @param $socialDriver
|
||||
* @return \Laravel\Socialite\Contracts\User
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return SocialUser
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
public function handleRegistrationCallback($socialDriver)
|
||||
public function handleRegistrationCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
|
||||
// Get user details from social driver
|
||||
$socialUser = $this->socialite->driver($driver)->user();
|
||||
|
||||
// Check social account has not already been used
|
||||
if ($this->socialAccount->where('driver_id', '=', $socialUser->getId())->exists()) {
|
||||
throw new UserRegistrationException(trans('errors.social_account_in_use', ['socialAccount'=>$socialDriver]), '/login');
|
||||
@@ -84,17 +78,26 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the login process on a oAuth callback.
|
||||
* @param $socialDriver
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* Get the social user details via the social driver.
|
||||
* @param string $socialDriver
|
||||
* @return SocialUser
|
||||
* @throws SocialDriverNotConfigured
|
||||
* @throws SocialSignInException
|
||||
*/
|
||||
public function handleLoginCallback($socialDriver)
|
||||
public function getSocialUser(string $socialDriver)
|
||||
{
|
||||
$driver = $this->validateDriver($socialDriver);
|
||||
// Get user details from social driver
|
||||
$socialUser = $this->socialite->driver($driver)->user();
|
||||
return $this->socialite->driver($driver)->user();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the login process on a oAuth callback.
|
||||
* @param $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws SocialSignInAccountNotUsed
|
||||
*/
|
||||
public function handleLoginCallback($socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialId = $socialUser->getId();
|
||||
|
||||
// Get any attached social accounts or users
|
||||
@@ -136,7 +139,7 @@ class SocialAuthService
|
||||
$message .= trans('errors.social_account_register_instructions', ['socialAccount' => title_case($socialDriver)]);
|
||||
}
|
||||
|
||||
throw new SocialSignInException($message, '/login');
|
||||
throw new SocialSignInAccountNotUsed($message, '/login');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -199,8 +202,28 @@ class SocialAuthService
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param \Laravel\Socialite\Contracts\User $socialUser
|
||||
* Check if the current config for the given driver allows auto-registration.
|
||||
* @param string $driver
|
||||
* @return bool
|
||||
*/
|
||||
public function driverAutoRegisterEnabled(string $driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.auto_register') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current config for the given driver allow email address auto-confirmation.
|
||||
* @param string $driver
|
||||
* @return bool
|
||||
*/
|
||||
public function driverAutoConfirmEmailEnabled(string $driver)
|
||||
{
|
||||
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return SocialAccount
|
||||
*/
|
||||
public function fillSocialAccount($socialDriver, $socialUser)
|
||||
@@ -224,4 +247,20 @@ class SocialAuthService
|
||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => title_case($socialDriver)]));
|
||||
return redirect(user()->getEditUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide redirect options per service for the Laravel Socialite driver
|
||||
* @param $driverName
|
||||
* @return \Laravel\Socialite\Contracts\Provider
|
||||
*/
|
||||
public function getSocialDriver(string $driverName)
|
||||
{
|
||||
$driver = $this->socialite->driver($driverName);
|
||||
|
||||
if ($driverName === 'google' && config('services.google.select_account')) {
|
||||
$driver->with(['prompt' => 'select_account']);
|
||||
}
|
||||
|
||||
return $driver;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class EntityPermission extends Model
|
||||
{
|
||||
@@ -1,4 +1,8 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Model;
|
||||
|
||||
class JointPermission extends Model
|
||||
{
|
||||
@@ -1,14 +1,14 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\EntityPermission;
|
||||
use BookStack\JointPermission;
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Ownable;
|
||||
use BookStack\Page;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Query\Builder as QueryBuilder;
|
||||
@@ -22,38 +22,53 @@ class PermissionService
|
||||
protected $userRoles = false;
|
||||
protected $currentUserModel = false;
|
||||
|
||||
public $book;
|
||||
public $chapter;
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var JointPermission
|
||||
*/
|
||||
protected $jointPermission;
|
||||
|
||||
/**
|
||||
* @var Role
|
||||
*/
|
||||
protected $role;
|
||||
|
||||
/**
|
||||
* @var EntityPermission
|
||||
*/
|
||||
protected $entityPermission;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
protected $entityCache;
|
||||
|
||||
/**
|
||||
* PermissionService constructor.
|
||||
* @param JointPermission $jointPermission
|
||||
* @param EntityPermission $entityPermission
|
||||
* @param Connection $db
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param Role $role
|
||||
* @param Connection $db
|
||||
* @param EntityProvider $entityProvider
|
||||
*/
|
||||
public function __construct(JointPermission $jointPermission, EntityPermission $entityPermission, Connection $db, Book $book, Chapter $chapter, Page $page, Role $role)
|
||||
{
|
||||
public function __construct(
|
||||
JointPermission $jointPermission,
|
||||
Permissions\EntityPermission $entityPermission,
|
||||
Role $role,
|
||||
Connection $db,
|
||||
EntityProvider $entityProvider
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->jointPermission = $jointPermission;
|
||||
$this->entityPermission = $entityPermission;
|
||||
$this->role = $role;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
// TODO - Update so admin still goes through filters
|
||||
$this->entityProvider = $entityProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,7 +82,7 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Prepare the local entity cache and ensure it's empty
|
||||
* @param Entity[] $entities
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
*/
|
||||
protected function readyEntityCache($entities = [])
|
||||
{
|
||||
@@ -93,7 +108,7 @@ class PermissionService
|
||||
return $this->entityCache['book']->get($bookId);
|
||||
}
|
||||
|
||||
$book = $this->book->find($bookId);
|
||||
$book = $this->entityProvider->book->find($bookId);
|
||||
if ($book === null) {
|
||||
$book = false;
|
||||
}
|
||||
@@ -104,7 +119,7 @@ class PermissionService
|
||||
/**
|
||||
* Get a chapter via ID, Checks local cache
|
||||
* @param $chapterId
|
||||
* @return Book
|
||||
* @return \BookStack\Entities\Book
|
||||
*/
|
||||
protected function getChapter($chapterId)
|
||||
{
|
||||
@@ -112,7 +127,7 @@ class PermissionService
|
||||
return $this->entityCache['chapter']->get($chapterId);
|
||||
}
|
||||
|
||||
$chapter = $this->chapter->find($chapterId);
|
||||
$chapter = $this->entityProvider->chapter->find($chapterId);
|
||||
if ($chapter === null) {
|
||||
$chapter = false;
|
||||
}
|
||||
@@ -159,6 +174,12 @@ class PermissionService
|
||||
$this->bookFetchQuery()->chunk(5, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -167,11 +188,26 @@ class PermissionService
|
||||
*/
|
||||
protected function bookFetchQuery()
|
||||
{
|
||||
return $this->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']);
|
||||
}]);
|
||||
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']);
|
||||
}]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Collection $shelves
|
||||
* @param array $roles
|
||||
* @param bool $deleteOld
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function buildJointPermissionsForShelves($shelves, $roles, $deleteOld = false)
|
||||
{
|
||||
if ($deleteOld) {
|
||||
$this->deleteManyJointPermissionsForEntities($shelves->all());
|
||||
}
|
||||
$this->createManyJointPermissions($shelves, $roles);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +239,8 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a particular entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntity(Entity $entity)
|
||||
{
|
||||
@@ -214,7 +251,9 @@ class PermissionService
|
||||
return;
|
||||
}
|
||||
|
||||
$entities[] = $entity->book;
|
||||
if ($entity->book) {
|
||||
$entities[] = $entity->book;
|
||||
}
|
||||
|
||||
if ($entity->isA('page') && $entity->chapter_id) {
|
||||
$entities[] = $entity->chapter;
|
||||
@@ -226,13 +265,13 @@ class PermissionService
|
||||
}
|
||||
}
|
||||
|
||||
$this->deleteManyJointPermissionsForEntities($entities);
|
||||
$this->buildJointPermissionsForEntities(collect($entities));
|
||||
}
|
||||
|
||||
/**
|
||||
* Rebuild the entity jointPermissions for a collection of entities.
|
||||
* @param Collection $entities
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function buildJointPermissionsForEntities(Collection $entities)
|
||||
{
|
||||
@@ -254,6 +293,12 @@ class PermissionService
|
||||
$this->bookFetchQuery()->chunk(20, function ($books) use ($roles) {
|
||||
$this->buildJointPermissionsForBooks($books, $roles);
|
||||
});
|
||||
|
||||
// Chunk through all bookshelves
|
||||
$this->entityProvider->bookshelf->newQuery()->select(['id', 'restricted', 'created_by'])
|
||||
->chunk(50, function ($shelves) use ($roles) {
|
||||
$this->buildJointPermissionsForShelves($shelves, $roles);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -289,7 +334,7 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Delete all of the entity jointPermissions for a list of entities.
|
||||
* @param Entity[] $entities
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function deleteManyJointPermissionsForEntities($entities)
|
||||
@@ -370,7 +415,7 @@ class PermissionService
|
||||
|
||||
/**
|
||||
* Get the actions related to an entity.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @return array
|
||||
*/
|
||||
protected function getActions(Entity $entity)
|
||||
@@ -412,7 +457,7 @@ class PermissionService
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $hasAccess, $hasAccess);
|
||||
}
|
||||
|
||||
if ($entity->isA('book')) {
|
||||
if ($entity->isA('book') || $entity->isA('bookshelf')) {
|
||||
return $this->createJointPermissionDataArray($entity, $role, $action, $roleHasPermission, $roleHasPermissionOwn);
|
||||
}
|
||||
|
||||
@@ -456,7 +501,7 @@ class PermissionService
|
||||
/**
|
||||
* Create an array of data with the information of an entity jointPermissions.
|
||||
* Used to build data for bulk insertion.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param Role $role
|
||||
* @param $action
|
||||
* @param $permissionAll
|
||||
@@ -484,11 +529,6 @@ class PermissionService
|
||||
*/
|
||||
public function checkOwnableUserAccess(Ownable $ownable, $permission)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return true;
|
||||
}
|
||||
|
||||
$explodedPermission = explode('-', $permission);
|
||||
|
||||
$baseQuery = $ownable->where('id', '=', $ownable->id);
|
||||
@@ -516,10 +556,43 @@ 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.
|
||||
* @param Entity $entity
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @param $action
|
||||
* @return bool|mixed
|
||||
*/
|
||||
@@ -569,29 +642,30 @@ class PermissionService
|
||||
*/
|
||||
public function bookChildrenQuery($book_id, $filterDrafts = false, $fetchPageContent = false)
|
||||
{
|
||||
$pageSelect = $this->db->table('pages')->selectRaw($this->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);
|
||||
});
|
||||
}
|
||||
});
|
||||
$chapterSelect = $this->db->table('chapters')->selectRaw($this->chapter->entityRawQuery())->where('book_id', '=', $book_id);
|
||||
$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);
|
||||
});
|
||||
}
|
||||
});
|
||||
$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);
|
||||
|
||||
if (!$this->isAdmin()) {
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
// Add joint permission filter
|
||||
$whereQuery = $this->db->table('joint_permissions as jp')->selectRaw('COUNT(*)')
|
||||
->whereRaw('jp.entity_id=U.id')->whereRaw('jp.entity_type=U.entity_type')
|
||||
->where('jp.action', '=', 'view')->whereIn('jp.role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
$query->where('jp.has_permission', '=', 1)->orWhere(function ($query) {
|
||||
$query->where('jp.has_permission_own', '=', 1)->where('jp.created_by', '=', $this->currentUser()->id);
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
}
|
||||
});
|
||||
$query->whereRaw("({$whereQuery->toSql()}) > 0")->mergeBindings($whereQuery);
|
||||
|
||||
$query->orderBy('draft', 'desc')->orderBy('priority', 'asc');
|
||||
$this->clean();
|
||||
@@ -601,7 +675,7 @@ class PermissionService
|
||||
/**
|
||||
* Add restrictions for a generic entity
|
||||
* @param string $entityType
|
||||
* @param Builder|Entity $query
|
||||
* @param Builder|\BookStack\Entities\Entity $query
|
||||
* @param string $action
|
||||
* @return Builder
|
||||
*/
|
||||
@@ -619,11 +693,6 @@ class PermissionService
|
||||
});
|
||||
}
|
||||
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
return $this->entityRestrictionQuery($query);
|
||||
}
|
||||
@@ -639,10 +708,6 @@ class PermissionService
|
||||
*/
|
||||
public function filterRestrictedEntityRelations($query, $tableName, $entityIdColumn, $entityTypeColumn, $action = 'view')
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = $action;
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn, 'entityTypeColumn' => $entityTypeColumn];
|
||||
@@ -675,20 +740,16 @@ class PermissionService
|
||||
*/
|
||||
public function filterRelatedPages($query, $tableName, $entityIdColumn)
|
||||
{
|
||||
if ($this->isAdmin()) {
|
||||
$this->clean();
|
||||
return $query;
|
||||
}
|
||||
|
||||
$this->currentAction = 'view';
|
||||
$tableDetails = ['tableName' => $tableName, 'entityIdColumn' => $entityIdColumn];
|
||||
|
||||
$q = $query->where(function ($query) use ($tableDetails) {
|
||||
$query->where(function ($query) use (&$tableDetails) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails) {
|
||||
$pageMorphClass = $this->entityProvider->page->getMorphClass();
|
||||
$q = $query->where(function ($query) use ($tableDetails, $pageMorphClass) {
|
||||
$query->where(function ($query) use (&$tableDetails, $pageMorphClass) {
|
||||
$query->whereExists(function ($permissionQuery) use (&$tableDetails, $pageMorphClass) {
|
||||
$permissionQuery->select('id')->from('joint_permissions')
|
||||
->whereRaw('joint_permissions.entity_id=' . $tableDetails['tableName'] . '.' . $tableDetails['entityIdColumn'])
|
||||
->where('entity_type', '=', 'Bookstack\\Page')
|
||||
->where('entity_type', '=', $pageMorphClass)
|
||||
->where('action', '=', $this->currentAction)
|
||||
->whereIn('role_id', $this->getRoles())
|
||||
->where(function ($query) {
|
||||
@@ -704,22 +765,9 @@ class PermissionService
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current user is an admin.
|
||||
* @return bool
|
||||
*/
|
||||
private function isAdmin()
|
||||
{
|
||||
if ($this->isAdminUser === null) {
|
||||
$this->isAdminUser = ($this->currentUser()->id !== null) ? $this->currentUser()->hasSystemRole('admin') : false;
|
||||
}
|
||||
|
||||
return $this->isAdminUser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current user
|
||||
* @return User
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
private function currentUser()
|
||||
{
|
||||
@@ -1,10 +1,8 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Permissions;
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\RolePermission;
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\PermissionService;
|
||||
use Setting;
|
||||
|
||||
class PermissionsRepo
|
||||
{
|
||||
@@ -19,9 +17,9 @@ class PermissionsRepo
|
||||
* PermissionsRepo constructor.
|
||||
* @param RolePermission $permission
|
||||
* @param Role $role
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(RolePermission $permission, Role $role, PermissionService $permissionService)
|
||||
public function __construct(RolePermission $permission, Role $role, Permissions\PermissionService $permissionService)
|
||||
{
|
||||
$this->permission = $permission;
|
||||
$this->role = $role;
|
||||
@@ -80,7 +78,7 @@ class PermissionsRepo
|
||||
|
||||
/**
|
||||
* Updates an existing role.
|
||||
* Ensure Admin role always has all permissions.
|
||||
* Ensure Admin role always have core permissions.
|
||||
* @param $roleId
|
||||
* @param $roleData
|
||||
* @throws PermissionsException
|
||||
@@ -90,13 +88,18 @@ class PermissionsRepo
|
||||
$role = $this->role->findOrFail($roleId);
|
||||
|
||||
$permissions = isset($roleData['permissions']) ? array_keys($roleData['permissions']) : [];
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
if ($role->system_name === 'admin') {
|
||||
$permissions = $this->permission->all()->pluck('id')->toArray();
|
||||
$role->permissions()->sync($permissions);
|
||||
$permissions = array_merge($permissions, [
|
||||
'users-manage',
|
||||
'user-roles-manage',
|
||||
'restrictions-manage-all',
|
||||
'restrictions-manage-own',
|
||||
'settings-manage',
|
||||
]);
|
||||
}
|
||||
|
||||
$this->assignRolePermissions($role, $permissions);
|
||||
|
||||
$role->fill($roleData);
|
||||
$role->save();
|
||||
$this->permissionService->buildJointPermissionForRole($role);
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth\Permissions;
|
||||
|
||||
use BookStack\Auth\Role;
|
||||
use BookStack\Model;
|
||||
|
||||
class RolePermission extends Model
|
||||
{
|
||||
@@ -1,9 +1,12 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Model;
|
||||
|
||||
class Role extends Model
|
||||
{
|
||||
|
||||
protected $fillable = ['display_name', 'description'];
|
||||
protected $fillable = ['display_name', 'description', 'external_auth_id'];
|
||||
|
||||
/**
|
||||
* The roles that belong to the role.
|
||||
@@ -27,7 +30,7 @@ class Role extends Model
|
||||
*/
|
||||
public function permissions()
|
||||
{
|
||||
return $this->belongsToMany(RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
return $this->belongsToMany(Permissions\RolePermission::class, 'permission_role', 'role_id', 'permission_id');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -48,18 +51,18 @@ class Role extends Model
|
||||
|
||||
/**
|
||||
* Add a permission to this role.
|
||||
* @param RolePermission $permission
|
||||
* @param \BookStack\Auth\Permissions\RolePermission $permission
|
||||
*/
|
||||
public function attachPermission(RolePermission $permission)
|
||||
public function attachPermission(Permissions\RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->attach($permission->id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach a single permission from this role.
|
||||
* @param RolePermission $permission
|
||||
* @param \BookStack\Auth\Permissions\RolePermission $permission
|
||||
*/
|
||||
public function detachPermission(RolePermission $permission)
|
||||
public function detachPermission(Permissions\RolePermission $permission)
|
||||
{
|
||||
$this->permissions()->detach($permission->id);
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class SocialAccount extends Model
|
||||
{
|
||||
@@ -1,6 +1,8 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use BookStack\Model;
|
||||
use BookStack\Notifications\ResetPassword;
|
||||
use BookStack\Uploads\Image;
|
||||
use Illuminate\Auth\Authenticatable;
|
||||
use Illuminate\Auth\Passwords\CanResetPassword;
|
||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Auth;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Image;
|
||||
use BookStack\Role;
|
||||
use BookStack\User;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use BookStack\Uploads\Image;
|
||||
use Exception;
|
||||
use Images;
|
||||
|
||||
@@ -43,7 +43,7 @@ class UserRepo
|
||||
*/
|
||||
public function getById($id)
|
||||
{
|
||||
return $this->user->findOrFail($id);
|
||||
return $this->user->newQuery()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,33 +76,31 @@ class UserRepo
|
||||
return $query->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Creates a new user and attaches a role to them.
|
||||
* @param array $data
|
||||
* @return User
|
||||
* @param boolean $verifyEmail
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
public function registerNew(array $data)
|
||||
public function registerNew(array $data, $verifyEmail = false)
|
||||
{
|
||||
$user = $this->create($data);
|
||||
$user = $this->create($data, $verifyEmail);
|
||||
$this->attachDefaultRole($user);
|
||||
|
||||
// Get avatar from gravatar and save
|
||||
$this->downloadGravatarToUserAvatar($user);
|
||||
$this->downloadAndAssignUserAvatar($user);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Give a user the default role. Used when creating a new user.
|
||||
* @param $user
|
||||
* @param User $user
|
||||
*/
|
||||
public function attachDefaultRole($user)
|
||||
public function attachDefaultRole(User $user)
|
||||
{
|
||||
$roleId = setting('registration-role');
|
||||
if ($roleId === false) {
|
||||
$roleId = $this->role->first()->id;
|
||||
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
$user->attachRoleId($roleId);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,7 +120,7 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Checks if the give user is the only admin.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function isOnlyAdmin(User $user)
|
||||
@@ -138,24 +136,59 @@ class UserRepo
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the assigned user roles via an array of role IDs.
|
||||
* @param User $user
|
||||
* @param array $roles
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function setUserRoles(User $user, array $roles)
|
||||
{
|
||||
if ($this->demotingLastAdmin($user, $roles)) {
|
||||
throw new UserUpdateException(trans('errors.role_cannot_remove_only_admin'), $user->getEditUrl());
|
||||
}
|
||||
|
||||
$user->roles()->sync($roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given user is the last admin and their new roles no longer
|
||||
* contains the admin role.
|
||||
* @param User $user
|
||||
* @param array $newRoles
|
||||
* @return bool
|
||||
*/
|
||||
protected function demotingLastAdmin(User $user, array $newRoles) : bool
|
||||
{
|
||||
if ($this->isOnlyAdmin($user)) {
|
||||
$adminRole = $this->role->getSystemRole('admin');
|
||||
if (!in_array(strval($adminRole->id), $newRoles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new basic instance of user.
|
||||
* @param array $data
|
||||
* @return User
|
||||
* @param boolean $verifyEmail
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
public function create(array $data)
|
||||
public function create(array $data, $verifyEmail = false)
|
||||
{
|
||||
return $this->user->forceCreate([
|
||||
'name' => $data['name'],
|
||||
'email' => $data['email'],
|
||||
'password' => bcrypt($data['password']),
|
||||
'email_confirmed' => false
|
||||
'email_confirmed' => $verifyEmail
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the given user from storage, Delete all related content.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @throws Exception
|
||||
*/
|
||||
public function destroy(User $user)
|
||||
@@ -172,7 +205,7 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Get the latest activity for a user.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return array
|
||||
@@ -184,7 +217,7 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Get the recently created content for this given user.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
@@ -205,15 +238,15 @@ class UserRepo
|
||||
|
||||
/**
|
||||
* Get asset created counts for the give user.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @return array
|
||||
*/
|
||||
public function getAssetCounts(User $user)
|
||||
{
|
||||
return [
|
||||
'pages' => $this->entityRepo->page->where('created_by', '=', $user->id)->count(),
|
||||
'chapters' => $this->entityRepo->chapter->where('created_by', '=', $user->id)->count(),
|
||||
'books' => $this->entityRepo->book->where('created_by', '=', $user->id)->count(),
|
||||
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
|
||||
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
|
||||
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -237,25 +270,24 @@ class UserRepo
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a gravatar image for a user and set it as their avatar.
|
||||
* Does not run if gravatar disabled in config.
|
||||
* Get an avatar image for a user and set it as their avatar.
|
||||
* Returns early if avatars disabled or not set in config.
|
||||
* @param User $user
|
||||
* @return bool
|
||||
*/
|
||||
public function downloadGravatarToUserAvatar(User $user)
|
||||
public function downloadAndAssignUserAvatar(User $user)
|
||||
{
|
||||
// Get avatar from gravatar and save
|
||||
if (!config('services.gravatar')) {
|
||||
if (!Images::avatarFetchEnabled()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$avatar = Images::saveUserGravatar($user);
|
||||
$avatar = Images::saveUserAvatar($user);
|
||||
$user->avatar()->associate($avatar);
|
||||
$user->save();
|
||||
return true;
|
||||
} catch (Exception $e) {
|
||||
\Log::error('Failed to save user gravatar image');
|
||||
\Log::error('Failed to save user avatar image');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
@@ -30,7 +30,7 @@ class CleanupImages extends Command
|
||||
|
||||
/**
|
||||
* Create a new command instance.
|
||||
* @param ImageService $imageService
|
||||
* @param \BookStack\Uploads\ImageService $imageService
|
||||
*/
|
||||
public function __construct(ImageService $imageService)
|
||||
{
|
||||
@@ -72,7 +72,9 @@ class CleanupImages extends Command
|
||||
|
||||
protected function showDeletedImages($paths)
|
||||
{
|
||||
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) return;
|
||||
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
|
||||
return;
|
||||
}
|
||||
if (count($paths) > 0) {
|
||||
$this->line('Images to delete:');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Actions\Activity;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearActivity extends Command
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\PageRevision;
|
||||
use BookStack\Entities\PageRevision;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class ClearRevisions extends Command
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class CreateAdmin extends Command
|
||||
@@ -76,7 +76,7 @@ class CreateAdmin extends Command
|
||||
|
||||
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
||||
$this->userRepo->attachSystemRole($user, 'admin');
|
||||
$this->userRepo->downloadGravatarToUserAvatar($user);
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
$user->email_confirmed = true;
|
||||
$user->save();
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\User;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class DeleteUsers extends Command
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegeneratePermissions extends Command
|
||||
@@ -31,7 +31,7 @@ class RegeneratePermissions extends Command
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param PermissionService $permissionService
|
||||
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(PermissionService $permissionService)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
namespace BookStack\Console\Commands;
|
||||
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Entities\SearchService;
|
||||
use Illuminate\Console\Command;
|
||||
|
||||
class RegenerateSearch extends Command
|
||||
@@ -26,7 +26,7 @@ class RegenerateSearch extends Command
|
||||
/**
|
||||
* Create a new command instance.
|
||||
*
|
||||
* @param SearchService $searchService
|
||||
* @param \BookStack\Entities\SearchService $searchService
|
||||
*/
|
||||
public function __construct(SearchService $searchService)
|
||||
{
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
|
||||
class Book extends Entity
|
||||
{
|
||||
@@ -6,6 +8,15 @@ class Book extends Entity
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Book';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this book.
|
||||
* @param string|bool $path
|
||||
@@ -48,14 +59,6 @@ class Book extends Entity
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
/*
|
||||
* Get the edit url for this book.
|
||||
* @return string
|
||||
*/
|
||||
public function getEditUrl()
|
||||
{
|
||||
return $this->getUrl() . '/edit';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all pages within this book.
|
||||
@@ -75,6 +78,15 @@ class Book extends Entity
|
||||
return $this->hasMany(Chapter::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shelves this book is contained within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function shelves()
|
||||
{
|
||||
return $this->belongsToMany(Bookshelf::class, 'bookshelves_books', 'book_id', 'bookshelf_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
94
app/Entities/Bookshelf.php
Normal file
94
app/Entities/Bookshelf.php
Normal file
@@ -0,0 +1,94 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Image;
|
||||
|
||||
class Bookshelf extends Entity
|
||||
{
|
||||
protected $table = 'bookshelves';
|
||||
|
||||
public $searchFactor = 3;
|
||||
|
||||
protected $fillable = ['name', 'description', 'image_id'];
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Bookshelf';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the books in this shelf.
|
||||
* Should not be used directly since does not take into account permissions.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
|
||||
*/
|
||||
public function books()
|
||||
{
|
||||
return $this->belongsToMany(Book::class, 'bookshelves_books', 'bookshelf_id', 'book_id')->orderBy('order', 'asc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the url for this bookshelf.
|
||||
* @param string|bool $path
|
||||
* @return string
|
||||
*/
|
||||
public function getUrl($path = false)
|
||||
{
|
||||
if ($path !== false) {
|
||||
return baseUrl('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||
}
|
||||
return baseUrl('/shelves/' . urlencode($this->slug));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns BookShelf cover image, if cover does not exists return default cover image.
|
||||
* @param int $width - Width of the image
|
||||
* @param int $height - Height of the image
|
||||
* @return string
|
||||
*/
|
||||
public function getBookCover($width = 440, $height = 250)
|
||||
{
|
||||
$default = baseUrl('/book_default_cover.png');
|
||||
if (!$this->image_id) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
try {
|
||||
$cover = $this->cover ? baseUrl($this->cover->getThumb($width, $height, false)) : $default;
|
||||
} catch (\Exception $err) {
|
||||
$cover = $default;
|
||||
}
|
||||
return $cover;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the cover image of the book
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function cover()
|
||||
{
|
||||
return $this->belongsTo(Image::class, 'image_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an excerpt of this book's description to the specified length or less.
|
||||
* @param int $length
|
||||
* @return string
|
||||
*/
|
||||
public function getExcerpt($length = 100)
|
||||
{
|
||||
$description = $this->description;
|
||||
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||
* @return string
|
||||
*/
|
||||
public function entityRawQuery()
|
||||
{
|
||||
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";
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
class Chapter extends Entity
|
||||
{
|
||||
@@ -6,6 +6,15 @@ class Chapter extends Entity
|
||||
|
||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Chapter';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the book this chapter is within.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
@@ -1,7 +1,31 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Actions\Activity;
|
||||
use BookStack\Actions\Comment;
|
||||
use BookStack\Actions\Tag;
|
||||
use BookStack\Actions\View;
|
||||
use BookStack\Auth\Permissions\EntityPermission;
|
||||
use BookStack\Auth\Permissions\JointPermission;
|
||||
use BookStack\Ownable;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
|
||||
/**
|
||||
* Class Entity
|
||||
* The base class for book-like items such as pages, chapters & books.
|
||||
* This is not a database model in itself but extended.
|
||||
*
|
||||
* @property integer $id
|
||||
* @property string $name
|
||||
* @property string $slug
|
||||
* @property Carbon $created_at
|
||||
* @property Carbon $updated_at
|
||||
* @property int $created_by
|
||||
* @property int $updated_by
|
||||
* @property boolean $restricted
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class Entity extends Ownable
|
||||
{
|
||||
|
||||
@@ -15,6 +39,17 @@ class Entity extends Ownable
|
||||
*/
|
||||
public $searchFactor = 1.0;
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* Set here since, due to folder changes, the namespace used
|
||||
* in the database no longer matches the class namespace.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Entity';
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this entity to another given entity.
|
||||
* Matches by comparing class and id.
|
||||
@@ -152,13 +187,13 @@ class Entity extends Ownable
|
||||
*/
|
||||
public static function getEntityInstance($type)
|
||||
{
|
||||
$types = ['Page', 'Book', 'Chapter'];
|
||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||
if (!in_array($className, $types)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return app('BookStack\\' . $className);
|
||||
return app('BookStack\\Entities\\' . $className);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,10 +203,10 @@ class Entity extends Ownable
|
||||
*/
|
||||
public function getShortName($length = 25)
|
||||
{
|
||||
if (strlen($this->name) <= $length) {
|
||||
if (mb_strlen($this->name) <= $length) {
|
||||
return $this->name;
|
||||
}
|
||||
return substr($this->name, 0, $length - 3) . '...';
|
||||
return mb_substr($this->name, 0, $length - 3) . '...';
|
||||
}
|
||||
|
||||
/**
|
||||
87
app/Entities/EntityProvider.php
Normal file
87
app/Entities/EntityProvider.php
Normal file
@@ -0,0 +1,87 @@
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
/**
|
||||
* Class EntityProvider
|
||||
*
|
||||
* Provides access to the core entity models.
|
||||
* Wrapped up in this provider since they are often used together
|
||||
* so this is a neater alternative to injecting all in individually.
|
||||
*
|
||||
* @package BookStack\Entities
|
||||
*/
|
||||
class EntityProvider
|
||||
{
|
||||
|
||||
/**
|
||||
* @var Bookshelf
|
||||
*/
|
||||
public $bookshelf;
|
||||
|
||||
/**
|
||||
* @var Book
|
||||
*/
|
||||
public $book;
|
||||
|
||||
/**
|
||||
* @var Chapter
|
||||
*/
|
||||
public $chapter;
|
||||
|
||||
/**
|
||||
* @var Page
|
||||
*/
|
||||
public $page;
|
||||
|
||||
/**
|
||||
* @var PageRevision
|
||||
*/
|
||||
public $pageRevision;
|
||||
|
||||
/**
|
||||
* EntityProvider constructor.
|
||||
* @param Bookshelf $bookshelf
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param PageRevision $pageRevision
|
||||
*/
|
||||
public function __construct(
|
||||
Bookshelf $bookshelf,
|
||||
Book $book,
|
||||
Chapter $chapter,
|
||||
Page $page,
|
||||
PageRevision $pageRevision
|
||||
) {
|
||||
$this->bookshelf = $bookshelf;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->pageRevision = $pageRevision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch all core entity types as an associated array
|
||||
* with their basic names as the keys.
|
||||
* @return Entity[]
|
||||
*/
|
||||
public function all()
|
||||
{
|
||||
return [
|
||||
'bookshelf' => $this->bookshelf,
|
||||
'book' => $this->book,
|
||||
'chapter' => $this->chapter,
|
||||
'page' => $this->page,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance by it's basic name.
|
||||
* @param string $type
|
||||
* @return Entity
|
||||
*/
|
||||
public function get(string $type)
|
||||
{
|
||||
$type = strtolower($type);
|
||||
return $this->all()[$type];
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Page;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Uploads\ImageService;
|
||||
|
||||
class ExportService
|
||||
{
|
||||
@@ -13,7 +11,8 @@ class ExportService
|
||||
|
||||
/**
|
||||
* ExportService constructor.
|
||||
* @param $entityRepo
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ImageService $imageService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, ImageService $imageService)
|
||||
{
|
||||
@@ -24,7 +23,7 @@ class ExportService
|
||||
/**
|
||||
* Convert a page to a self-contained HTML file.
|
||||
* Includes required CSS & image content. Images are base64 encoded into the HTML.
|
||||
* @param Page $page
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -39,7 +38,7 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter to a self-contained HTML file.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -89,7 +88,7 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter to a PDF file.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return mixed|string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -108,7 +107,7 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a book to a PDF file
|
||||
* @param Book $book
|
||||
* @param \BookStack\Entities\Book $book
|
||||
* @return string
|
||||
* @throws \Throwable
|
||||
*/
|
||||
@@ -208,7 +207,7 @@ class ExportService
|
||||
|
||||
/**
|
||||
* Convert a chapter into a plain text string.
|
||||
* @param Chapter $chapter
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return string
|
||||
*/
|
||||
public function chapterToPlainText(Chapter $chapter)
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Uploads\Attachment;
|
||||
|
||||
class Page extends Entity
|
||||
{
|
||||
@@ -8,6 +10,15 @@ class Page extends Entity
|
||||
|
||||
public $textField = 'text';
|
||||
|
||||
/**
|
||||
* Get the morph class for this model.
|
||||
* @return string
|
||||
*/
|
||||
public function getMorphClass()
|
||||
{
|
||||
return 'BookStack\\Page';
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts this page into a simplified array.
|
||||
* @return mixed
|
||||
@@ -28,6 +39,15 @@ class Page extends Entity
|
||||
return $this->belongsTo(Book::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parent item
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function parent()
|
||||
{
|
||||
return $this->chapter_id ? $this->chapter() : $this->book();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the chapter that this page is in, If applicable.
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
@@ -103,4 +123,13 @@ class Page extends Entity
|
||||
$htmlQuery = $withContent ? 'html' : "'' as html";
|
||||
return "'BookStack\\\\Page' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, {$htmlQuery}, book_id, priority, chapter_id, draft, created_by, updated_by, updated_at, created_at";
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current revision for the page if existing
|
||||
* @return \BookStack\Entities\PageRevision|null
|
||||
*/
|
||||
public function getCurrentRevision()
|
||||
{
|
||||
return $this->revisions()->first();
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Model;
|
||||
|
||||
class PageRevision extends Model
|
||||
{
|
||||
832
app/Entities/Repos/EntityRepo.php
Normal file
832
app/Entities/Repos/EntityRepo.php
Normal file
@@ -0,0 +1,832 @@
|
||||
<?php namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Actions\TagRepo;
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\EntityProvider;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Entities\SearchService;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Exceptions\NotifyException;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use DOMDocument;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
|
||||
/**
|
||||
* @var ViewService
|
||||
*/
|
||||
protected $viewService;
|
||||
|
||||
/**
|
||||
* @var TagRepo
|
||||
*/
|
||||
protected $tagRepo;
|
||||
|
||||
/**
|
||||
* @var SearchService
|
||||
*/
|
||||
protected $searchService;
|
||||
|
||||
/**
|
||||
* EntityRepo constructor.
|
||||
* @param EntityProvider $entityProvider
|
||||
* @param ViewService $viewService
|
||||
* @param PermissionService $permissionService
|
||||
* @param TagRepo $tagRepo
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
public function __construct(
|
||||
EntityProvider $entityProvider,
|
||||
ViewService $viewService,
|
||||
PermissionService $permissionService,
|
||||
TagRepo $tagRepo,
|
||||
SearchService $searchService
|
||||
) {
|
||||
$this->entityProvider = $entityProvider;
|
||||
$this->viewService = $viewService;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->tagRepo = $tagRepo;
|
||||
$this->searchService = $searchService;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base query for searching entities via permission system
|
||||
* @param string $type
|
||||
* @param bool $allowDrafts
|
||||
* @param string $permission
|
||||
* @return \Illuminate\Database\Query\Builder
|
||||
*/
|
||||
protected function entityQuery($type, $allowDrafts = false, $permission = 'view')
|
||||
{
|
||||
$q = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type), $permission);
|
||||
if (strtolower($type) === 'page' && !$allowDrafts) {
|
||||
$q = $q->where('draft', '=', false);
|
||||
}
|
||||
return $q;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entity with the given id exists.
|
||||
* @param $type
|
||||
* @param $id
|
||||
* @return bool
|
||||
*/
|
||||
public function exists($type, $id)
|
||||
{
|
||||
return $this->entityQuery($type)->where('id', '=', $id)->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity by ID
|
||||
* @param string $type
|
||||
* @param integer $id
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return \BookStack\Entities\Entity
|
||||
*/
|
||||
public function getById($type, $id, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
$query = $this->entityQuery($type, $allowDrafts);
|
||||
|
||||
if ($ignorePermissions) {
|
||||
$query = $this->entityProvider->get($type)->newQuery();
|
||||
}
|
||||
|
||||
return $query->find($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $type
|
||||
* @param []int $ids
|
||||
* @param bool $allowDrafts
|
||||
* @param bool $ignorePermissions
|
||||
* @return \Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|Collection
|
||||
*/
|
||||
public function getManyById($type, $ids, $allowDrafts = false, $ignorePermissions = false)
|
||||
{
|
||||
$query = $this->entityQuery($type, $allowDrafts);
|
||||
|
||||
if ($ignorePermissions) {
|
||||
$query = $this->entityProvider->get($type)->newQuery();
|
||||
}
|
||||
|
||||
return $query->whereIn('id', $ids)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity by its url slug.
|
||||
* @param string $type
|
||||
* @param string $slug
|
||||
* @param string|bool $bookSlug
|
||||
* @return \BookStack\Entities\Entity
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function getBySlug($type, $slug, $bookSlug = false)
|
||||
{
|
||||
$q = $this->entityQuery($type)->where('slug', '=', $slug);
|
||||
|
||||
if (strtolower($type) === 'chapter' || strtolower($type) === 'page') {
|
||||
$q = $q->where('book_id', '=', function ($query) use ($bookSlug) {
|
||||
$query->select('id')
|
||||
->from($this->entityProvider->book->getTable())
|
||||
->where('slug', '=', $bookSlug)->limit(1);
|
||||
});
|
||||
}
|
||||
$entity = $q->first();
|
||||
if ($entity === null) {
|
||||
throw new NotFoundException(trans('errors.' . strtolower($type) . '_not_found'));
|
||||
}
|
||||
return $entity;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get all entities of a type with the given permission, limited by count unless count is false.
|
||||
* @param string $type
|
||||
* @param integer|bool $count
|
||||
* @param string $permission
|
||||
* @return Collection
|
||||
*/
|
||||
public function getAll($type, $count = 20, $permission = 'view')
|
||||
{
|
||||
$q = $this->entityQuery($type, false, $permission)->orderBy('name', 'asc');
|
||||
if ($count !== false) {
|
||||
$q = $q->take($count);
|
||||
}
|
||||
return $q->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all entities in a paginated format
|
||||
* @param $type
|
||||
* @param int $count
|
||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
||||
*/
|
||||
public function getAllPaginated($type, $count = 10)
|
||||
{
|
||||
return $this->entityQuery($type)->orderBy('name', 'asc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently created entities of the given type.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyCreated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
|
||||
->orderBy('created_at', 'desc');
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently updated entities of the given type.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @param bool|callable $additionalQuery
|
||||
* @return Collection
|
||||
*/
|
||||
public function getRecentlyUpdated($type, $count = 20, $page = 0, $additionalQuery = false)
|
||||
{
|
||||
$query = $this->permissionService->enforceEntityRestrictions($type, $this->entityProvider->get($type))
|
||||
->orderBy('updated_at', 'desc');
|
||||
if (strtolower($type) === 'page') {
|
||||
$query = $query->where('draft', '=', false);
|
||||
}
|
||||
if ($additionalQuery !== false && is_callable($additionalQuery)) {
|
||||
$additionalQuery($query);
|
||||
}
|
||||
return $query->skip($page * $count)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most recently viewed entities.
|
||||
* @param string|bool $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyViewed($type, $count = 10, $page = 0)
|
||||
{
|
||||
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
|
||||
return $this->viewService->getUserRecentlyViewed($count, $page, $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system with pagination.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyCreatedPaginated($type, $count = 20)
|
||||
{
|
||||
return $this->entityQuery($type)->orderBy('created_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest pages added to the system with pagination.
|
||||
* @param string $type
|
||||
* @param int $count
|
||||
* @return mixed
|
||||
*/
|
||||
public function getRecentlyUpdatedPaginated($type, $count = 20)
|
||||
{
|
||||
return $this->entityQuery($type)->orderBy('updated_at', 'desc')->paginate($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most popular entities base on all views.
|
||||
* @param string|bool $type
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return mixed
|
||||
*/
|
||||
public function getPopular($type, $count = 10, $page = 0)
|
||||
{
|
||||
$filter = is_bool($type) ? false : $this->entityProvider->get($type);
|
||||
return $this->viewService->getPopular($count, $page, $filter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft pages owned by the current user.
|
||||
* @param int $count
|
||||
* @param int $page
|
||||
* @return Collection
|
||||
*/
|
||||
public function getUserDraftPages($count = 20, $page = 0)
|
||||
{
|
||||
return $this->entityProvider->page->where('draft', '=', true)
|
||||
->where('created_by', '=', user()->id)
|
||||
->orderBy('updated_at', 'desc')
|
||||
->skip($count * $page)->take($count)->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the number of entities the given user has created.
|
||||
* @param string $type
|
||||
* @param User $user
|
||||
* @return int
|
||||
*/
|
||||
public function getUserTotalCreated(string $type, User $user)
|
||||
{
|
||||
return $this->entityProvider->get($type)
|
||||
->where('created_by', '=', $user->id)->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param \BookStack\Entities\Bookshelf $bookshelf
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getBookshelfChildren(Bookshelf $bookshelf)
|
||||
{
|
||||
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 bool $filterDrafts
|
||||
* @param bool $renderPages
|
||||
* @return mixed
|
||||
*/
|
||||
public function getBookChildren(Book $book, $filterDrafts = false, $renderPages = false)
|
||||
{
|
||||
$q = $this->permissionService->bookChildrenQuery($book->id, $filterDrafts, $renderPages)->get();
|
||||
$entities = [];
|
||||
$parents = [];
|
||||
$tree = [];
|
||||
|
||||
foreach ($q as $index => $rawEntity) {
|
||||
if ($rawEntity->entity_type === $this->entityProvider->page->getMorphClass()) {
|
||||
$entities[$index] = $this->entityProvider->page->newFromBuilder($rawEntity);
|
||||
if ($renderPages) {
|
||||
$entities[$index]->html = $rawEntity->html;
|
||||
$entities[$index]->html = $this->renderPage($entities[$index]);
|
||||
};
|
||||
} else if ($rawEntity->entity_type === $this->entityProvider->chapter->getMorphClass()) {
|
||||
$entities[$index] = $this->entityProvider->chapter->newFromBuilder($rawEntity);
|
||||
$key = $entities[$index]->entity_type . ':' . $entities[$index]->id;
|
||||
$parents[$key] = $entities[$index];
|
||||
$parents[$key]->setAttribute('pages', collect());
|
||||
}
|
||||
if ($entities[$index]->chapter_id === 0 || $entities[$index]->chapter_id === '0') {
|
||||
$tree[] = $entities[$index];
|
||||
}
|
||||
$entities[$index]->book = $book;
|
||||
}
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
if ($entity->chapter_id === 0 || $entity->chapter_id === '0') {
|
||||
continue;
|
||||
}
|
||||
$parentKey = $this->entityProvider->chapter->getMorphClass() . ':' . $entity->chapter_id;
|
||||
if (!isset($parents[$parentKey])) {
|
||||
$tree[] = $entity;
|
||||
continue;
|
||||
}
|
||||
$chapter = $parents[$parentKey];
|
||||
$chapter->pages->push($entity);
|
||||
}
|
||||
|
||||
return collect($tree);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the child items for a chapter sorted by priority but
|
||||
* with draft items floated to the top.
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
||||
*/
|
||||
public function getChapterChildren(Chapter $chapter)
|
||||
{
|
||||
return $this->permissionService->enforceEntityRestrictions('page', $chapter->pages())
|
||||
->orderBy('draft', 'DESC')->orderBy('priority', 'ASC')->get();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the next sequential priority for a new child element in the given book.
|
||||
* @param \BookStack\Entities\Book $book
|
||||
* @return int
|
||||
*/
|
||||
public function getNewBookPriority(Book $book)
|
||||
{
|
||||
$lastElem = $this->getBookChildren($book)->pop();
|
||||
return $lastElem ? $lastElem->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new priority for a new page to be added to the given chapter.
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @return int
|
||||
*/
|
||||
public function getNewChapterPriority(Chapter $chapter)
|
||||
{
|
||||
$lastPage = $chapter->pages('DESC')->first();
|
||||
return $lastPage !== null ? $lastPage->priority + 1 : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a suitable slug for an entity.
|
||||
* @param string $type
|
||||
* @param string $name
|
||||
* @param bool|integer $currentId
|
||||
* @param bool|integer $bookId Only pass if type is not a book
|
||||
* @return string
|
||||
*/
|
||||
public function findSuitableSlug($type, $name, $currentId = false, $bookId = false)
|
||||
{
|
||||
$slug = $this->nameToSlug($name);
|
||||
while ($this->slugExists($type, $slug, $currentId, $bookId)) {
|
||||
$slug .= '-' . substr(md5(rand(1, 500)), 0, 3);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a slug already exists in the database.
|
||||
* @param string $type
|
||||
* @param string $slug
|
||||
* @param bool|integer $currentId
|
||||
* @param bool|integer $bookId
|
||||
* @return bool
|
||||
*/
|
||||
protected function slugExists($type, $slug, $currentId = false, $bookId = false)
|
||||
{
|
||||
$query = $this->entityProvider->get($type)->where('slug', '=', $slug);
|
||||
if (strtolower($type) === 'page' || strtolower($type) === 'chapter') {
|
||||
$query = $query->where('book_id', '=', $bookId);
|
||||
}
|
||||
if ($currentId) {
|
||||
$query = $query->where('id', '!=', $currentId);
|
||||
}
|
||||
return $query->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity restrictions from a request
|
||||
* @param Request $request
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function updateEntityPermissionsFromRequest(Request $request, Entity $entity)
|
||||
{
|
||||
$entity->restricted = $request->get('restricted', '') === 'true';
|
||||
$entity->permissions()->delete();
|
||||
|
||||
if ($request->filled('restrictions')) {
|
||||
foreach ($request->get('restrictions') as $roleId => $restrictions) {
|
||||
foreach ($restrictions as $action => $value) {
|
||||
$entity->permissions()->create([
|
||||
'role_id' => $roleId,
|
||||
'action' => strtolower($action)
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$entity->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Create a new entity from request input.
|
||||
* Used for books and chapters.
|
||||
* @param string $type
|
||||
* @param array $input
|
||||
* @param bool|Book $book
|
||||
* @return \BookStack\Entities\Entity
|
||||
*/
|
||||
public function createFromInput($type, $input = [], $book = false)
|
||||
{
|
||||
$isChapter = strtolower($type) === 'chapter';
|
||||
$entityModel = $this->entityProvider->get($type)->newInstance($input);
|
||||
$entityModel->slug = $this->findSuitableSlug($type, $entityModel->name, false, $isChapter ? $book->id : false);
|
||||
$entityModel->created_by = user()->id;
|
||||
$entityModel->updated_by = user()->id;
|
||||
$isChapter ? $book->chapters()->save($entityModel) : $entityModel->save();
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update entity details from request input.
|
||||
* Used for books and chapters
|
||||
* @param string $type
|
||||
* @param \BookStack\Entities\Entity $entityModel
|
||||
* @param array $input
|
||||
* @return \BookStack\Entities\Entity
|
||||
*/
|
||||
public function updateFromInput($type, Entity $entityModel, $input = [])
|
||||
{
|
||||
if ($entityModel->name !== $input['name']) {
|
||||
$entityModel->slug = $this->findSuitableSlug($type, $input['name'], $entityModel->id);
|
||||
}
|
||||
$entityModel->fill($input);
|
||||
$entityModel->updated_by = user()->id;
|
||||
$entityModel->save();
|
||||
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($entityModel, $input['tags']);
|
||||
}
|
||||
|
||||
$this->permissionService->buildJointPermissionsForEntity($entityModel);
|
||||
$this->searchService->indexEntity($entityModel);
|
||||
return $entityModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync the books assigned to a shelf from a comma-separated list
|
||||
* of book IDs.
|
||||
* @param \BookStack\Entities\Bookshelf $shelf
|
||||
* @param string $books
|
||||
*/
|
||||
public function updateShelfBooks(Bookshelf $shelf, string $books)
|
||||
{
|
||||
$ids = explode(',', $books);
|
||||
|
||||
// Check books exist and match ordering
|
||||
$bookIds = $this->entityQuery('book')->whereIn('id', $ids)->get(['id'])->pluck('id');
|
||||
$syncData = [];
|
||||
foreach ($ids as $index => $id) {
|
||||
if ($bookIds->contains($id)) {
|
||||
$syncData[$id] = ['order' => $index];
|
||||
}
|
||||
}
|
||||
|
||||
$shelf->books()->sync($syncData);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the book that an entity belongs to.
|
||||
* @param string $type
|
||||
* @param integer $newBookId
|
||||
* @param Entity $entity
|
||||
* @param bool $rebuildPermissions
|
||||
* @return \BookStack\Entities\Entity
|
||||
*/
|
||||
public function changeBook($type, $newBookId, Entity $entity, $rebuildPermissions = false)
|
||||
{
|
||||
$entity->book_id = $newBookId;
|
||||
// Update related activity
|
||||
foreach ($entity->activity as $activity) {
|
||||
$activity->book_id = $newBookId;
|
||||
$activity->save();
|
||||
}
|
||||
$entity->slug = $this->findSuitableSlug($type, $entity->name, $entity->id, $newBookId);
|
||||
$entity->save();
|
||||
|
||||
// Update all child pages if a chapter
|
||||
if (strtolower($type) === 'chapter') {
|
||||
foreach ($entity->pages as $page) {
|
||||
$this->changeBook('page', $newBookId, $page, false);
|
||||
}
|
||||
}
|
||||
|
||||
// Update permissions if applicable
|
||||
if ($rebuildPermissions) {
|
||||
$entity->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($entity->book);
|
||||
}
|
||||
|
||||
return $entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Alias method to update the book jointPermissions in the PermissionService.
|
||||
* @param Book $book
|
||||
*/
|
||||
public function buildJointPermissionsForBook(Book $book)
|
||||
{
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a name as a url slug.
|
||||
* @param $name
|
||||
* @return string
|
||||
*/
|
||||
protected function nameToSlug($name)
|
||||
{
|
||||
$slug = preg_replace('/[\+\/\\\?\@\}\{\.\,\=\[\]\#\&\!\*\'\;\:\$\%]/', '', mb_strtolower($name));
|
||||
$slug = preg_replace('/\s{2,}/', ' ', $slug);
|
||||
$slug = str_replace(' ', '-', $slug);
|
||||
if ($slug === "") {
|
||||
$slug = substr(md5(rand(1, 500)), 0, 5);
|
||||
}
|
||||
return $slug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the page for viewing
|
||||
* @param Page $page
|
||||
* @param bool $blankIncludes
|
||||
* @return string
|
||||
*/
|
||||
public function renderPage(Page $page, bool $blankIncludes = false) : string
|
||||
{
|
||||
$content = $page->html;
|
||||
|
||||
if (!config('app.allow_content_scripts')) {
|
||||
$content = $this->escapeScripts($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);
|
||||
$pageId = intval($splitInclude[0]);
|
||||
if (is_nan($pageId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$matchedPage = $this->getById('page', $pageId);
|
||||
if ($matchedPage === null) {
|
||||
$html = str_replace($matches[0][$index], '', $html);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (count($splitInclude) === 1) {
|
||||
$html = str_replace($matches[0][$index], $matchedPage->html, $html);
|
||||
continue;
|
||||
}
|
||||
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
||||
if ($matchingElem === null) {
|
||||
$html = str_replace($matches[0][$index], '', $html);
|
||||
continue;
|
||||
}
|
||||
$innerContent = '';
|
||||
$isTopLevel = in_array(strtolower($matchingElem->nodeName), $topLevelTags);
|
||||
if ($isTopLevel) {
|
||||
$innerContent .= $doc->saveHTML($matchingElem);
|
||||
} else {
|
||||
foreach ($matchingElem->childNodes as $childNode) {
|
||||
$innerContent .= $doc->saveHTML($childNode);
|
||||
}
|
||||
}
|
||||
$html = str_replace($matches[0][$index], trim($innerContent), $html);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape script tags within HTML content.
|
||||
* @param string $html
|
||||
* @return string
|
||||
*/
|
||||
protected function escapeScripts(string $html) : string
|
||||
{
|
||||
$scriptSearchRegex = '/<script.*?>.*?<\/script>/ms';
|
||||
$matches = [];
|
||||
preg_match_all($scriptSearchRegex, $html, $matches);
|
||||
|
||||
foreach ($matches[0] as $match) {
|
||||
$html = str_replace($match, htmlentities($match), $html);
|
||||
}
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for image usage within page content.
|
||||
* @param $imageString
|
||||
* @return mixed
|
||||
*/
|
||||
public function searchForImage($imageString)
|
||||
{
|
||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get();
|
||||
foreach ($pages as $page) {
|
||||
$page->url = $page->getUrl();
|
||||
$page->html = '';
|
||||
$page->text = '';
|
||||
}
|
||||
return count($pages) > 0 ? $pages : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a bookshelf instance
|
||||
* @param \BookStack\Entities\Bookshelf $shelf
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyBookshelf(Bookshelf $shelf)
|
||||
{
|
||||
$this->destroyEntityCommonRelations($shelf);
|
||||
$shelf->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy the provided book and all its child entities.
|
||||
* @param \BookStack\Entities\Book $book
|
||||
* @throws NotifyException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyBook(Book $book)
|
||||
{
|
||||
foreach ($book->pages as $page) {
|
||||
$this->destroyPage($page);
|
||||
}
|
||||
foreach ($book->chapters as $chapter) {
|
||||
$this->destroyChapter($chapter);
|
||||
}
|
||||
$this->destroyEntityCommonRelations($book);
|
||||
$book->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a chapter and its relations.
|
||||
* @param \BookStack\Entities\Chapter $chapter
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyChapter(Chapter $chapter)
|
||||
{
|
||||
if (count($chapter->pages) > 0) {
|
||||
foreach ($chapter->pages as $page) {
|
||||
$page->chapter_id = 0;
|
||||
$page->save();
|
||||
}
|
||||
}
|
||||
$this->destroyEntityCommonRelations($chapter);
|
||||
$chapter->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy a given page along with its dependencies.
|
||||
* @param Page $page
|
||||
* @throws NotifyException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function destroyPage(Page $page)
|
||||
{
|
||||
// Check if set as custom homepage
|
||||
$customHome = setting('app-homepage', '0:');
|
||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
||||
}
|
||||
|
||||
$this->destroyEntityCommonRelations($page);
|
||||
|
||||
// Delete Attached Files
|
||||
$attachmentService = app(AttachmentService::class);
|
||||
foreach ($page->attachments as $attachment) {
|
||||
$attachmentService->deleteFile($attachment);
|
||||
}
|
||||
|
||||
$page->delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy or handle the common relations connected to an entity.
|
||||
* @param \BookStack\Entities\Entity $entity
|
||||
* @throws \Throwable
|
||||
*/
|
||||
protected function destroyEntityCommonRelations(Entity $entity)
|
||||
{
|
||||
\Activity::removeEntity($entity);
|
||||
$entity->views()->delete();
|
||||
$entity->permissions()->delete();
|
||||
$entity->tags()->delete();
|
||||
$entity->comments()->delete();
|
||||
$this->permissionService->deleteJointPermissionsForEntity($entity);
|
||||
$this->searchService->deleteEntityTerms($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permissions of a bookshelf to all child books.
|
||||
* Returns the number of books that had permissions updated.
|
||||
* @param \BookStack\Entities\Bookshelf $bookshelf
|
||||
* @return int
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function copyBookshelfPermissions(Bookshelf $bookshelf)
|
||||
{
|
||||
$shelfPermissions = $bookshelf->permissions()->get(['role_id', 'action'])->toArray();
|
||||
$shelfBooks = $bookshelf->books()->get();
|
||||
$updatedBookCount = 0;
|
||||
|
||||
foreach ($shelfBooks as $book) {
|
||||
if (!userCan('restrictions-manage', $book)) {
|
||||
continue;
|
||||
}
|
||||
$book->permissions()->delete();
|
||||
$book->restricted = $bookshelf->restricted;
|
||||
$book->permissions()->createMany($shelfPermissions);
|
||||
$book->save();
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
$updatedBookCount++;
|
||||
}
|
||||
|
||||
return $updatedBookCount;
|
||||
}
|
||||
}
|
||||
508
app/Entities/Repos/PageRepo.php
Normal file
508
app/Entities/Repos/PageRepo.php
Normal file
@@ -0,0 +1,508 @@
|
||||
<?php namespace BookStack\Entities\Repos;
|
||||
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Entities\PageRevision;
|
||||
use Carbon\Carbon;
|
||||
use DOMDocument;
|
||||
use DOMXPath;
|
||||
|
||||
class PageRepo extends EntityRepo
|
||||
{
|
||||
|
||||
/**
|
||||
* Get page by slug.
|
||||
* @param string $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @return Page
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function getPageBySlug(string $pageSlug, string $bookSlug)
|
||||
{
|
||||
return $this->getBySlug('page', $pageSlug, $bookSlug);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through page revisions and retrieve the last page in the
|
||||
* current book that has a slug equal to the one given.
|
||||
* @param string $pageSlug
|
||||
* @param string $bookSlug
|
||||
* @return null|Page
|
||||
*/
|
||||
public function getPageByOldSlug(string $pageSlug, string $bookSlug)
|
||||
{
|
||||
$revision = $this->entityProvider->pageRevision->where('slug', '=', $pageSlug)
|
||||
->whereHas('page', function ($query) {
|
||||
$this->permissionService->enforceEntityRestrictions('page', $query);
|
||||
})
|
||||
->where('type', '=', 'version')
|
||||
->where('book_slug', '=', $bookSlug)
|
||||
->orderBy('created_at', 'desc')
|
||||
->with('page')->first();
|
||||
return $revision !== null ? $revision->page : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a page with any fillable data and saves it into the database.
|
||||
* @param Page $page
|
||||
* @param int $book_id
|
||||
* @param array $input
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function updatePage(Page $page, int $book_id, array $input)
|
||||
{
|
||||
// Hold the old details to compare later
|
||||
$oldHtml = $page->html;
|
||||
$oldName = $page->name;
|
||||
|
||||
// Prevent slug being updated if no name change
|
||||
if ($page->name !== $input['name']) {
|
||||
$page->slug = $this->findSuitableSlug('page', $input['name'], $page->id, $book_id);
|
||||
}
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($page, $input['tags']);
|
||||
}
|
||||
|
||||
// Update with new details
|
||||
$userId = user()->id;
|
||||
$page->fill($input);
|
||||
$page->html = $this->formatHtml($input['html']);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$page->markdown = '';
|
||||
}
|
||||
$page->updated_by = $userId;
|
||||
$page->revision_count++;
|
||||
$page->save();
|
||||
|
||||
// Remove all update drafts for this user & page.
|
||||
$this->userUpdatePageDraftsQuery($page, $userId)->delete();
|
||||
|
||||
// Save a revision after updating
|
||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $input['summary'] !== null) {
|
||||
$this->savePageRevision($page, $input['summary']);
|
||||
}
|
||||
|
||||
$this->searchService->indexEntity($page);
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a page revision into the system.
|
||||
* @param Page $page
|
||||
* @param null|string $summary
|
||||
* @return PageRevision
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function savePageRevision(Page $page, string $summary = null)
|
||||
{
|
||||
$revision = $this->entityProvider->pageRevision->newInstance($page->toArray());
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$revision->markdown = '';
|
||||
}
|
||||
$revision->page_id = $page->id;
|
||||
$revision->slug = $page->slug;
|
||||
$revision->book_slug = $page->book->slug;
|
||||
$revision->created_by = user()->id;
|
||||
$revision->created_at = $page->updated_at;
|
||||
$revision->type = 'version';
|
||||
$revision->summary = $summary;
|
||||
$revision->revision_number = $page->revision_count;
|
||||
$revision->save();
|
||||
|
||||
$revisionLimit = config('app.revision_limit');
|
||||
if ($revisionLimit !== false) {
|
||||
$revisionsToDelete = $this->entityProvider->pageRevision->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc')->skip(intval($revisionLimit))->take(10)->get(['id']);
|
||||
if ($revisionsToDelete->count() > 0) {
|
||||
$this->entityProvider->pageRevision->whereIn('id', $revisionsToDelete->pluck('id'))->delete();
|
||||
}
|
||||
}
|
||||
|
||||
return $revision;
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a page's html to be tagged correctly
|
||||
* within the system.
|
||||
* @param string $htmlText
|
||||
* @return string
|
||||
*/
|
||||
protected function formatHtml(string $htmlText)
|
||||
{
|
||||
if ($htmlText == '') {
|
||||
return $htmlText;
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($htmlText, 'HTML-ENTITIES', 'UTF-8'));
|
||||
|
||||
$container = $doc->documentElement;
|
||||
$body = $container->childNodes->item(0);
|
||||
$childNodes = $body->childNodes;
|
||||
|
||||
// Ensure no duplicate ids are used
|
||||
$idArray = [];
|
||||
|
||||
foreach ($childNodes as $index => $childNode) {
|
||||
/** @var \DOMElement $childNode */
|
||||
if (get_class($childNode) !== 'DOMElement') {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// Generate inner html as a string
|
||||
$html = '';
|
||||
foreach ($childNodes as $childNode) {
|
||||
$html .= $doc->saveHTML($childNode);
|
||||
}
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plain text version of a page's content.
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @return string
|
||||
*/
|
||||
protected function pageToPlainText(Page $page) : string
|
||||
{
|
||||
$html = $this->renderPage($page, true);
|
||||
return strip_tags($html);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new draft page instance.
|
||||
* @param Book $book
|
||||
* @param Chapter|null $chapter
|
||||
* @return \BookStack\Entities\Page
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function getDraftPage(Book $book, Chapter $chapter = null)
|
||||
{
|
||||
$page = $this->entityProvider->page->newInstance();
|
||||
$page->name = trans('entities.pages_initial_name');
|
||||
$page->created_by = user()->id;
|
||||
$page->updated_by = user()->id;
|
||||
$page->draft = true;
|
||||
|
||||
if ($chapter) {
|
||||
$page->chapter_id = $chapter->id;
|
||||
}
|
||||
|
||||
$book->pages()->save($page);
|
||||
$page = $this->entityProvider->page->find($page->id);
|
||||
$this->permissionService->buildJointPermissionsForEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a page update draft.
|
||||
* @param Page $page
|
||||
* @param array $data
|
||||
* @return PageRevision|Page
|
||||
*/
|
||||
public function updatePageDraft(Page $page, array $data = [])
|
||||
{
|
||||
// If the page itself is a draft simply update that
|
||||
if ($page->draft) {
|
||||
$page->fill($data);
|
||||
if (isset($data['html'])) {
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
}
|
||||
$page->save();
|
||||
return $page;
|
||||
}
|
||||
|
||||
// Otherwise save the data to a revision
|
||||
$userId = user()->id;
|
||||
$drafts = $this->userUpdatePageDraftsQuery($page, $userId)->get();
|
||||
|
||||
if ($drafts->count() > 0) {
|
||||
$draft = $drafts->first();
|
||||
} else {
|
||||
$draft = $this->entityProvider->pageRevision->newInstance();
|
||||
$draft->page_id = $page->id;
|
||||
$draft->slug = $page->slug;
|
||||
$draft->book_slug = $page->book->slug;
|
||||
$draft->created_by = $userId;
|
||||
$draft->type = 'update_draft';
|
||||
}
|
||||
|
||||
$draft->fill($data);
|
||||
if (setting('app-editor') !== 'markdown') {
|
||||
$draft->markdown = '';
|
||||
}
|
||||
|
||||
$draft->save();
|
||||
return $draft;
|
||||
}
|
||||
|
||||
/**
|
||||
* Publish a draft page to make it a normal page.
|
||||
* Sets the slug and updates the content.
|
||||
* @param Page $draftPage
|
||||
* @param array $input
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function publishPageDraft(Page $draftPage, array $input)
|
||||
{
|
||||
$draftPage->fill($input);
|
||||
|
||||
// Save page tags if present
|
||||
if (isset($input['tags'])) {
|
||||
$this->tagRepo->saveTagsToEntity($draftPage, $input['tags']);
|
||||
}
|
||||
|
||||
$draftPage->slug = $this->findSuitableSlug('page', $draftPage->name, false, $draftPage->book->id);
|
||||
$draftPage->html = $this->formatHtml($input['html']);
|
||||
$draftPage->text = $this->pageToPlainText($draftPage);
|
||||
$draftPage->draft = false;
|
||||
$draftPage->revision_count = 1;
|
||||
|
||||
$draftPage->save();
|
||||
$this->savePageRevision($draftPage, trans('entities.pages_initial_revision'));
|
||||
$this->searchService->indexEntity($draftPage);
|
||||
return $draftPage;
|
||||
}
|
||||
|
||||
/**
|
||||
* The base query for getting user update drafts.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return mixed
|
||||
*/
|
||||
protected function userUpdatePageDraftsQuery(Page $page, int $userId)
|
||||
{
|
||||
return $this->entityProvider->pageRevision->where('created_by', '=', $userId)
|
||||
->where('type', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->orderBy('created_at', 'desc');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the latest updated draft revision for a particular page and user.
|
||||
* @param Page $page
|
||||
* @param $userId
|
||||
* @return PageRevision|null
|
||||
*/
|
||||
public function getUserPageDraft(Page $page, int $userId)
|
||||
{
|
||||
return $this->userUpdatePageDraftsQuery($page, $userId)->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification message that informs the user that they are editing a draft page.
|
||||
* @param PageRevision $draft
|
||||
* @return string
|
||||
*/
|
||||
public function getUserPageDraftMessage(PageRevision $draft)
|
||||
{
|
||||
$message = trans('entities.pages_editing_draft_notification', ['timeDiff' => $draft->updated_at->diffForHumans()]);
|
||||
if ($draft->page->updated_at->timestamp <= $draft->updated_at->timestamp) {
|
||||
return $message;
|
||||
}
|
||||
return $message . "\n" . trans('entities.pages_draft_edited_notification');
|
||||
}
|
||||
|
||||
/**
|
||||
* A query to check for active update drafts on a particular page.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return mixed
|
||||
*/
|
||||
protected function activePageEditingQuery(Page $page, int $minRange = null)
|
||||
{
|
||||
$query = $this->entityProvider->pageRevision->where('type', '=', 'update_draft')
|
||||
->where('page_id', '=', $page->id)
|
||||
->where('updated_at', '>', $page->updated_at)
|
||||
->where('created_by', '!=', user()->id)
|
||||
->with('createdBy');
|
||||
|
||||
if ($minRange !== null) {
|
||||
$query = $query->where('updated_at', '>=', Carbon::now()->subMinutes($minRange));
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a page is being actively editing.
|
||||
* Checks for edits since last page updated.
|
||||
* Passing in a minuted range will check for edits
|
||||
* within the last x minutes.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return bool
|
||||
*/
|
||||
public function isPageEditingActive(Page $page, int $minRange = null)
|
||||
{
|
||||
$draftSearch = $this->activePageEditingQuery($page, $minRange);
|
||||
return $draftSearch->count() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a notification message concerning the editing activity on a particular page.
|
||||
* @param Page $page
|
||||
* @param int $minRange
|
||||
* @return string
|
||||
*/
|
||||
public function getPageEditingActiveMessage(Page $page, int $minRange = null)
|
||||
{
|
||||
$pageDraftEdits = $this->activePageEditingQuery($page, $minRange)->get();
|
||||
|
||||
$userMessage = $pageDraftEdits->count() > 1 ? trans('entities.pages_draft_edit_active.start_a', ['count' => $pageDraftEdits->count()]): trans('entities.pages_draft_edit_active.start_b', ['userName' => $pageDraftEdits->first()->createdBy->name]);
|
||||
$timeMessage = $minRange === null ? trans('entities.pages_draft_edit_active.time_a') : trans('entities.pages_draft_edit_active.time_b', ['minCount'=>$minRange]);
|
||||
return trans('entities.pages_draft_edit_active.message', ['start' => $userMessage, 'time' => $timeMessage]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the headers on the page to get a navigation menu
|
||||
* @param string $pageContent
|
||||
* @return array
|
||||
*/
|
||||
public function getPageNav(string $pageContent)
|
||||
{
|
||||
if ($pageContent == '') {
|
||||
return [];
|
||||
}
|
||||
libxml_use_internal_errors(true);
|
||||
$doc = new DOMDocument();
|
||||
$doc->loadHTML(mb_convert_encoding($pageContent, 'HTML-ENTITIES', 'UTF-8'));
|
||||
$xPath = new DOMXPath($doc);
|
||||
$headers = $xPath->query("//h1|//h2|//h3|//h4|//h5|//h6");
|
||||
|
||||
if (is_null($headers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$tree = collect([]);
|
||||
foreach ($headers as $header) {
|
||||
$text = $header->nodeValue;
|
||||
$tree->push([
|
||||
'nodeName' => strtolower($header->nodeName),
|
||||
'level' => intval(str_replace('h', '', $header->nodeName)),
|
||||
'link' => '#' . $header->getAttribute('id'),
|
||||
'text' => strlen($text) > 30 ? substr($text, 0, 27) . '...' : $text
|
||||
]);
|
||||
}
|
||||
|
||||
// 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;
|
||||
});
|
||||
}
|
||||
return $tree->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a revision's content back into a page.
|
||||
* @param Page $page
|
||||
* @param Book $book
|
||||
* @param int $revisionId
|
||||
* @return Page
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function restorePageRevision(Page $page, Book $book, int $revisionId)
|
||||
{
|
||||
$page->revision_count++;
|
||||
$this->savePageRevision($page);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
$page->fill($revision->toArray());
|
||||
$page->slug = $this->findSuitableSlug('page', $page->name, $page->id, $book->id);
|
||||
$page->text = $this->pageToPlainText($page);
|
||||
$page->updated_by = user()->id;
|
||||
$page->save();
|
||||
$this->searchService->indexEntity($page);
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the page's parent to the given entity.
|
||||
* @param Page $page
|
||||
* @param Entity $parent
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function changePageParent(Page $page, Entity $parent)
|
||||
{
|
||||
$book = $parent->isA('book') ? $parent : $parent->book;
|
||||
$page->chapter_id = $parent->isA('chapter') ? $parent->id : 0;
|
||||
$page->save();
|
||||
if ($page->book->id !== $book->id) {
|
||||
$page = $this->changeBook('page', $book->id, $page);
|
||||
}
|
||||
$page->load('book');
|
||||
$this->permissionService->buildJointPermissionsForEntity($book);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a copy of a page in a new location with a new name.
|
||||
* @param \BookStack\Entities\Page $page
|
||||
* @param \BookStack\Entities\Entity $newParent
|
||||
* @param string $newName
|
||||
* @return \BookStack\Entities\Page
|
||||
* @throws \Throwable
|
||||
*/
|
||||
public function copyPage(Page $page, Entity $newParent, string $newName = '')
|
||||
{
|
||||
$newBook = $newParent->isA('book') ? $newParent : $newParent->book;
|
||||
$newChapter = $newParent->isA('chapter') ? $newParent : null;
|
||||
$copyPage = $this->getDraftPage($newBook, $newChapter);
|
||||
$pageData = $page->getAttributes();
|
||||
|
||||
// Update name
|
||||
if (!empty($newName)) {
|
||||
$pageData['name'] = $newName;
|
||||
}
|
||||
|
||||
// Copy tags from previous page if set
|
||||
if ($page->tags) {
|
||||
$pageData['tags'] = [];
|
||||
foreach ($page->tags as $tag) {
|
||||
$pageData['tags'][] = ['name' => $tag->name, 'value' => $tag->value];
|
||||
}
|
||||
}
|
||||
|
||||
// Set priority
|
||||
if ($newParent->isA('chapter')) {
|
||||
$pageData['priority'] = $this->getNewChapterPriority($newParent);
|
||||
} else {
|
||||
$pageData['priority'] = $this->getNewBookPriority($newParent);
|
||||
}
|
||||
|
||||
return $this->publishPageDraft($copyPage, $pageData);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,34 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Book;
|
||||
use BookStack\Chapter;
|
||||
use BookStack\Entity;
|
||||
use BookStack\Page;
|
||||
use BookStack\SearchTerm;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use Illuminate\Database\Connection;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
use Illuminate\Database\Query\Builder;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class SearchService
|
||||
{
|
||||
/**
|
||||
* @var SearchTerm
|
||||
*/
|
||||
protected $searchTerm;
|
||||
protected $book;
|
||||
protected $chapter;
|
||||
protected $page;
|
||||
|
||||
/**
|
||||
* @var EntityProvider
|
||||
*/
|
||||
protected $entityProvider;
|
||||
|
||||
/**
|
||||
* @var Connection
|
||||
*/
|
||||
protected $db;
|
||||
|
||||
/**
|
||||
* @var PermissionService
|
||||
*/
|
||||
protected $permissionService;
|
||||
protected $entities;
|
||||
|
||||
|
||||
/**
|
||||
* Acceptable operators to be used in a query
|
||||
@@ -29,24 +39,15 @@ class SearchService
|
||||
/**
|
||||
* SearchService constructor.
|
||||
* @param SearchTerm $searchTerm
|
||||
* @param Book $book
|
||||
* @param Chapter $chapter
|
||||
* @param Page $page
|
||||
* @param EntityProvider $entityProvider
|
||||
* @param Connection $db
|
||||
* @param PermissionService $permissionService
|
||||
*/
|
||||
public function __construct(SearchTerm $searchTerm, Book $book, Chapter $chapter, Page $page, Connection $db, PermissionService $permissionService)
|
||||
public function __construct(SearchTerm $searchTerm, EntityProvider $entityProvider, Connection $db, PermissionService $permissionService)
|
||||
{
|
||||
$this->searchTerm = $searchTerm;
|
||||
$this->book = $book;
|
||||
$this->chapter = $chapter;
|
||||
$this->page = $page;
|
||||
$this->entityProvider = $entityProvider;
|
||||
$this->db = $db;
|
||||
$this->entities = [
|
||||
'page' => $this->page,
|
||||
'chapter' => $this->chapter,
|
||||
'book' => $this->book
|
||||
];
|
||||
$this->permissionService = $permissionService;
|
||||
}
|
||||
|
||||
@@ -65,12 +66,13 @@ class SearchService
|
||||
* @param string $entityType
|
||||
* @param int $page
|
||||
* @param int $count - Count of each entity to search, Total returned could can be larger and not guaranteed.
|
||||
* @param string $action
|
||||
* @return array[int, Collection];
|
||||
*/
|
||||
public function searchEntities($searchString, $entityType = 'all', $page = 1, $count = 20, $action = 'view')
|
||||
{
|
||||
$terms = $this->parseSearchString($searchString);
|
||||
$entityTypes = array_keys($this->entities);
|
||||
$entityTypes = array_keys($this->entityProvider->all());
|
||||
$entityTypesToSearch = $entityTypes;
|
||||
|
||||
if ($entityType !== 'all') {
|
||||
@@ -167,17 +169,17 @@ class SearchService
|
||||
* @param array $terms
|
||||
* @param string $entityType
|
||||
* @param string $action
|
||||
* @return \Illuminate\Database\Eloquent\Builder
|
||||
* @return EloquentBuilder
|
||||
*/
|
||||
protected function buildEntitySearchQuery($terms, $entityType = 'page', $action = 'view')
|
||||
{
|
||||
$entity = $this->getEntity($entityType);
|
||||
$entity = $this->entityProvider->get($entityType);
|
||||
$entitySelect = $entity->newQuery();
|
||||
|
||||
// Handle normal search terms
|
||||
if (count($terms['search']) > 0) {
|
||||
$subQuery = $this->db->table('search_terms')->select('entity_id', 'entity_type', \DB::raw('SUM(score) as score'));
|
||||
$subQuery->where('entity_type', '=', 'BookStack\\' . ucfirst($entityType));
|
||||
$subQuery->where('entity_type', '=', $entity->getMorphClass());
|
||||
$subQuery->where(function (Builder $query) use ($terms) {
|
||||
foreach ($terms['search'] as $inputTerm) {
|
||||
$query->orWhere('term', 'like', $inputTerm .'%');
|
||||
@@ -191,9 +193,9 @@ class SearchService
|
||||
|
||||
// Handle exact term matching
|
||||
if (count($terms['exact']) > 0) {
|
||||
$entitySelect->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($terms, $entity) {
|
||||
$entitySelect->where(function (EloquentBuilder $query) use ($terms, $entity) {
|
||||
foreach ($terms['exact'] as $inputTerm) {
|
||||
$query->where(function (\Illuminate\Database\Eloquent\Builder $query) use ($inputTerm, $entity) {
|
||||
$query->where(function (EloquentBuilder $query) use ($inputTerm, $entity) {
|
||||
$query->where('name', 'like', '%'.$inputTerm .'%')
|
||||
->orWhere($entity->textField, 'like', '%'.$inputTerm .'%');
|
||||
});
|
||||
@@ -281,14 +283,14 @@ class SearchService
|
||||
|
||||
/**
|
||||
* Apply a tag search term onto a entity query.
|
||||
* @param \Illuminate\Database\Eloquent\Builder $query
|
||||
* @param EloquentBuilder $query
|
||||
* @param string $tagTerm
|
||||
* @return mixed
|
||||
*/
|
||||
protected function applyTagSearch(\Illuminate\Database\Eloquent\Builder $query, $tagTerm)
|
||||
protected function applyTagSearch(EloquentBuilder $query, $tagTerm)
|
||||
{
|
||||
preg_match("/^(.*?)((".$this->getRegexEscapedOperators().")(.*?))?$/", $tagTerm, $tagSplit);
|
||||
$query->whereHas('tags', function (\Illuminate\Database\Eloquent\Builder $query) use ($tagSplit) {
|
||||
$query->whereHas('tags', function (EloquentBuilder $query) use ($tagSplit) {
|
||||
$tagName = $tagSplit[1];
|
||||
$tagOperator = count($tagSplit) > 2 ? $tagSplit[3] : '';
|
||||
$tagValue = count($tagSplit) > 3 ? $tagSplit[4] : '';
|
||||
@@ -313,16 +315,6 @@ class SearchService
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an entity instance via type.
|
||||
* @param $type
|
||||
* @return Entity
|
||||
*/
|
||||
protected function getEntity($type)
|
||||
{
|
||||
return $this->entities[strtolower($type)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Index the given entity.
|
||||
* @param Entity $entity
|
||||
@@ -342,7 +334,7 @@ class SearchService
|
||||
|
||||
/**
|
||||
* Index multiple Entities at once
|
||||
* @param Entity[] $entities
|
||||
* @param \BookStack\Entities\Entity[] $entities
|
||||
*/
|
||||
protected function indexEntities($entities)
|
||||
{
|
||||
@@ -370,20 +362,12 @@ class SearchService
|
||||
{
|
||||
$this->searchTerm->truncate();
|
||||
|
||||
// Chunk through all books
|
||||
$this->book->chunk(1000, function ($books) {
|
||||
$this->indexEntities($books);
|
||||
});
|
||||
|
||||
// Chunk through all chapters
|
||||
$this->chapter->chunk(1000, function ($chapters) {
|
||||
$this->indexEntities($chapters);
|
||||
});
|
||||
|
||||
// Chunk through all pages
|
||||
$this->page->chunk(1000, function ($pages) {
|
||||
$this->indexEntities($pages);
|
||||
});
|
||||
foreach ($this->entityProvider->all() as $entityModel) {
|
||||
$selectFields = ['id', 'name', $entityModel->textField];
|
||||
$entityModel->newQuery()->select($selectFields)->chunk(1000, function ($entities) {
|
||||
$this->indexEntities($entities);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -432,7 +416,7 @@ class SearchService
|
||||
* Custom entity search filters
|
||||
*/
|
||||
|
||||
protected function filterUpdatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterUpdatedAfter(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
@@ -442,7 +426,7 @@ class SearchService
|
||||
$query->where('updated_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterUpdatedBefore(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
@@ -452,7 +436,7 @@ class SearchService
|
||||
$query->where('updated_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedAfter(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterCreatedAfter(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
@@ -462,7 +446,7 @@ class SearchService
|
||||
$query->where('created_at', '>=', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBefore(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterCreatedBefore(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
try {
|
||||
$date = date_create($input);
|
||||
@@ -472,7 +456,7 @@ class SearchService
|
||||
$query->where('created_at', '<', $date);
|
||||
}
|
||||
|
||||
protected function filterCreatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterCreatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
@@ -483,7 +467,7 @@ class SearchService
|
||||
$query->where('created_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterUpdatedBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterUpdatedBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
if (!is_numeric($input) && $input !== 'me') {
|
||||
return;
|
||||
@@ -494,41 +478,41 @@ class SearchService
|
||||
$query->where('updated_by', '=', $input);
|
||||
}
|
||||
|
||||
protected function filterInName(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterInName(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('name', 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterInTitle(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterInTitle(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$this->filterInName($query, $model, $input);
|
||||
}
|
||||
|
||||
protected function filterInBody(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterInBody(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where($model->textField, 'like', '%' .$input. '%');
|
||||
}
|
||||
|
||||
protected function filterIsRestricted(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterIsRestricted(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->where('restricted', '=', true);
|
||||
}
|
||||
|
||||
protected function filterViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterViewedByMe(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereHas('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterNotViewedByMe(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterNotViewedByMe(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$query->whereDoesntHave('views', function ($query) {
|
||||
$query->where('user_id', '=', user()->id);
|
||||
});
|
||||
}
|
||||
|
||||
protected function filterSortBy(\Illuminate\Database\Eloquent\Builder $query, Entity $model, $input)
|
||||
protected function filterSortBy(EloquentBuilder $query, Entity $model, $input)
|
||||
{
|
||||
$functionName = camel_case('sort_by_' . $input);
|
||||
if (method_exists($this, $functionName)) {
|
||||
@@ -541,7 +525,7 @@ class SearchService
|
||||
* Sorting filter options
|
||||
*/
|
||||
|
||||
protected function sortByLastCommented(\Illuminate\Database\Eloquent\Builder $query, Entity $model)
|
||||
protected function sortByLastCommented(EloquentBuilder $query, Entity $model)
|
||||
{
|
||||
$commentsTable = $this->db->getTablePrefix() . 'comments';
|
||||
$morphClass = str_replace('\\', '\\\\', $model->getMorphClass());
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Entities;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class SearchTerm extends Model
|
||||
{
|
||||
@@ -3,12 +3,12 @@
|
||||
namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Auth\Access\AuthorizationException;
|
||||
use Illuminate\Auth\AuthenticationException;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
class Handler extends ExceptionHandler
|
||||
|
||||
7
app/Exceptions/HttpFetchException.php
Normal file
7
app/Exceptions/HttpFetchException.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class HttpFetchException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -11,7 +11,7 @@ class NotifyException extends \Exception
|
||||
* @param string $message
|
||||
* @param string $redirectLocation
|
||||
*/
|
||||
public function __construct($message, $redirectLocation)
|
||||
public function __construct(string $message, string $redirectLocation = "/")
|
||||
{
|
||||
$this->message = $message;
|
||||
$this->redirectLocation = $redirectLocation;
|
||||
|
||||
6
app/Exceptions/SocialSignInAccountNotUsed.php
Normal file
6
app/Exceptions/SocialSignInAccountNotUsed.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class SocialSignInAccountNotUsed extends SocialSignInException
|
||||
{
|
||||
|
||||
}
|
||||
5
app/Exceptions/UserUpdateException.php
Normal file
5
app/Exceptions/UserUpdateException.php
Normal file
@@ -0,0 +1,5 @@
|
||||
<?php namespace BookStack\Exceptions;
|
||||
|
||||
class UserUpdateException extends NotifyException
|
||||
{
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?php namespace BookStack\Services\Facades;
|
||||
<?php namespace BookStack\Facades;
|
||||
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\AttachmentService;
|
||||
use BookStack\Uploads\Attachment;
|
||||
use BookStack\Uploads\AttachmentService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class AttachmentController extends Controller
|
||||
@@ -15,7 +15,7 @@ class AttachmentController extends Controller
|
||||
|
||||
/**
|
||||
* AttachmentController constructor.
|
||||
* @param AttachmentService $attachmentService
|
||||
* @param \BookStack\Uploads\AttachmentService $attachmentService
|
||||
* @param Attachment $attachment
|
||||
* @param EntityRepo $entityRepo
|
||||
*/
|
||||
@@ -201,10 +201,7 @@ class AttachmentController extends Controller
|
||||
}
|
||||
|
||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||
return response($attachmentContents, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||
]);
|
||||
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,10 +2,11 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\AuthException;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||
use Illuminate\Http\Request;
|
||||
@@ -36,18 +37,21 @@ class LoginController extends Controller
|
||||
protected $redirectAfterLogout = '/login';
|
||||
|
||||
protected $socialAuthService;
|
||||
protected $ldapService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService
|
||||
* @param LdapService $ldapService
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
$this->ldapService = $ldapService;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->redirectPath = baseUrl('/');
|
||||
$this->redirectAfterLogout = baseUrl('/login');
|
||||
@@ -66,6 +70,7 @@ class LoginController extends Controller
|
||||
* @param Authenticatable $user
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
* @throws AuthException
|
||||
* @throws \BookStack\Exceptions\LdapException
|
||||
*/
|
||||
protected function authenticated(Request $request, Authenticatable $user)
|
||||
{
|
||||
@@ -96,6 +101,11 @@ class LoginController extends Controller
|
||||
auth()->login($user);
|
||||
}
|
||||
|
||||
// Sync LDAP groups if required
|
||||
if ($this->ldapService->shouldSyncGroups()) {
|
||||
$this->ldapService->syncGroups($user, $request->get($this->username()));
|
||||
}
|
||||
|
||||
$path = session()->pull('url.intended', '/');
|
||||
$path = baseUrl($path, true);
|
||||
return redirect($path);
|
||||
@@ -125,6 +135,7 @@ class LoginController extends Controller
|
||||
* Redirect to the relevant social site.
|
||||
* @param $socialDriver
|
||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
*/
|
||||
public function getSocialLogin($socialDriver)
|
||||
{
|
||||
|
||||
@@ -2,20 +2,19 @@
|
||||
|
||||
namespace BookStack\Http\Controllers\Auth;
|
||||
|
||||
use BookStack\Exceptions\ConfirmationEmailException;
|
||||
use BookStack\Auth\SocialAccount;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||
use BookStack\Exceptions\SocialSignInException;
|
||||
use BookStack\Exceptions\UserRegistrationException;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\EmailConfirmationService;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\SocialAccount;
|
||||
use BookStack\User;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Exception;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||
use Validator;
|
||||
use BookStack\Http\Controllers\Controller;
|
||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||
|
||||
class RegisterController extends Controller
|
||||
{
|
||||
@@ -47,11 +46,11 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Create a new controller instance.
|
||||
*
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param EmailConfirmationService $emailConfirmationService
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
|
||||
* @param \BookStack\Auth\EmailConfirmationService $emailConfirmationService
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
public function __construct(\BookStack\Auth\Access\SocialAuthService $socialAuthService, \BookStack\Auth\Access\EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||
{
|
||||
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
|
||||
$this->socialAuthService = $socialAuthService;
|
||||
@@ -118,7 +117,7 @@ class RegisterController extends Controller
|
||||
/**
|
||||
* Create a new user instance after a valid registration.
|
||||
* @param array $data
|
||||
* @return User
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
protected function create(array $data)
|
||||
{
|
||||
@@ -133,25 +132,28 @@ class RegisterController extends Controller
|
||||
* The registrations flow for all users.
|
||||
* @param array $userData
|
||||
* @param bool|false|SocialAccount $socialAccount
|
||||
* @param bool $emailVerified
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
*/
|
||||
protected function registerUser(array $userData, $socialAccount = false)
|
||||
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
|
||||
{
|
||||
if (setting('registration-restrict')) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||
$registrationRestrict = setting('registration-restrict');
|
||||
|
||||
if ($registrationRestrict) {
|
||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
||||
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||
}
|
||||
}
|
||||
|
||||
$newUser = $this->userRepo->registerNew($userData);
|
||||
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
|
||||
if ($socialAccount) {
|
||||
$newUser->socialAccounts()->save($socialAccount);
|
||||
}
|
||||
|
||||
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||
if ((setting('registration-confirmation') || $registrationRestrict) && !$emailVerified) {
|
||||
$newUser->save();
|
||||
|
||||
try {
|
||||
@@ -250,7 +252,6 @@ class RegisterController extends Controller
|
||||
* @throws SocialSignInException
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
* @throws ConfirmationEmailException
|
||||
*/
|
||||
public function socialCallback($socialDriver, Request $request)
|
||||
{
|
||||
@@ -267,12 +268,24 @@ class RegisterController extends Controller
|
||||
}
|
||||
|
||||
$action = session()->pull('social-callback');
|
||||
|
||||
// Attempt login or fall-back to register if allowed.
|
||||
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
||||
if ($action == 'login') {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||
try {
|
||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||
} catch (SocialSignInAccountNotUsed $exception) {
|
||||
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
|
||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||
}
|
||||
throw $exception;
|
||||
}
|
||||
}
|
||||
|
||||
if ($action == 'register') {
|
||||
return $this->socialRegisterCallback($socialDriver);
|
||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||
}
|
||||
|
||||
return redirect()->back();
|
||||
}
|
||||
|
||||
@@ -288,15 +301,16 @@ class RegisterController extends Controller
|
||||
|
||||
/**
|
||||
* Register a new user after a registration callback.
|
||||
* @param $socialDriver
|
||||
* @param string $socialDriver
|
||||
* @param SocialUser $socialUser
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws UserRegistrationException
|
||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
||||
*/
|
||||
protected function socialRegisterCallback($socialDriver)
|
||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
||||
{
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||
$socialAccount = $this->socialAuthService->fillSocialAccount($socialDriver, $socialUser);
|
||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
||||
|
||||
// Create an array of the user data to create a new user instance
|
||||
$userData = [
|
||||
@@ -304,6 +318,6 @@ class RegisterController extends Controller
|
||||
'email' => $socialUser->getEmail(),
|
||||
'password' => str_random(30)
|
||||
];
|
||||
return $this->registerUser($userData, $socialAccount);
|
||||
return $this->registerUser($userData, $socialAccount, $emailVerified);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Book;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
@@ -19,8 +19,8 @@ class BookController extends Controller
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
* @param \BookStack\Auth\UserRepo $userRepo
|
||||
* @param \BookStack\Entities\ExportService $exportService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
@@ -204,7 +204,7 @@ class BookController extends Controller
|
||||
|
||||
// Get the books involved in the sort
|
||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
||||
$booksInvolved = $this->entityRepo->book->newQuery()->whereIn('id', $bookIdsInvolved)->get();
|
||||
$booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
|
||||
// Throw permission error if invalid ids or inaccessible books given.
|
||||
if (count($bookIdsInvolved) !== count($booksInvolved)) {
|
||||
$this->showPermissionError();
|
||||
@@ -299,10 +299,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$pdfContent = $this->exportService->bookToPdf($book);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
|
||||
]);
|
||||
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -314,10 +311,7 @@ class BookController extends Controller
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
|
||||
]);
|
||||
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -328,10 +322,7 @@ class BookController extends Controller
|
||||
public function exportPlainText($bookSlug)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$htmlContent = $this->exportService->bookToPlainText($book);
|
||||
return response()->make($htmlContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
|
||||
]);
|
||||
$textContent = $this->exportService->bookToPlainText($book);
|
||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
||||
}
|
||||
}
|
||||
|
||||
242
app/Http/Controllers/BookshelfController.php
Normal file
242
app/Http/Controllers/BookshelfController.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
class BookshelfController extends Controller
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
protected $userRepo;
|
||||
protected $exportService;
|
||||
|
||||
/**
|
||||
* BookController constructor.
|
||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param \BookStack\Entities\ExportService $exportService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->userRepo = $userRepo;
|
||||
$this->exportService = $exportService;
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
/**
|
||||
* Display a listing of the book.
|
||||
* @return Response
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18);
|
||||
$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->setPageTitle(trans('entities.shelves'));
|
||||
return view('shelves/index', [
|
||||
'shelves' => $shelves,
|
||||
'recents' => $recents,
|
||||
'popular' => $popular,
|
||||
'new' => $new,
|
||||
'shelvesViewType' => $shelvesViewType
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new bookshelf.
|
||||
* @return Response
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
||||
$this->setPageTitle(trans('entities.shelves_create'));
|
||||
return view('shelves/create', ['books' => $books]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$this->checkPermission('bookshelf-create-all');
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
]);
|
||||
|
||||
$bookshelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
||||
$this->entityRepo->updateShelfBooks($bookshelf, $request->get('books', ''));
|
||||
Activity::add($bookshelf, 'bookshelf_create');
|
||||
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Display the specified bookshelf.
|
||||
* @param String $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function show(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('book-view', $bookshelf);
|
||||
|
||||
$books = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
Views::add($bookshelf);
|
||||
|
||||
$this->setPageTitle($bookshelf->getShortName());
|
||||
return view('shelves/show', [
|
||||
'shelf' => $bookshelf,
|
||||
'books' => $books,
|
||||
'activity' => Activity::entityActivity($bookshelf, 20, 0)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified bookshelf.
|
||||
* @param $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function edit(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
||||
|
||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($bookshelf);
|
||||
$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,
|
||||
'books' => $books,
|
||||
'shelfBooks' => $shelfBooks,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update the specified bookshelf in storage.
|
||||
* @param Request $request
|
||||
* @param string $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function update(Request $request, string $slug)
|
||||
{
|
||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255',
|
||||
'description' => 'string|max:1000',
|
||||
]);
|
||||
|
||||
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
||||
Activity::add($shelf, 'bookshelf_update');
|
||||
|
||||
return redirect($shelf->getUrl());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows the page to confirm deletion
|
||||
* @param $slug
|
||||
* @return \Illuminate\View\View
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showDelete(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $bookshelf Bookshelf */
|
||||
$this->checkOwnablePermission('bookshelf-delete', $bookshelf);
|
||||
|
||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $bookshelf->getShortName()]));
|
||||
return view('shelves/delete', ['shelf' => $bookshelf]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified bookshelf from storage.
|
||||
* @param string $slug
|
||||
* @return Response
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
* @throws \Throwable
|
||||
*/
|
||||
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);
|
||||
return redirect('/shelves');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the Restrictions view.
|
||||
* @param $slug
|
||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function showRestrict(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('shelves.restrictions', [
|
||||
'shelf' => $bookshelf,
|
||||
'roles' => $roles
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the restrictions for this bookshelf.
|
||||
* @param $slug
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function restrict(string $slug, Request $request)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $bookshelf);
|
||||
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Copy the permissions of a bookshelf to the child books.
|
||||
* @param string $slug
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws \BookStack\Exceptions\NotFoundException
|
||||
*/
|
||||
public function copyPermissions(string $slug)
|
||||
{
|
||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $bookshelf);
|
||||
|
||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($bookshelf);
|
||||
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
||||
return redirect($bookshelf->getUrl());
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\ExportService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
@@ -19,7 +19,7 @@ class ChapterController extends Controller
|
||||
* ChapterController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param UserRepo $userRepo
|
||||
* @param ExportService $exportService
|
||||
* @param \BookStack\Entities\ExportService $exportService
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||
{
|
||||
@@ -161,6 +161,7 @@ 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);
|
||||
$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 === '') {
|
||||
@@ -250,10 +252,7 @@ class ChapterController extends Controller
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
|
||||
]);
|
||||
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -266,10 +265,7 @@ class ChapterController extends Controller
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
|
||||
]);
|
||||
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,10 +277,7 @@ class ChapterController extends Controller
|
||||
public function exportPlainText($bookSlug, $chapterSlug)
|
||||
{
|
||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->chapterToPlainText($chapter);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
|
||||
]);
|
||||
$chapterText = $this->exportService->chapterToPlainText($chapter);
|
||||
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\CommentRepo;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Actions\CommentRepo;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
@@ -13,8 +13,8 @@ class CommentController extends Controller
|
||||
|
||||
/**
|
||||
* CommentController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param CommentRepo $commentRepo
|
||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
||||
* @param \BookStack\Actions\CommentRepo $commentRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
||||
{
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
|
||||
namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Ownable;
|
||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Routing\Controller as BaseController;
|
||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||
use BookStack\User;
|
||||
|
||||
abstract class Controller extends BaseController
|
||||
{
|
||||
@@ -136,7 +136,6 @@ abstract class Controller extends BaseController
|
||||
|
||||
/**
|
||||
* Create the response for when a request fails validation.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @param array $errors
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
@@ -151,4 +150,18 @@ abstract class Controller extends BaseController
|
||||
->withInput($request->input())
|
||||
->withErrors($errors, $this->errorBag());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a response that forces a download in the browser.
|
||||
* @param string $content
|
||||
* @param string $fileName
|
||||
* @return \Illuminate\Http\Response
|
||||
*/
|
||||
protected function downloadResponse(string $content, string $fileName)
|
||||
{
|
||||
return response()->make($content, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $fileName . '"'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
|
||||
@@ -33,42 +32,42 @@ class HomeController extends Controller
|
||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||
|
||||
|
||||
$customHomepage = false;
|
||||
$books = false;
|
||||
$booksViewType = false;
|
||||
|
||||
// Check book homepage
|
||||
$bookHomepageSetting = setting('app-book-homepage');
|
||||
if ($bookHomepageSetting) {
|
||||
$books = $this->entityRepo->getAllPaginated('book', 18);
|
||||
$booksViewType = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books', 'list'));
|
||||
} else {
|
||||
// Check custom homepage
|
||||
$homepageSetting = setting('app-homepage');
|
||||
if ($homepageSetting) {
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
}
|
||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
||||
$homepageOption = setting('app-homepage-type', 'default');
|
||||
if (!in_array($homepageOption, $homepageOptions)) {
|
||||
$homepageOption = 'default';
|
||||
}
|
||||
|
||||
$view = 'home';
|
||||
if ($bookHomepageSetting) {
|
||||
$view = 'home-book';
|
||||
} else if ($customHomepage) {
|
||||
$view = 'home-custom';
|
||||
}
|
||||
|
||||
return view('common/' . $view, [
|
||||
$commonData = [
|
||||
'activity' => $activity,
|
||||
'recents' => $recents,
|
||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||
'draftPages' => $draftPages,
|
||||
'customHomepage' => $customHomepage,
|
||||
'books' => $books,
|
||||
'booksViewType' => $booksViewType
|
||||
]);
|
||||
];
|
||||
|
||||
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]);
|
||||
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]);
|
||||
return view('common.home-book', $data);
|
||||
}
|
||||
|
||||
if ($homepageOption === 'page') {
|
||||
$homepageSetting = setting('app-homepage', '0:');
|
||||
$id = intval(explode(':', $homepageSetting)[0]);
|
||||
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||
$this->entityRepo->renderPage($customHomepage, true);
|
||||
return view('common.home-custom', array_merge($commonData, ['customHomepage' => $customHomepage]));
|
||||
}
|
||||
|
||||
return view('common.home', $commonData);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -80,6 +79,7 @@ class HomeController extends Controller
|
||||
{
|
||||
$locale = app()->getLocale();
|
||||
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||
|
||||
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||
$resp = cache($cacheKey);
|
||||
} else {
|
||||
@@ -90,15 +90,6 @@ class HomeController extends Controller
|
||||
'entities' => trans('entities'),
|
||||
'errors' => trans('errors')
|
||||
];
|
||||
if ($locale !== 'en') {
|
||||
$enTrans = [
|
||||
'common' => trans('common', [], 'en'),
|
||||
'components' => trans('components', [], 'en'),
|
||||
'entities' => trans('entities', [], 'en'),
|
||||
'errors' => trans('errors', [], 'en')
|
||||
];
|
||||
$translations = array_replace_recursive($enTrans, $translations);
|
||||
}
|
||||
$resp = 'window.translations = ' . json_encode($translations);
|
||||
cache()->put($cacheKey, $resp, 120);
|
||||
}
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\ImageRepo;
|
||||
use BookStack\Repos\PageRepo;
|
||||
use BookStack\Uploads\Image;
|
||||
use BookStack\Uploads\ImageRepo;
|
||||
use Illuminate\Filesystem\Filesystem as File;
|
||||
use Illuminate\Http\Request;
|
||||
use BookStack\Image;
|
||||
use BookStack\Repos\PageRepo;
|
||||
|
||||
class ImageController extends Controller
|
||||
{
|
||||
@@ -220,7 +219,7 @@ class ImageController extends Controller
|
||||
|
||||
/**
|
||||
* Show the usage of an image on pages.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
||||
* @param $id
|
||||
* @return \Illuminate\Http\JsonResponse
|
||||
*/
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Activity;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\ExportService;
|
||||
use BookStack\Entities\Repos\PageRepo;
|
||||
use BookStack\Exceptions\NotFoundException;
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\ExportService;
|
||||
use Carbon\Carbon;
|
||||
use GatherContent\Htmldiff\Htmldiff;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Views;
|
||||
use GatherContent\Htmldiff\Htmldiff;
|
||||
|
||||
class PageController extends Controller
|
||||
{
|
||||
|
||||
protected $entityRepo;
|
||||
protected $pageRepo;
|
||||
protected $exportService;
|
||||
protected $userRepo;
|
||||
|
||||
/**
|
||||
* PageController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param ExportService $exportService
|
||||
* @param \BookStack\Entities\Repos\PageRepo $pageRepo
|
||||
* @param \BookStack\Entities\ExportService $exportService
|
||||
* @param UserRepo $userRepo
|
||||
*/
|
||||
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
|
||||
{
|
||||
$this->entityRepo = $entityRepo;
|
||||
$this->pageRepo = $pageRepo;
|
||||
$this->exportService = $exportService;
|
||||
$this->userRepo = $userRepo;
|
||||
parent::__construct();
|
||||
@@ -38,21 +38,28 @@ class PageController extends Controller
|
||||
* @param string $chapterSlug
|
||||
* @return Response
|
||||
* @internal param bool $pageSlug
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function create($bookSlug, $chapterSlug = null)
|
||||
{
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
if ($chapterSlug !== null) {
|
||||
$chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$book = $chapter->book;
|
||||
} else {
|
||||
$chapter = null;
|
||||
$book = $this->pageRepo->getBySlug('book', $bookSlug);
|
||||
}
|
||||
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
// Redirect to draft edit screen if signed in
|
||||
if ($this->signedIn) {
|
||||
$draft = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
return redirect($draft->getUrl());
|
||||
}
|
||||
|
||||
// Otherwise show edit view
|
||||
// Otherwise show the edit view if they're a guest
|
||||
$this->setPageTitle(trans('entities.pages_new'));
|
||||
return view('pages/guest-create', ['parent' => $parent]);
|
||||
}
|
||||
@@ -71,13 +78,19 @@ class PageController extends Controller
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||
if ($chapterSlug !== null) {
|
||||
$chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||
$book = $chapter->book;
|
||||
} else {
|
||||
$chapter = null;
|
||||
$book = $this->pageRepo->getBySlug('book', $bookSlug);
|
||||
}
|
||||
|
||||
$parent = $chapter ? $chapter : $book;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$page = $this->entityRepo->getDraftPage($book, $chapter);
|
||||
$this->entityRepo->publishPageDraft($page, [
|
||||
$page = $this->pageRepo->getDraftPage($book, $chapter);
|
||||
$this->pageRepo->publishPageDraft($page, [
|
||||
'name' => $request->get('name'),
|
||||
'html' => ''
|
||||
]);
|
||||
@@ -92,8 +105,8 @@ class PageController extends Controller
|
||||
*/
|
||||
public function editDraft($bookSlug, $pageId)
|
||||
{
|
||||
$draft = $this->entityRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft->book);
|
||||
$draft = $this->pageRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-create', $draft->parent);
|
||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||
|
||||
$draftsEnabled = $this->signedIn;
|
||||
@@ -119,21 +132,19 @@ class PageController extends Controller
|
||||
]);
|
||||
|
||||
$input = $request->all();
|
||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||
$draftPage = $this->pageRepo->getById('page', $pageId, true);
|
||||
$book = $draftPage->book;
|
||||
|
||||
$draftPage = $this->entityRepo->getById('page', $pageId, true);
|
||||
|
||||
$chapterId = intval($draftPage->chapter_id);
|
||||
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
|
||||
$parent = $draftPage->parent;
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
if ($parent->isA('chapter')) {
|
||||
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
|
||||
$input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
|
||||
} else {
|
||||
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
|
||||
$input['priority'] = $this->pageRepo->getNewBookPriority($parent);
|
||||
}
|
||||
|
||||
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
|
||||
$page = $this->pageRepo->publishPageDraft($draftPage, $input);
|
||||
|
||||
Activity::add($page, 'page_create', $book->id);
|
||||
return redirect($page->getUrl());
|
||||
@@ -150,9 +161,9 @@ class PageController extends Controller
|
||||
public function show($bookSlug, $pageSlug)
|
||||
{
|
||||
try {
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
} catch (NotFoundException $e) {
|
||||
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||
if ($page === null) {
|
||||
throw $e;
|
||||
}
|
||||
@@ -161,9 +172,9 @@ class PageController extends Controller
|
||||
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->entityRepo->getPageNav($page->html);
|
||||
$page->html = $this->pageRepo->renderPage($page);
|
||||
$sidebarTree = $this->pageRepo->getBookChildren($page->book);
|
||||
$pageNav = $this->pageRepo->getPageNav($page->html);
|
||||
|
||||
// check if the comment's are enabled
|
||||
$commentsEnabled = !setting('app-disable-comments');
|
||||
@@ -189,7 +200,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function getPageAjax($pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
$page = $this->pageRepo->getById('page', $pageId);
|
||||
return response()->json($page);
|
||||
}
|
||||
|
||||
@@ -198,28 +209,29 @@ class PageController extends Controller
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @return Response
|
||||
* @throws NotFoundException
|
||||
*/
|
||||
public function edit($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
|
||||
$page->isDraft = false;
|
||||
|
||||
// Check for active editing
|
||||
$warnings = [];
|
||||
if ($this->entityRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
|
||||
if ($this->pageRepo->isPageEditingActive($page, 60)) {
|
||||
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
|
||||
}
|
||||
|
||||
// Check for a current draft version for this user
|
||||
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
$page->name = $draft->name;
|
||||
$page->html = $draft->html;
|
||||
$page->markdown = $draft->markdown;
|
||||
$userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||
if ($userPageDraft !== null) {
|
||||
$page->name = $userPageDraft->name;
|
||||
$page->html = $userPageDraft->html;
|
||||
$page->markdown = $userPageDraft->markdown;
|
||||
$page->isDraft = true;
|
||||
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
|
||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
|
||||
}
|
||||
|
||||
if (count($warnings) > 0) {
|
||||
@@ -247,9 +259,9 @@ class PageController extends Controller
|
||||
$this->validate($request, [
|
||||
'name' => 'required|string|max:255'
|
||||
]);
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
|
||||
$this->pageRepo->updatePage($page, $page->book->id, $request->all());
|
||||
Activity::add($page, 'page_update', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
@@ -262,7 +274,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function saveDraft(Request $request, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$page = $this->pageRepo->getById('page', $pageId, true);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
|
||||
if (!$this->signedIn) {
|
||||
@@ -272,7 +284,7 @@ class PageController extends Controller
|
||||
], 500);
|
||||
}
|
||||
|
||||
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||
|
||||
$updateTime = $draft->updated_at->timestamp;
|
||||
return response()->json([
|
||||
@@ -290,7 +302,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function redirectFromLink($pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId);
|
||||
$page = $this->pageRepo->getById('page', $pageId);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
@@ -302,7 +314,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showDelete($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$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]);
|
||||
@@ -318,7 +330,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showDeleteDraft($bookSlug, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$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]);
|
||||
@@ -333,10 +345,10 @@ class PageController extends Controller
|
||||
*/
|
||||
public function destroy($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
$this->entityRepo->destroyPage($page);
|
||||
$this->pageRepo->destroyPage($page);
|
||||
|
||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||
session()->flash('success', trans('entities.pages_delete_success'));
|
||||
@@ -352,11 +364,11 @@ class PageController extends Controller
|
||||
*/
|
||||
public function destroyDraft($bookSlug, $pageId)
|
||||
{
|
||||
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||
$page = $this->pageRepo->getById('page', $pageId, true);
|
||||
$book = $page->book;
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
session()->flash('success', trans('entities.pages_delete_draft_success'));
|
||||
$this->entityRepo->destroyPage($page);
|
||||
$this->pageRepo->destroyPage($page);
|
||||
return redirect($book->getUrl());
|
||||
}
|
||||
|
||||
@@ -368,7 +380,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRevisions($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$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]);
|
||||
}
|
||||
@@ -382,7 +394,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
@@ -407,7 +419,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||
if ($revision === null) {
|
||||
abort(404);
|
||||
@@ -437,13 +449,47 @@ class PageController extends Controller
|
||||
*/
|
||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
|
||||
$page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
|
||||
Activity::add($page, 'page_restore', $page->book->id);
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Deletes a revision using the id of the specified revision.
|
||||
* @param string $bookSlug
|
||||
* @param string $pageSlug
|
||||
* @param int $revId
|
||||
* @throws NotFoundException
|
||||
* @throws BadRequestException
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
*/
|
||||
public function destroyRevision($bookSlug, $pageSlug, $revId)
|
||||
{
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
|
||||
$revision = $page->revisions()->where('id', '=', $revId)->first();
|
||||
if ($revision === null) {
|
||||
throw new NotFoundException("Revision #{$revId} not found");
|
||||
}
|
||||
|
||||
// Get the current revision for the page
|
||||
$currentRevision = $page->getCurrentRevision();
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
$revision->delete();
|
||||
session()->flash('success', trans('entities.revision_delete_success'));
|
||||
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exports a page to a PDF.
|
||||
* https://github.com/barryvdh/laravel-dompdf
|
||||
@@ -453,13 +499,10 @@ class PageController extends Controller
|
||||
*/
|
||||
public function exportPdf($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$page->html = $this->pageRepo->renderPage($page);
|
||||
$pdfContent = $this->exportService->pageToPdf($page);
|
||||
return response()->make($pdfContent, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||
]);
|
||||
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -470,13 +513,10 @@ class PageController extends Controller
|
||||
*/
|
||||
public function exportHtml($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page->html = $this->entityRepo->renderPage($page);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$page->html = $this->pageRepo->renderPage($page);
|
||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
|
||||
]);
|
||||
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -487,12 +527,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function exportPlainText($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||
return response()->make($containedHtml, 200, [
|
||||
'Content-Type' => 'application/octet-stream',
|
||||
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
|
||||
]);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$pageText = $this->exportService->pageToPlainText($page);
|
||||
return $this->downloadResponse($pageText, $pageSlug . '.txt');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,7 +538,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyCreated()
|
||||
{
|
||||
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
|
||||
$pages = $this->pageRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => trans('entities.recently_created_pages'),
|
||||
'pages' => $pages
|
||||
@@ -514,7 +551,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRecentlyUpdated()
|
||||
{
|
||||
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
||||
return view('pages/detailed-listing', [
|
||||
'title' => trans('entities.recently_updated_pages'),
|
||||
'pages' => $pages
|
||||
@@ -529,7 +566,7 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showRestrict($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$roles = $this->userRepo->getRestrictableRoles();
|
||||
return view('pages/restrictions', [
|
||||
@@ -547,8 +584,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showMove($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$this->checkOwnablePermission('page-delete', $page);
|
||||
return view('pages/move', [
|
||||
'book' => $page->book,
|
||||
'page' => $page
|
||||
@@ -565,8 +603,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function move($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$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 === '') {
|
||||
@@ -579,7 +618,7 @@ class PageController extends Controller
|
||||
|
||||
|
||||
try {
|
||||
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||
$parent = $this->pageRepo->getById($entityType, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||
return redirect()->back();
|
||||
@@ -587,7 +626,7 @@ class PageController extends Controller
|
||||
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$this->entityRepo->changePageParent($page, $parent);
|
||||
$this->pageRepo->changePageParent($page, $parent);
|
||||
Activity::add($page, 'page_move', $page->book->id);
|
||||
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
||||
|
||||
@@ -603,8 +642,8 @@ class PageController extends Controller
|
||||
*/
|
||||
public function showCopy($bookSlug, $pageSlug)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
session()->flashInput(['name' => $page->name]);
|
||||
return view('pages/copy', [
|
||||
'book' => $page->book,
|
||||
@@ -622,8 +661,8 @@ class PageController extends Controller
|
||||
*/
|
||||
public function copy($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-update', $page);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('page-view', $page);
|
||||
|
||||
$entitySelection = $request->get('entity_selection', null);
|
||||
if ($entitySelection === null || $entitySelection === '') {
|
||||
@@ -634,7 +673,7 @@ class PageController extends Controller
|
||||
$entityId = intval($stringExploded[1]);
|
||||
|
||||
try {
|
||||
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||
$parent = $this->pageRepo->getById($entityType, $entityId);
|
||||
} catch (\Exception $e) {
|
||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||
return redirect()->back();
|
||||
@@ -643,7 +682,7 @@ class PageController extends Controller
|
||||
|
||||
$this->checkOwnablePermission('page-create', $parent);
|
||||
|
||||
$pageCopy = $this->entityRepo->copyPage($page, $parent, $request->get('name', ''));
|
||||
$pageCopy = $this->pageRepo->copyPage($page, $parent, $request->get('name', ''));
|
||||
|
||||
Activity::add($pageCopy, 'page_create', $pageCopy->book->id);
|
||||
session()->flash('success', trans('entities.pages_copy_success'));
|
||||
@@ -661,9 +700,9 @@ class PageController extends Controller
|
||||
*/
|
||||
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||
{
|
||||
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||
session()->flash('success', trans('entities.pages_permissions_success'));
|
||||
return redirect($page->getUrl());
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionsRepo;
|
||||
use BookStack\Exceptions\PermissionsException;
|
||||
use BookStack\Repos\PermissionsRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class PermissionController extends Controller
|
||||
@@ -11,7 +11,7 @@ class PermissionController extends Controller
|
||||
|
||||
/**
|
||||
* PermissionController constructor.
|
||||
* @param PermissionsRepo $permissionsRepo
|
||||
* @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
|
||||
*/
|
||||
public function __construct(PermissionsRepo $permissionsRepo)
|
||||
{
|
||||
@@ -78,6 +78,7 @@ class PermissionController extends Controller
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||
* @throws PermissionsException
|
||||
*/
|
||||
public function updateRole($id, Request $request)
|
||||
{
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\EntityRepo;
|
||||
use BookStack\Services\SearchService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Actions\ViewService;
|
||||
use BookStack\Entities\Repos\EntityRepo;
|
||||
use BookStack\Entities\SearchService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class SearchController extends Controller
|
||||
@@ -13,7 +13,7 @@ class SearchController extends Controller
|
||||
|
||||
/**
|
||||
* SearchController constructor.
|
||||
* @param EntityRepo $entityRepo
|
||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
||||
* @param ViewService $viewService
|
||||
* @param SearchService $searchService
|
||||
*/
|
||||
@@ -97,7 +97,7 @@ class SearchController extends Controller
|
||||
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
||||
} else {
|
||||
$entityNames = $entityTypes->map(function ($type) {
|
||||
return 'BookStack\\' . ucfirst($type);
|
||||
return 'BookStack\\' . ucfirst($type); // TODO - Extract this elsewhere, too specific and stringy
|
||||
})->toArray();
|
||||
$entities = $this->viewService->getPopular(20, 0, $entityNames, $permission);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Uploads\ImageService;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use Setting;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use BookStack\Repos\TagRepo;
|
||||
use BookStack\Actions\TagRepo;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TagController extends Controller
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<?php namespace BookStack\Http\Controllers;
|
||||
|
||||
use Exception;
|
||||
use BookStack\Auth\Access\SocialAuthService;
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Auth\UserRepo;
|
||||
use BookStack\Exceptions\UserUpdateException;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\Response;
|
||||
use BookStack\Repos\UserRepo;
|
||||
use BookStack\Services\SocialAuthService;
|
||||
use BookStack\User;
|
||||
|
||||
class UserController extends Controller
|
||||
{
|
||||
@@ -60,6 +60,7 @@ class UserController extends Controller
|
||||
* Store a newly created user in storage.
|
||||
* @param Request $request
|
||||
* @return Response
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
@@ -90,10 +91,10 @@ class UserController extends Controller
|
||||
|
||||
if ($request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
$this->userRepo->setUserRoles($user, $roles);
|
||||
}
|
||||
|
||||
$this->userRepo->downloadGravatarToUserAvatar($user);
|
||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
||||
|
||||
return redirect('/settings/users');
|
||||
}
|
||||
@@ -101,7 +102,7 @@ class UserController extends Controller
|
||||
/**
|
||||
* Show the form for editing the specified user.
|
||||
* @param int $id
|
||||
* @param SocialAuthService $socialAuthService
|
||||
* @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
|
||||
* @return Response
|
||||
*/
|
||||
public function edit($id, SocialAuthService $socialAuthService)
|
||||
@@ -123,8 +124,9 @@ class UserController extends Controller
|
||||
/**
|
||||
* Update the specified user in storage.
|
||||
* @param Request $request
|
||||
* @param int $id
|
||||
* @param int $id
|
||||
* @return Response
|
||||
* @throws UserUpdateException
|
||||
*/
|
||||
public function update(Request $request, $id)
|
||||
{
|
||||
@@ -141,13 +143,13 @@ class UserController extends Controller
|
||||
'setting' => 'array'
|
||||
]);
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user = $this->userRepo->getById($id);
|
||||
$user->fill($request->all());
|
||||
|
||||
// Role updates
|
||||
if (userCan('users-manage') && $request->filled('roles')) {
|
||||
$roles = $request->get('roles');
|
||||
$user->roles()->sync($roles);
|
||||
$this->userRepo->setUserRoles($user, $roles);
|
||||
}
|
||||
|
||||
// Password updates
|
||||
@@ -186,7 +188,7 @@ class UserController extends Controller
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$user = $this->user->findOrFail($id);
|
||||
$user = $this->userRepo->getById($id);
|
||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||
return view('users/delete', ['user' => $user]);
|
||||
}
|
||||
@@ -195,6 +197,7 @@ class UserController extends Controller
|
||||
* Remove the specified user from storage.
|
||||
* @param int $id
|
||||
* @return Response
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
@@ -252,7 +255,7 @@ class UserController extends Controller
|
||||
return $this->currentUser->id == $id;
|
||||
});
|
||||
|
||||
$viewType = $request->get('book_view_type');
|
||||
$viewType = $request->get('view_type');
|
||||
if (!in_array($viewType, ['grid', 'list'])) {
|
||||
$viewType = 'list';
|
||||
}
|
||||
@@ -262,4 +265,27 @@ class UserController extends Controller
|
||||
|
||||
return redirect()->back(302, [], "/settings/users/$id");
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the user's preferred shelf-list display setting.
|
||||
* @param $id
|
||||
* @param Request $request
|
||||
* @return \Illuminate\Http\RedirectResponse
|
||||
*/
|
||||
public function switchShelfView($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->userRepo->getById($id);
|
||||
setting()->putUser($user, 'bookshelves_view_type', $viewType);
|
||||
|
||||
return redirect()->back(302, [], "/settings/users/$id");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,45 @@
|
||||
|
||||
use Carbon\Carbon;
|
||||
use Closure;
|
||||
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.
|
||||
*
|
||||
@@ -15,21 +51,54 @@ class Localization
|
||||
public function handle($request, Closure $next)
|
||||
{
|
||||
$defaultLang = config('app.locale');
|
||||
if (user()->isDefault()) {
|
||||
$locale = $defaultLang;
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (!in_array($lang, $availableLocales)) {
|
||||
continue;
|
||||
}
|
||||
$locale = $lang;
|
||||
break;
|
||||
}
|
||||
config()->set('app.default_locale', $defaultLang);
|
||||
|
||||
if (user()->isDefault() && config('app.auto_detect_locale')) {
|
||||
$locale = $this->autoDetectLocale($request, $defaultLang);
|
||||
} else {
|
||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||
}
|
||||
|
||||
// Set text direction
|
||||
if (in_array($locale, $this->rtlLocales)) {
|
||||
config()->set('app.rtl', true);
|
||||
}
|
||||
|
||||
app()->setLocale($locale);
|
||||
Carbon::setLocale($locale);
|
||||
$this->setSystemDateLocale($locale);
|
||||
return $next($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autodetect the visitors locale by matching locales in their headers
|
||||
* against the locales supported by BookStack.
|
||||
* @param Request $request
|
||||
* @param string $default
|
||||
* @return string
|
||||
*/
|
||||
protected function autoDetectLocale(Request $request, string $default)
|
||||
{
|
||||
$availableLocales = config('app.locales');
|
||||
foreach ($request->getLanguages() as $lang) {
|
||||
if (in_array($lang, $availableLocales)) {
|
||||
return $lang;
|
||||
}
|
||||
}
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
namespace BookStack\Http\Middleware;
|
||||
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Fideloper\Proxy\TrustProxies as Middleware;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class TrustProxies extends Middleware
|
||||
{
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Notifications;
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ConfirmEmail extends Notification implements ShouldQueue
|
||||
class ConfirmEmail extends MailNotification
|
||||
{
|
||||
|
||||
use Queueable;
|
||||
|
||||
public $token;
|
||||
|
||||
/**
|
||||
@@ -23,17 +13,6 @@ class ConfirmEmail extends Notification implements ShouldQueue
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's delivery channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the mail representation of the notification.
|
||||
*
|
||||
@@ -43,10 +22,10 @@ class ConfirmEmail extends Notification implements ShouldQueue
|
||||
public function toMail($notifiable)
|
||||
{
|
||||
$appName = ['appName' => setting('app-name')];
|
||||
return (new MailMessage)
|
||||
->subject(trans('auth.email_confirm_subject', $appName))
|
||||
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
return $this->newMailMessage()
|
||||
->subject(trans('auth.email_confirm_subject', $appName))
|
||||
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||
->line(trans('auth.email_confirm_text'))
|
||||
->action(trans('auth.email_confirm_action'), baseUrl('/register/confirm/' . $this->token));
|
||||
}
|
||||
}
|
||||
|
||||
34
app/Notifications/MailNotification.php
Normal file
34
app/Notifications/MailNotification.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
class MailNotification extends Notification implements ShouldQueue
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array|string
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new mail message.
|
||||
* @return MailMessage
|
||||
*/
|
||||
protected function newMailMessage()
|
||||
{
|
||||
return (new MailMessage)->view([
|
||||
'html' => 'vendor.notifications.email',
|
||||
'text' => 'vendor.notifications.email-plain'
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
<?php
|
||||
<?php namespace BookStack\Notifications;
|
||||
|
||||
namespace BookStack\Notifications;
|
||||
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
class ResetPassword extends Notification
|
||||
class ResetPassword extends MailNotification
|
||||
{
|
||||
/**
|
||||
* The password reset token.
|
||||
@@ -24,17 +19,6 @@ class ResetPassword extends Notification
|
||||
$this->token = $token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the notification's channels.
|
||||
*
|
||||
* @param mixed $notifiable
|
||||
* @return array|string
|
||||
*/
|
||||
public function via($notifiable)
|
||||
{
|
||||
return ['mail'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the mail representation of the notification.
|
||||
*
|
||||
@@ -42,7 +26,7 @@ class ResetPassword extends Notification
|
||||
*/
|
||||
public function toMail()
|
||||
{
|
||||
return (new MailMessage)
|
||||
return $this->newMailMessage()
|
||||
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
|
||||
->line(trans('auth.email_reset_text'))
|
||||
->action(trans('auth.reset_password'), baseUrl('password/reset/' . $this->token))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
|
||||
abstract class Ownable extends Model
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Services\SettingService;
|
||||
use BookStack\Setting;
|
||||
use Blade;
|
||||
use BookStack\Entities\Book;
|
||||
use BookStack\Entities\Bookshelf;
|
||||
use BookStack\Entities\Chapter;
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Settings\Setting;
|
||||
use BookStack\Settings\SettingService;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Schema;
|
||||
use Validator;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
@@ -20,12 +27,21 @@ class AppServiceProvider extends ServiceProvider
|
||||
return in_array($value->getMimeType(), $imageMimes);
|
||||
});
|
||||
|
||||
\Blade::directive('icon', function ($expression) {
|
||||
// Custom blade view directives
|
||||
Blade::directive('icon', function ($expression) {
|
||||
return "<?php echo icon($expression); ?>";
|
||||
});
|
||||
|
||||
// Allow longer string lengths after upgrade to utf8mb4
|
||||
\Schema::defaultStringLength(191);
|
||||
Schema::defaultStringLength(191);
|
||||
|
||||
// Set morph-map due to namespace changes
|
||||
Relation::morphMap([
|
||||
'BookStack\\Bookshelf' => Bookshelf::class,
|
||||
'BookStack\\Book' => Book::class,
|
||||
'BookStack\\Chapter' => Chapter::class,
|
||||
'BookStack\\Page' => Page::class,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Auth;
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AuthServiceProvider extends ServiceProvider
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
class BroadcastServiceProvider extends ServiceProvider
|
||||
{
|
||||
|
||||
@@ -2,18 +2,19 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Activity;
|
||||
use BookStack\Image;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Services\ViewService;
|
||||
use BookStack\Setting;
|
||||
use BookStack\View;
|
||||
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 BookStack\Services\ActivityService;
|
||||
use BookStack\Services\SettingService;
|
||||
use Intervention\Image\ImageManager;
|
||||
|
||||
class CustomFacadeProvider extends ServiceProvider
|
||||
@@ -61,7 +62,8 @@ class CustomFacadeProvider extends ServiceProvider
|
||||
$this->app->make(Image::class),
|
||||
$this->app->make(ImageManager::class),
|
||||
$this->app->make(Factory::class),
|
||||
$this->app->make(Repository::class)
|
||||
$this->app->make(Repository::class),
|
||||
$this->app->make(HttpFetcher::class)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Contracts\Events\Dispatcher as DispatcherContract;
|
||||
use Illuminate\Foundation\Support\Providers\EventServiceProvider as ServiceProvider;
|
||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||
|
||||
@@ -20,6 +19,7 @@ class EventServiceProvider extends ServiceProvider
|
||||
'SocialiteProviders\Okta\OktaExtendSocialite@handle',
|
||||
'SocialiteProviders\GitLab\GitLabExtendSocialite@handle',
|
||||
'SocialiteProviders\Twitch\TwitchExtendSocialite@handle',
|
||||
'SocialiteProviders\Discord\DiscordExtendSocialite@handle',
|
||||
],
|
||||
];
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Role;
|
||||
use BookStack\Services\LdapService;
|
||||
use BookStack\User;
|
||||
use BookStack\Auth\Access\LdapService;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
use Illuminate\Contracts\Auth\UserProvider;
|
||||
|
||||
@@ -19,7 +17,7 @@ class LdapUserProvider implements UserProvider
|
||||
protected $model;
|
||||
|
||||
/**
|
||||
* @var LdapService
|
||||
* @var \BookStack\Auth\LdapService
|
||||
*/
|
||||
protected $ldapService;
|
||||
|
||||
@@ -27,7 +25,7 @@ class LdapUserProvider implements UserProvider
|
||||
/**
|
||||
* LdapUserProvider constructor.
|
||||
* @param $model
|
||||
* @param LdapService $ldapService
|
||||
* @param \BookStack\Auth\LdapService $ldapService
|
||||
*/
|
||||
public function __construct($model, LdapService $ldapService)
|
||||
{
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace BookStack\Providers;
|
||||
|
||||
use Illuminate\Routing\Router;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Route;
|
||||
|
||||
|
||||
31
app/Providers/TranslationServiceProvider.php
Normal file
31
app/Providers/TranslationServiceProvider.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?php namespace BookStack\Providers;
|
||||
|
||||
use BookStack\Translation\Translator;
|
||||
|
||||
class TranslationServiceProvider extends \Illuminate\Translation\TranslationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register the service provider.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function register()
|
||||
{
|
||||
$this->registerLoader();
|
||||
|
||||
$this->app->singleton('translator', function ($app) {
|
||||
$loader = $app['translation.loader'];
|
||||
|
||||
// When registering the translator component, we'll need to set the default
|
||||
// locale as well as the fallback locale. So, we'll grab the application
|
||||
// configuration so we can easily get both of these values from there.
|
||||
$locale = $app['config']['app.locale'];
|
||||
|
||||
$trans = new Translator($loader, $locale);
|
||||
|
||||
$trans->setFallback($app['config']['app.fallback_locale']);
|
||||
|
||||
return $trans;
|
||||
});
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,165 +0,0 @@
|
||||
<?php namespace BookStack\Services;
|
||||
|
||||
use BookStack\Exceptions\LdapException;
|
||||
use Illuminate\Contracts\Auth\Authenticatable;
|
||||
|
||||
/**
|
||||
* Class LdapService
|
||||
* Handles any app-specific LDAP tasks.
|
||||
* @package BookStack\Services
|
||||
*/
|
||||
class LdapService
|
||||
{
|
||||
|
||||
protected $ldap;
|
||||
protected $ldapConnection;
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* LdapService constructor.
|
||||
* @param Ldap $ldap
|
||||
*/
|
||||
public function __construct(Ldap $ldap)
|
||||
{
|
||||
$this->ldap = $ldap;
|
||||
$this->config = config('services.ldap');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the details of a user from LDAP using the given username.
|
||||
* User found via configurable user filter.
|
||||
* @param $userName
|
||||
* @return array|null
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function getUserDetails($userName)
|
||||
{
|
||||
$ldapConnection = $this->getConnection();
|
||||
$this->bindSystemUser($ldapConnection);
|
||||
|
||||
// Find user
|
||||
$userFilter = $this->buildFilter($this->config['user_filter'], ['user' => $userName]);
|
||||
$baseDn = $this->config['base_dn'];
|
||||
$emailAttr = $this->config['email_attribute'];
|
||||
$followReferrals = $this->config['follow_referrals'] ? 1 : 0;
|
||||
$this->ldap->setOption($ldapConnection, LDAP_OPT_REFERRALS, $followReferrals);
|
||||
$users = $this->ldap->searchAndGetEntries($ldapConnection, $baseDn, $userFilter, ['cn', 'uid', 'dn', $emailAttr]);
|
||||
if ($users['count'] === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $users[0];
|
||||
return [
|
||||
'uid' => (isset($user['uid'])) ? $user['uid'][0] : $user['dn'],
|
||||
'name' => $user['cn'][0],
|
||||
'dn' => $user['dn'],
|
||||
'email' => (isset($user[$emailAttr])) ? (is_array($user[$emailAttr]) ? $user[$emailAttr][0] : $user[$emailAttr]) : null
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Authenticatable $user
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @return bool
|
||||
* @throws LdapException
|
||||
*/
|
||||
public function validateUserCredentials(Authenticatable $user, $username, $password)
|
||||
{
|
||||
$ldapUser = $this->getUserDetails($username);
|
||||
if ($ldapUser === null) {
|
||||
return false;
|
||||
}
|
||||
if ($ldapUser['uid'] !== $user->external_auth_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$ldapConnection = $this->getConnection();
|
||||
try {
|
||||
$ldapBind = $this->ldap->bind($ldapConnection, $ldapUser['dn'], $password);
|
||||
} catch (\ErrorException $e) {
|
||||
$ldapBind = false;
|
||||
}
|
||||
|
||||
return $ldapBind;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind the system user to the LDAP connection using the given credentials
|
||||
* otherwise anonymous access is attempted.
|
||||
* @param $connection
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function bindSystemUser($connection)
|
||||
{
|
||||
$ldapDn = $this->config['dn'];
|
||||
$ldapPass = $this->config['pass'];
|
||||
|
||||
$isAnonymous = ($ldapDn === false || $ldapPass === false);
|
||||
if ($isAnonymous) {
|
||||
$ldapBind = $this->ldap->bind($connection);
|
||||
} else {
|
||||
$ldapBind = $this->ldap->bind($connection, $ldapDn, $ldapPass);
|
||||
}
|
||||
|
||||
if (!$ldapBind) {
|
||||
throw new LdapException(($isAnonymous ? trans('errors.ldap_fail_anonymous') : trans('errors.ldap_fail_authed')));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the connection to the LDAP server.
|
||||
* Creates a new connection if one does not exist.
|
||||
* @return resource
|
||||
* @throws LdapException
|
||||
*/
|
||||
protected function getConnection()
|
||||
{
|
||||
if ($this->ldapConnection !== null) {
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
// Check LDAP extension in installed
|
||||
if (!function_exists('ldap_connect') && config('app.env') !== 'testing') {
|
||||
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;
|
||||
$ldapConnection = $this->ldap->connect($hostName, count($ldapServer) > 2 ? intval($ldapServer[2]) : $defaultPort);
|
||||
|
||||
if ($ldapConnection === false) {
|
||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||
}
|
||||
|
||||
// Set any required options
|
||||
if ($this->config['version']) {
|
||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
||||
}
|
||||
|
||||
$this->ldapConnection = $ldapConnection;
|
||||
return $this->ldapConnection;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a filter string by injecting common variables.
|
||||
* @param string $filterString
|
||||
* @param array $attrs
|
||||
* @return string
|
||||
*/
|
||||
protected function buildFilter($filterString, array $attrs)
|
||||
{
|
||||
$newAttrs = [];
|
||||
foreach ($attrs as $key => $attrText) {
|
||||
$newKey = '${' . $key . '}';
|
||||
$newAttrs[$newKey] = $attrText;
|
||||
}
|
||||
return strtr($filterString, $newAttrs);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Settings;
|
||||
|
||||
use BookStack\Model;
|
||||
|
||||
class Setting extends Model
|
||||
{
|
||||
@@ -1,7 +1,5 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Settings;
|
||||
|
||||
use BookStack\Setting;
|
||||
use BookStack\User;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
|
||||
/**
|
||||
@@ -43,6 +41,7 @@ class SettingService
|
||||
if ($default === false) {
|
||||
$default = config('setting-defaults.' . $key, false);
|
||||
}
|
||||
|
||||
if (isset($this->localCache[$key])) {
|
||||
return $this->localCache[$key];
|
||||
}
|
||||
@@ -55,7 +54,7 @@ class SettingService
|
||||
|
||||
/**
|
||||
* Get a user-specific setting from the database or cache.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param $key
|
||||
* @param bool $default
|
||||
* @return bool|string
|
||||
@@ -174,7 +173,7 @@ class SettingService
|
||||
|
||||
/**
|
||||
* Put a user-specific setting into the database.
|
||||
* @param User $user
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param $key
|
||||
* @param $value
|
||||
* @return bool
|
||||
72
app/Translation/Translator.php
Normal file
72
app/Translation/Translator.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php namespace BookStack\Translation;
|
||||
|
||||
class Translator extends \Illuminate\Translation\Translator
|
||||
{
|
||||
|
||||
/**
|
||||
* Mapping of locales to their base locales
|
||||
* @var array
|
||||
*/
|
||||
protected $baseLocaleMap = [
|
||||
'de_informal' => 'de',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the translation for a given key.
|
||||
*
|
||||
* @param string $key
|
||||
* @param array $replace
|
||||
* @param string $locale
|
||||
* @return string|array|null
|
||||
*/
|
||||
public function trans($key, array $replace = [], $locale = null)
|
||||
{
|
||||
$translation = $this->get($key, $replace, $locale);
|
||||
|
||||
if (is_array($translation)) {
|
||||
$translation = $this->mergeBackupTranslations($translation, $key, $locale);
|
||||
}
|
||||
|
||||
return $translation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge the fallback translations, and base translations if existing,
|
||||
* into the provided core key => value array of translations content.
|
||||
* @param array $translationArray
|
||||
* @param string $key
|
||||
* @param null $locale
|
||||
* @return array
|
||||
*/
|
||||
protected function mergeBackupTranslations(array $translationArray, string $key, $locale = null)
|
||||
{
|
||||
$fallback = $this->get($key, [], $this->fallback);
|
||||
$baseLocale = $this->getBaseLocale($locale ?? $this->locale);
|
||||
$baseTranslations = $baseLocale ? $this->get($key, [], $baseLocale) : [];
|
||||
|
||||
return array_replace_recursive($fallback, $baseTranslations, $translationArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the array of locales to be checked.
|
||||
*
|
||||
* @param string|null $locale
|
||||
* @return array
|
||||
*/
|
||||
protected function localeArray($locale)
|
||||
{
|
||||
$primaryLocale = $locale ?: $this->locale;
|
||||
return array_filter([$primaryLocale, $this->getBaseLocale($primaryLocale), $this->fallback]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the locale to extend for the given locale.
|
||||
*
|
||||
* @param string $locale
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getBaseLocale($locale)
|
||||
{
|
||||
return $this->baseLocaleMap[$locale] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,7 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Entities\Page;
|
||||
use BookStack\Ownable;
|
||||
|
||||
class Attachment extends Ownable
|
||||
{
|
||||
@@ -18,7 +21,7 @@ class Attachment extends Ownable
|
||||
|
||||
/**
|
||||
* Get the page this file was uploaded to.
|
||||
* @return Page
|
||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
||||
*/
|
||||
public function page()
|
||||
{
|
||||
@@ -1,7 +1,6 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Exceptions\FileUploadException;
|
||||
use BookStack\Attachment;
|
||||
use Exception;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
33
app/Uploads/HttpFetcher.php
Normal file
33
app/Uploads/HttpFetcher.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
|
||||
class HttpFetcher
|
||||
{
|
||||
|
||||
/**
|
||||
* Fetch content from an external URI.
|
||||
* @param string $uri
|
||||
* @return bool|string
|
||||
* @throws HttpFetchException
|
||||
*/
|
||||
public function fetch(string $uri)
|
||||
{
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $uri,
|
||||
CURLOPT_RETURNTRANSFER => 1,
|
||||
CURLOPT_CONNECTTIMEOUT => 5
|
||||
]);
|
||||
|
||||
$data = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($err) {
|
||||
throw new HttpFetchException($err);
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php namespace BookStack;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Ownable;
|
||||
use Images;
|
||||
|
||||
class Image extends Ownable
|
||||
@@ -19,5 +20,4 @@ class Image extends Ownable
|
||||
{
|
||||
return Images::getThumbnail($this, $width, $height, $keepRatio);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
<?php namespace BookStack\Repos;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Image;
|
||||
use BookStack\Page;
|
||||
use BookStack\Services\ImageService;
|
||||
use BookStack\Services\PermissionService;
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Page;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageRepo
|
||||
@@ -18,8 +16,8 @@ class ImageRepo
|
||||
* ImageRepo constructor.
|
||||
* @param Image $image
|
||||
* @param ImageService $imageService
|
||||
* @param PermissionService $permissionService
|
||||
* @param Page $page
|
||||
* @param \BookStack\Auth\Permissions\PermissionService $permissionService
|
||||
* @param \BookStack\Entities\Page $page
|
||||
*/
|
||||
public function __construct(Image $image, ImageService $imageService, PermissionService $permissionService, Page $page)
|
||||
{
|
||||
@@ -1,14 +1,14 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use BookStack\Auth\User;
|
||||
use BookStack\Exceptions\HttpFetchException;
|
||||
use BookStack\Exceptions\ImageUploadException;
|
||||
use BookStack\Image;
|
||||
use BookStack\User;
|
||||
use DB;
|
||||
use Exception;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Intervention\Image\Exception\NotSupportedException;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Cache\Repository as Cache;
|
||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
|
||||
class ImageService extends UploadService
|
||||
@@ -18,6 +18,7 @@ class ImageService extends UploadService
|
||||
protected $cache;
|
||||
protected $storageUrl;
|
||||
protected $image;
|
||||
protected $http;
|
||||
|
||||
/**
|
||||
* ImageService constructor.
|
||||
@@ -25,12 +26,14 @@ class ImageService extends UploadService
|
||||
* @param ImageManager $imageTool
|
||||
* @param FileSystem $fileSystem
|
||||
* @param Cache $cache
|
||||
* @param HttpFetcher $http
|
||||
*/
|
||||
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache)
|
||||
public function __construct(Image $image, ImageManager $imageTool, FileSystem $fileSystem, Cache $cache, HttpFetcher $http)
|
||||
{
|
||||
$this->image = $image;
|
||||
$this->imageTool = $imageTool;
|
||||
$this->cache = $cache;
|
||||
$this->http = $http;
|
||||
parent::__construct($fileSystem);
|
||||
}
|
||||
|
||||
@@ -96,8 +99,9 @@ class ImageService extends UploadService
|
||||
private function saveNewFromUrl($url, $type, $imageName = false)
|
||||
{
|
||||
$imageName = $imageName ? $imageName : basename($url);
|
||||
$imageData = file_get_contents($url);
|
||||
if ($imageData === false) {
|
||||
try {
|
||||
$imageData = $this->http->fetch($url);
|
||||
} catch (HttpFetchException $exception) {
|
||||
throw new \Exception(trans('errors.cannot_get_image_from_url', ['url' => $url]));
|
||||
}
|
||||
return $this->saveNew($imageName, $imageData, $type);
|
||||
@@ -280,24 +284,57 @@ class ImageService extends UploadService
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a gravatar image and set a the profile image for a user.
|
||||
* @param User $user
|
||||
* Save an avatar image from an external service.
|
||||
* @param \BookStack\Auth\User $user
|
||||
* @param int $size
|
||||
* @return mixed
|
||||
* @return Image
|
||||
* @throws Exception
|
||||
*/
|
||||
public function saveUserGravatar(User $user, $size = 500)
|
||||
public function saveUserAvatar(User $user, $size = 500)
|
||||
{
|
||||
$emailHash = md5(strtolower(trim($user->email)));
|
||||
$url = 'https://www.gravatar.com/avatar/' . $emailHash . '?s=' . $size . '&d=identicon';
|
||||
$imageName = str_replace(' ', '-', $user->name . '-gravatar.png');
|
||||
$image = $this->saveNewFromUrl($url, 'user', $imageName);
|
||||
$avatarUrl = $this->getAvatarUrl();
|
||||
$email = strtolower(trim($user->email));
|
||||
|
||||
$replacements = [
|
||||
'${hash}' => md5($email),
|
||||
'${size}' => $size,
|
||||
'${email}' => urlencode($email),
|
||||
];
|
||||
|
||||
$userAvatarUrl = strtr($avatarUrl, $replacements);
|
||||
$imageName = str_replace(' ', '-', $user->name . '-avatar.png');
|
||||
$image = $this->saveNewFromUrl($userAvatarUrl, 'user', $imageName);
|
||||
$image->created_by = $user->id;
|
||||
$image->updated_by = $user->id;
|
||||
$image->save();
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if fetching external avatars is enabled.
|
||||
* @return bool
|
||||
*/
|
||||
public function avatarFetchEnabled()
|
||||
{
|
||||
$fetchUrl = $this->getAvatarUrl();
|
||||
return is_string($fetchUrl) && strpos($fetchUrl, 'http') === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the URL to fetch avatars from.
|
||||
* @return string|mixed
|
||||
*/
|
||||
protected function getAvatarUrl()
|
||||
{
|
||||
$url = trim(config('services.avatar_url'));
|
||||
|
||||
if (empty($url) && !config('services.disable_services')) {
|
||||
$url = 'https://www.gravatar.com/avatar/${hash}?s=${size}&d=identicon';
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete gallery and drawings that are not within HTML content of pages or page revisions.
|
||||
@@ -316,25 +353,25 @@ class ImageService extends UploadService
|
||||
$deletedPaths = [];
|
||||
|
||||
$this->image->newQuery()->whereIn('type', $types)
|
||||
->chunk(1000, function($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
|
||||
foreach ($images as $image) {
|
||||
$searchQuery = '%' . basename($image->path) . '%';
|
||||
$inPage = DB::table('pages')
|
||||
->chunk(1000, function ($images) use ($types, $checkRevisions, &$deletedPaths, $dryRun) {
|
||||
foreach ($images as $image) {
|
||||
$searchQuery = '%' . basename($image->path) . '%';
|
||||
$inPage = DB::table('pages')
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
$inRevision = false;
|
||||
if ($checkRevisions) {
|
||||
$inRevision = DB::table('page_revisions')
|
||||
$inRevision = false;
|
||||
if ($checkRevisions) {
|
||||
$inRevision = DB::table('page_revisions')
|
||||
->where('html', 'like', $searchQuery)->count() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$inPage && !$inRevision) {
|
||||
$deletedPaths[] = $image->path;
|
||||
if (!$dryRun) {
|
||||
$this->destroy($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if (!$inPage && !$inRevision) {
|
||||
$deletedPaths[] = $image->path;
|
||||
if (!$dryRun) {
|
||||
$this->destroy($image);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return $deletedPaths;
|
||||
}
|
||||
|
||||
@@ -366,14 +403,7 @@ class ImageService extends UploadService
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
$ch = curl_init();
|
||||
curl_setopt_array($ch, [CURLOPT_URL => $uri, CURLOPT_RETURNTRANSFER => 1, CURLOPT_CONNECTTIMEOUT => 5]);
|
||||
$imageData = curl_exec($ch);
|
||||
$err = curl_error($ch);
|
||||
curl_close($ch);
|
||||
if ($err) {
|
||||
throw new \Exception("Image fetch failed, Received error: " . $err);
|
||||
}
|
||||
$imageData = $this->http->fetch($uri);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
<?php namespace BookStack\Services;
|
||||
<?php namespace BookStack\Uploads;
|
||||
|
||||
use Illuminate\Contracts\Filesystem\Factory as FileSystem;
|
||||
use Illuminate\Contracts\Filesystem\Filesystem as FileSystemInstance;
|
||||
|
||||
class UploadService
|
||||
abstract class UploadService
|
||||
{
|
||||
|
||||
/**
|
||||
@@ -1,5 +1,7 @@
|
||||
<?php
|
||||
|
||||
use BookStack\Auth\Permissions\PermissionService;
|
||||
use BookStack\Entities\Entity;
|
||||
use BookStack\Ownable;
|
||||
|
||||
/**
|
||||
@@ -30,11 +32,11 @@ function versioned_asset($file = '')
|
||||
/**
|
||||
* Helper method to get the current User.
|
||||
* Defaults to public 'Guest' user if not logged in.
|
||||
* @return \BookStack\User
|
||||
* @return \BookStack\Auth\User
|
||||
*/
|
||||
function user()
|
||||
{
|
||||
return auth()->user() ?: \BookStack\User::getDefault();
|
||||
return auth()->user() ?: \BookStack\Auth\User::getDefault();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -50,30 +52,43 @@ function signedInUser()
|
||||
* 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\Services\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
|
||||
* @param bool $default
|
||||
* @return bool|string|\BookStack\Services\SettingService
|
||||
* @return bool|string|\BookStack\Settings\SettingService
|
||||
*/
|
||||
function setting($key = null, $default = false)
|
||||
{
|
||||
$settingService = resolve(\BookStack\Services\SettingService::class);
|
||||
$settingService = resolve(\BookStack\Settings\SettingService::class);
|
||||
if (is_null($key)) {
|
||||
return $settingService;
|
||||
}
|
||||
@@ -92,10 +107,15 @@ function baseUrl($path, $forceAppDomain = false)
|
||||
if ($isFullUrl && !$forceAppDomain) {
|
||||
return $path;
|
||||
}
|
||||
|
||||
$path = trim($path, '/');
|
||||
$base = rtrim(config('app.url'), '/');
|
||||
|
||||
// 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));
|
||||
}
|
||||
$explodedPath = explode('/', $path);
|
||||
$path = implode('/', array_splice($explodedPath, 3));
|
||||
}
|
||||
@@ -105,7 +125,7 @@ function baseUrl($path, $forceAppDomain = false)
|
||||
return url($path);
|
||||
}
|
||||
|
||||
return rtrim(config('app.url'), '/') . '/' . $path;
|
||||
return $base . '/' . $path;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -5,12 +5,18 @@
|
||||
"license": "MIT",
|
||||
"type": "project",
|
||||
"require": {
|
||||
"php": ">=7.0.0",
|
||||
"laravel/framework": "~5.5.22",
|
||||
"fideloper/proxy": "~3.3",
|
||||
"php": ">=7.0.5",
|
||||
"ext-json": "*",
|
||||
"ext-tidy": "*",
|
||||
"ext-dom": "*",
|
||||
"ext-xml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"ext-gd": "*",
|
||||
"ext-curl": "*",
|
||||
"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",
|
||||
@@ -20,7 +26,9 @@
|
||||
"socialiteproviders/microsoft-azure": "^3.0",
|
||||
"socialiteproviders/okta": "^1.0",
|
||||
"socialiteproviders/gitlab": "^3.0",
|
||||
"socialiteproviders/twitch": "^3.0"
|
||||
"socialiteproviders/twitch": "^3.0",
|
||||
"socialiteproviders/discord": "^2.0",
|
||||
"doctrine/dbal": "^2.5"
|
||||
},
|
||||
"require-dev": {
|
||||
"filp/whoops": "~2.0",
|
||||
@@ -79,7 +87,7 @@
|
||||
"optimize-autoloader": true,
|
||||
"preferred-install": "dist",
|
||||
"platform": {
|
||||
"php": "7.0"
|
||||
"php": "7.0.5"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1566
composer.lock
generated
1566
composer.lock
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user