mirror of
https://github.com/BookStackApp/BookStack.git
synced 2026-02-25 11:19:39 +03:00
Compare commits
88 Commits
captcha_ex
...
v0.18.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75915e8a94 | ||
|
|
9bde0ae4ea | ||
|
|
0c802d1f86 | ||
|
|
b7a96c6466 | ||
|
|
4b645a82c7 | ||
|
|
d599b77b6f | ||
|
|
26e93dc8c1 | ||
|
|
a4c9a8491b | ||
|
|
70ee636d87 | ||
|
|
b35f6dbb03 | ||
|
|
67d9e24d8f | ||
|
|
3903fda6ca | ||
|
|
441e46ebaa | ||
|
|
1f4260f359 | ||
|
|
dc0bf8ad4e | ||
|
|
102e326e6a | ||
|
|
2b25bf6f3b | ||
|
|
f93280696d | ||
|
|
1787391b07 | ||
|
|
a74a8ee483 | ||
|
|
7fa5405cb7 | ||
|
|
6725ddcc41 | ||
|
|
bce941db3f | ||
|
|
6d926048ec | ||
|
|
5335c973b4 | ||
|
|
15c3e5c96e | ||
|
|
a5d5904969 | ||
|
|
598758b991 | ||
|
|
9926e23bc8 | ||
|
|
5d3264bc63 | ||
|
|
d71f819f95 | ||
|
|
ee13509760 | ||
|
|
82d7bb1f32 | ||
|
|
cdfda508d8 | ||
|
|
da941e584f | ||
|
|
65874d7b96 | ||
|
|
ac9b8f405c | ||
|
|
8d1419a12e | ||
|
|
04f7a7d301 | ||
|
|
c10d2a1493 | ||
|
|
97bbf79ffd | ||
|
|
f7b01ae53d | ||
|
|
d704e1dbba | ||
|
|
ef2ff5e093 | ||
|
|
7caed3b0db | ||
|
|
45641d0754 | ||
|
|
4b1d08ba99 | ||
|
|
160fa99ba4 | ||
|
|
d2a5ab49ed | ||
|
|
c6404d8917 | ||
|
|
7113807f12 | ||
|
|
be711215e8 | ||
|
|
7e3b404240 | ||
|
|
e86901ca20 | ||
|
|
bdfa61c8b2 | ||
|
|
2cc36787f5 | ||
|
|
448ac61b48 | ||
|
|
753f6394f7 | ||
|
|
b1faf65934 | ||
|
|
09f478bd74 | ||
|
|
a0497feddd | ||
|
|
789693bde9 | ||
|
|
1fe933e4ea | ||
|
|
724b4b5a70 | ||
|
|
1778a56146 | ||
|
|
744865fcb2 | ||
|
|
7f8c8b448d | ||
|
|
a67c53826d | ||
|
|
14b131e850 | ||
|
|
9b55a52b85 | ||
|
|
db1d10e80f | ||
|
|
1be576966f | ||
|
|
b97e792c5f | ||
|
|
8dec674cc3 | ||
|
|
f784c03746 | ||
|
|
148e172fe8 | ||
|
|
56ae86646f | ||
|
|
1d2b6fdfa2 | ||
|
|
4fc75beed4 | ||
|
|
3b3bc0c4bf | ||
|
|
910faab88e | ||
|
|
f184d763ad | ||
|
|
a91d42634d | ||
|
|
f517ef3616 | ||
|
|
e99507ddcf | ||
|
|
d2cacf1945 | ||
|
|
448ac1405b | ||
|
|
6ad21ce885 |
68
.env.example
68
.env.example
@@ -1,14 +1,11 @@
|
|||||||
# Application key
|
# Environment
|
||||||
# Used for encryption where needed.
|
APP_ENV=production
|
||||||
# Run `php artisan key:generate` to generate a valid key.
|
APP_DEBUG=false
|
||||||
APP_KEY=SomeRandomString
|
APP_KEY=SomeRandomString
|
||||||
|
|
||||||
# Application URL
|
# The below url has to be set if using social auth options
|
||||||
# Remove the hash below and set a URL if using BookStack behind
|
# or if you are not using BookStack at the root path of your domain.
|
||||||
# a proxy, if using a third-party authentication option.
|
# APP_URL=http://bookstack.dev
|
||||||
# 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
|
# Database details
|
||||||
DB_HOST=localhost
|
DB_HOST=localhost
|
||||||
@@ -16,16 +13,55 @@ DB_DATABASE=database_database
|
|||||||
DB_USERNAME=database_username
|
DB_USERNAME=database_username
|
||||||
DB_PASSWORD=database_user_password
|
DB_PASSWORD=database_user_password
|
||||||
|
|
||||||
# Mail system to use
|
# Cache and session
|
||||||
# Can be 'smtp', 'mail' or 'sendmail'
|
CACHE_DRIVER=file
|
||||||
MAIL_DRIVER=smtp
|
SESSION_DRIVER=file
|
||||||
|
# If using Memcached, comment the above and uncomment these
|
||||||
|
#CACHE_DRIVER=memcached
|
||||||
|
#SESSION_DRIVER=memcached
|
||||||
|
QUEUE_DRIVER=sync
|
||||||
|
|
||||||
# SMTP mail options
|
# 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
|
||||||
|
|
||||||
|
# External services such as Gravatar
|
||||||
|
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_DRIVER=smtp
|
||||||
MAIL_HOST=localhost
|
MAIL_HOST=localhost
|
||||||
MAIL_PORT=1025
|
MAIL_PORT=1025
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
|
||||||
|
|
||||||
# A full list of options can be found in the '.env.example.complete' file.
|
|
||||||
@@ -1,237 +0,0 @@
|
|||||||
# Full list of environment variables that can be used with BookStack.
|
|
||||||
# Selectively copy these to your '.env' file as required.
|
|
||||||
# Each option is shown with it's default value.
|
|
||||||
# Do not copy this whole file to use as your '.env' file.
|
|
||||||
|
|
||||||
# Application environment
|
|
||||||
# Can be 'production', 'development', 'testing' or 'demo'
|
|
||||||
APP_ENV=production
|
|
||||||
|
|
||||||
# Enable debug mode
|
|
||||||
# Shows advanced debug information and errors.
|
|
||||||
# CAN EXPOSE OTHER VARIABLES, LEAVE DISABLED
|
|
||||||
APP_DEBUG=false
|
|
||||||
|
|
||||||
# Application key
|
|
||||||
# Used for encryption where needed.
|
|
||||||
# Run `php artisan key:generate` to generate a valid key.
|
|
||||||
APP_KEY=SomeRandomString
|
|
||||||
|
|
||||||
# Application URL
|
|
||||||
# This must be the root URL that you want to host BookStack on.
|
|
||||||
# All URL's in BookStack will be generated using this value.
|
|
||||||
APP_URL=https://example.com
|
|
||||||
|
|
||||||
# Application default language
|
|
||||||
# The default language choice to show.
|
|
||||||
# May be overridden by user-preference or visitor browser settings.
|
|
||||||
APP_LANG=en
|
|
||||||
|
|
||||||
# Auto-detect language for public visitors.
|
|
||||||
# Uses browser-sent headers to infer a language.
|
|
||||||
# APP_LANG will be used if such a header is not provided.
|
|
||||||
APP_AUTO_LANG_PUBLIC=true
|
|
||||||
|
|
||||||
# Application timezone
|
|
||||||
# Used where dates are displayed such as on exported content.
|
|
||||||
# Valid timezone values can be found here: https://www.php.net/manual/en/timezones.php
|
|
||||||
APP_TIMEZONE=UTC
|
|
||||||
|
|
||||||
# Database details
|
|
||||||
# Host can contain a port (localhost:3306) or a separate DB_PORT option can be used.
|
|
||||||
DB_HOST=localhost
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=database_database
|
|
||||||
DB_USERNAME=database_username
|
|
||||||
DB_PASSWORD=database_user_password
|
|
||||||
|
|
||||||
# Mail system to use
|
|
||||||
# Can be 'smtp', 'mail' or 'sendmail'
|
|
||||||
MAIL_DRIVER=smtp
|
|
||||||
|
|
||||||
# Mail sending options
|
|
||||||
MAIL_FROM=mail@bookstackapp.com
|
|
||||||
MAIL_FROM_NAME=BookStack
|
|
||||||
|
|
||||||
# SMTP mail options
|
|
||||||
MAIL_HOST=localhost
|
|
||||||
MAIL_PORT=1025
|
|
||||||
MAIL_USERNAME=null
|
|
||||||
MAIL_PASSWORD=null
|
|
||||||
MAIL_ENCRYPTION=null
|
|
||||||
|
|
||||||
# Cache & Session driver to use
|
|
||||||
# Can be 'file', 'database', 'memcached' or 'redis'
|
|
||||||
CACHE_DRIVER=file
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
|
|
||||||
# Session configuration
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
SESSION_COOKIE_NAME=bookstack_session
|
|
||||||
SESSION_SECURE_COOKIE=false
|
|
||||||
|
|
||||||
# Cache key prefix
|
|
||||||
# Can be used to prevent conflicts multiple BookStack instances use the same store.
|
|
||||||
CACHE_PREFIX=bookstack
|
|
||||||
|
|
||||||
# Memcached server configuration
|
|
||||||
# If using a UNIX socket path for the host, set the port to 0
|
|
||||||
# This follows the following format: HOST:PORT:WEIGHT
|
|
||||||
# For multiple servers separate with a comma
|
|
||||||
MEMCACHED_SERVERS=127.0.0.1:11211:100
|
|
||||||
|
|
||||||
# Redis server configuration
|
|
||||||
# This follows the following format: HOST:PORT:DATABASE
|
|
||||||
# or, if using a password: HOST:PORT:DATABASE:PASSWORD
|
|
||||||
# For multiple servers separate with a comma. These will be clustered.
|
|
||||||
REDIS_SERVERS=127.0.0.1:6379:0
|
|
||||||
|
|
||||||
# Queue driver to use
|
|
||||||
# Queue not really currently used but may be configurable in the future.
|
|
||||||
# Would advise not to change this for now.
|
|
||||||
QUEUE_DRIVER=sync
|
|
||||||
|
|
||||||
# Storage system to use
|
|
||||||
# Can be 'local', 'local_secure' or 's3'
|
|
||||||
STORAGE_TYPE=local
|
|
||||||
|
|
||||||
# Image storage system to use
|
|
||||||
# Defaults to the value of STORAGE_TYPE if unset.
|
|
||||||
# Accepts the same values as STORAGE_TYPE.
|
|
||||||
STORAGE_IMAGE_TYPE=local
|
|
||||||
|
|
||||||
# Attachment storage system to use
|
|
||||||
# Defaults to the value of STORAGE_TYPE if unset.
|
|
||||||
# Accepts the same values as STORAGE_TYPE although 'local' will be forced to 'local_secure'.
|
|
||||||
STORAGE_ATTACHMENT_TYPE=local_secure
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
84
.github/CODE_OF_CONDUCT.md
vendored
84
.github/CODE_OF_CONDUCT.md
vendored
@@ -1,84 +0,0 @@
|
|||||||
# Contributor Covenant Code of Conduct
|
|
||||||
|
|
||||||
## Our Pledge
|
|
||||||
|
|
||||||
In the interest of fostering an open and welcoming environment, we as
|
|
||||||
contributors and maintainers pledge to making participation in our project and
|
|
||||||
our community a harassment-free experience for everyone, regardless of age, body
|
|
||||||
size, disability, ethnicity, gender identity and expression, level of experience,
|
|
||||||
education, socio-economic status, nationality, personal appearance, race,
|
|
||||||
religion, or sexual identity and orientation.
|
|
||||||
|
|
||||||
## Our Standards
|
|
||||||
|
|
||||||
Examples of behavior that contributes to creating a positive environment
|
|
||||||
include:
|
|
||||||
|
|
||||||
* Being respectful of differing viewpoints and experiences
|
|
||||||
* Gracefully accepting constructive criticism
|
|
||||||
* Focusing on what is best for the community
|
|
||||||
* Showing empathy towards other community members
|
|
||||||
|
|
||||||
Examples of unacceptable behavior by participants include:
|
|
||||||
|
|
||||||
* The use of sexualized language or imagery and unwelcome sexual attention or
|
|
||||||
advances
|
|
||||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
|
||||||
* Public or private harassment
|
|
||||||
* Publishing others' private information, such as a physical or electronic
|
|
||||||
address, without explicit permission
|
|
||||||
* Other conduct which could reasonably be considered inappropriate in a
|
|
||||||
professional setting
|
|
||||||
|
|
||||||
### Project Maintainer Standards
|
|
||||||
|
|
||||||
Project maintainers should generally follow these additional standards:
|
|
||||||
|
|
||||||
* Avoid using a negative or harsh tone in communication, Even if the other party
|
|
||||||
is being negative themselves.
|
|
||||||
* When providing criticism, try to make it constructive to lead the other person
|
|
||||||
down the correct path.
|
|
||||||
* Keep the [project definition](https://github.com/BookStackApp/BookStack#project-definition)
|
|
||||||
in mind when deciding what's in scope of the Project.
|
|
||||||
|
|
||||||
## Our Responsibilities
|
|
||||||
|
|
||||||
Project maintainers are responsible for clarifying the standards of acceptable
|
|
||||||
behavior and are expected to take appropriate and fair corrective action in
|
|
||||||
response to any instances of unacceptable behavior. In addition, Project
|
|
||||||
maintainers are responsible for following the standards themselves.
|
|
||||||
|
|
||||||
Project maintainers have the right and responsibility to remove, edit, or
|
|
||||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
|
||||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
|
||||||
permanently any contributor for other behaviors that they deem inappropriate,
|
|
||||||
threatening, offensive, or harmful.
|
|
||||||
|
|
||||||
## Scope
|
|
||||||
|
|
||||||
This Code of Conduct applies both within project spaces and in public spaces
|
|
||||||
when an individual is representing the project or its community. Examples of
|
|
||||||
representing a project or community include using an official project e-mail
|
|
||||||
address, posting via an official social media account, or acting as an appointed
|
|
||||||
representative at an online or offline event. Representation of a project may be
|
|
||||||
further defined and clarified by project maintainers.
|
|
||||||
|
|
||||||
## Enforcement
|
|
||||||
|
|
||||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
|
||||||
reported by contacting the project team at the email address shown on [the profile here](https://github.com/ssddanbrown). All
|
|
||||||
complaints will be reviewed and investigated and will result in a response that
|
|
||||||
is deemed necessary and appropriate to the circumstances. The project team is
|
|
||||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
|
||||||
Further details of specific enforcement policies may be posted separately.
|
|
||||||
|
|
||||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
|
||||||
faith may face temporary or permanent repercussions as determined by other
|
|
||||||
members of the project's leadership.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
|
|
||||||
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
|
|
||||||
|
|
||||||
[homepage]: https://www.contributor-covenant.org
|
|
||||||
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
13
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
### For Feature Requests
|
||||||
|
|
||||||
|
Desired Feature:
|
||||||
|
|
||||||
|
### For Bug Reports
|
||||||
|
|
||||||
|
* BookStack Version:
|
||||||
|
* PHP Version:
|
||||||
|
* MySQL Version:
|
||||||
|
|
||||||
|
##### Expected Behavior
|
||||||
|
|
||||||
|
##### Actual Behavior
|
||||||
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
29
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,29 +0,0 @@
|
|||||||
---
|
|
||||||
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
14
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,14 +0,0 @@
|
|||||||
---
|
|
||||||
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.
|
|
||||||
12
.gitignore
vendored
12
.gitignore
vendored
@@ -2,13 +2,11 @@
|
|||||||
/node_modules
|
/node_modules
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
.env
|
.env
|
||||||
.idea
|
|
||||||
npm-debug.log
|
|
||||||
yarn-error.log
|
|
||||||
/public/dist
|
/public/dist
|
||||||
|
.idea
|
||||||
/public/plugins
|
/public/plugins
|
||||||
/public/css
|
/public/css/*.map
|
||||||
/public/js
|
/public/js/*.map
|
||||||
/public/bower
|
/public/bower
|
||||||
/public/build/
|
/public/build/
|
||||||
/storage/images
|
/storage/images
|
||||||
@@ -20,5 +18,5 @@ yarn.lock
|
|||||||
nbproject
|
nbproject
|
||||||
.buildpath
|
.buildpath
|
||||||
.project
|
.project
|
||||||
.settings/
|
.settings/org.eclipse.wst.common.project.facet.core.xml
|
||||||
webpack-stats.json
|
.settings/org.eclipse.php.core.prefs
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ dist: trusty
|
|||||||
sudo: false
|
sudo: false
|
||||||
language: php
|
language: php
|
||||||
php:
|
php:
|
||||||
- 7.0.20
|
- 7.0.7
|
||||||
- 7.1.9
|
|
||||||
|
|
||||||
cache:
|
cache:
|
||||||
directories:
|
directories:
|
||||||
@@ -15,6 +14,7 @@ before_script:
|
|||||||
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
- mysql -u root -e "GRANT ALL ON \`bookstack-test\`.* TO 'bookstack-test'@'localhost';"
|
||||||
- mysql -u root -e "FLUSH PRIVILEGES;"
|
- mysql -u root -e "FLUSH PRIVILEGES;"
|
||||||
- phpenv config-rm xdebug.ini
|
- phpenv config-rm xdebug.ini
|
||||||
|
- composer dump-autoload --no-interaction
|
||||||
- composer install --prefer-dist --no-interaction
|
- composer install --prefer-dist --no-interaction
|
||||||
- php artisan clear-compiled -n
|
- php artisan clear-compiled -n
|
||||||
- php artisan optimize -n
|
- php artisan optimize -n
|
||||||
|
|||||||
3
LICENSE
3
LICENSE
@@ -1,7 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2018 Dan Brown and the BookStack Project contributors
|
Copyright (c) 2016 Dan Brown
|
||||||
https://github.com/BookStackApp/BookStack/graphs/contributors
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Model;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @property string key
|
* @property string key
|
||||||
@@ -19,9 +16,7 @@ class Activity extends Model
|
|||||||
*/
|
*/
|
||||||
public function entity()
|
public function entity()
|
||||||
{
|
{
|
||||||
if ($this->entity_type === '') {
|
if ($this->entity_type === '') $this->entity_type = null;
|
||||||
$this->entity_type = null;
|
|
||||||
}
|
|
||||||
return $this->morphTo('entity');
|
return $this->morphTo('entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,8 +43,8 @@ class Activity extends Model
|
|||||||
* @param $activityB
|
* @param $activityB
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
public function isSimilarTo($activityB)
|
public function isSimilarTo($activityB) {
|
||||||
{
|
|
||||||
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
return [$this->key, $this->entity_type, $this->entity_id] === [$activityB->key, $activityB->entity_type, $activityB->entity_id];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack;
|
|
||||||
|
|
||||||
class Application extends \Illuminate\Foundation\Application
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path to the application configuration files.
|
|
||||||
*
|
|
||||||
* @param string $path Optionally, a path to append to the config path
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function configPath($path = '')
|
|
||||||
{
|
|
||||||
return $this->basePath.DIRECTORY_SEPARATOR.'app'.DIRECTORY_SEPARATOR.'Config'.($path ? DIRECTORY_SEPARATOR.$path : $path);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
<?php namespace BookStack\Uploads;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
use BookStack\Entities\Page;
|
|
||||||
use BookStack\Ownable;
|
|
||||||
|
|
||||||
class Attachment extends Ownable
|
class Attachment extends Ownable
|
||||||
{
|
{
|
||||||
@@ -13,15 +11,13 @@ class Attachment extends Ownable
|
|||||||
*/
|
*/
|
||||||
public function getFileName()
|
public function getFileName()
|
||||||
{
|
{
|
||||||
if (str_contains($this->name, '.')) {
|
if (str_contains($this->name, '.')) return $this->name;
|
||||||
return $this->name;
|
|
||||||
}
|
|
||||||
return $this->name . '.' . $this->extension;
|
return $this->name . '.' . $this->extension;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the page this file was uploaded to.
|
* Get the page this file was uploaded to.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
|
* @return Page
|
||||||
*/
|
*/
|
||||||
public function page()
|
public function page()
|
||||||
{
|
{
|
||||||
@@ -34,9 +30,7 @@ class Attachment extends Ownable
|
|||||||
*/
|
*/
|
||||||
public function getUrl()
|
public function getUrl()
|
||||||
{
|
{
|
||||||
if ($this->external && strpos($this->path, 'http') !== 0) {
|
return baseUrl('/attachments/' . $this->id);
|
||||||
return $this->path;
|
|
||||||
}
|
|
||||||
return url('/attachments/' . $this->id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
|
||||||
use BookStack\Notifications\ConfirmEmail;
|
|
||||||
|
|
||||||
class EmailConfirmationService extends UserTokenService
|
|
||||||
{
|
|
||||||
protected $tokenTable = 'email_confirmations';
|
|
||||||
protected $expiryTime = 24;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new confirmation for a user,
|
|
||||||
* Also removes any existing old ones.
|
|
||||||
* @param User $user
|
|
||||||
* @throws ConfirmationEmailException
|
|
||||||
*/
|
|
||||||
public function sendConfirmation(User $user)
|
|
||||||
{
|
|
||||||
if ($user->email_confirmed) {
|
|
||||||
throw new ConfirmationEmailException(trans('errors.email_already_confirmed'), '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->deleteByUser($user);
|
|
||||||
$token = $this->createTokenForUser($user);
|
|
||||||
|
|
||||||
$user->notify(new ConfirmEmail($token));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if confirmation is required in this instance.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function confirmationRequired() : bool
|
|
||||||
{
|
|
||||||
return setting('registration-confirmation')
|
|
||||||
|| setting('registration-restrict');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
<?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'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
$serverDetails = $this->parseServerString($this->config['server']);
|
|
||||||
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
|
|
||||||
|
|
||||||
if ($ldapConnection === false) {
|
|
||||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set any required options
|
|
||||||
if ($this->config['version']) {
|
|
||||||
$this->ldap->setVersion($ldapConnection, $this->config['version']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->ldapConnection = $ldapConnection;
|
|
||||||
return $this->ldapConnection;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse a LDAP server string and return the host and port for
|
|
||||||
* a connection. Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'
|
|
||||||
* @param $serverString
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
protected function parseServerString($serverString)
|
|
||||||
{
|
|
||||||
$serverNameParts = explode(':', $serverString);
|
|
||||||
|
|
||||||
// If we have a protocol just return the full string since PHP will ignore a separate port.
|
|
||||||
if ($serverNameParts[0] === 'ldaps' || $serverNameParts[0] === 'ldap') {
|
|
||||||
return ['host' => $serverString, 'port' => 389];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, extract the port out
|
|
||||||
$hostName = $serverNameParts[0];
|
|
||||||
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
|
||||||
return ['host' => $hostName, 'port' => $ldapPort];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Build a filter string by injecting common variables.
|
|
||||||
* @param string $filterString
|
|
||||||
* @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,23 +0,0 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Notifications\UserInvite;
|
|
||||||
|
|
||||||
class UserInviteService extends UserTokenService
|
|
||||||
{
|
|
||||||
protected $tokenTable = 'user_invites';
|
|
||||||
protected $expiryTime = 336; // Two weeks
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send an invitation to a user to sign into BookStack
|
|
||||||
* Removes existing invitation tokens.
|
|
||||||
* @param User $user
|
|
||||||
*/
|
|
||||||
public function sendInvitation(User $user)
|
|
||||||
{
|
|
||||||
$this->deleteByUser($user);
|
|
||||||
$token = $this->createTokenForUser($user);
|
|
||||||
$user->notify(new UserInvite($token));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<?php namespace BookStack\Auth\Access;
|
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
|
||||||
use Carbon\Carbon;
|
|
||||||
use Illuminate\Database\Connection as Database;
|
|
||||||
use stdClass;
|
|
||||||
|
|
||||||
class UserTokenService
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Name of table where user tokens are stored.
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $tokenTable = 'user_tokens';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Token expiry time in hours.
|
|
||||||
* @var int
|
|
||||||
*/
|
|
||||||
protected $expiryTime = 24;
|
|
||||||
|
|
||||||
protected $db;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserTokenService constructor.
|
|
||||||
* @param Database $db
|
|
||||||
*/
|
|
||||||
public function __construct(Database $db)
|
|
||||||
{
|
|
||||||
$this->db = $db;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete all email confirmations that belong to a user.
|
|
||||||
* @param User $user
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function deleteByUser(User $user)
|
|
||||||
{
|
|
||||||
return $this->db->table($this->tokenTable)
|
|
||||||
->where('user_id', '=', $user->id)
|
|
||||||
->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the user id from a token, while check the token exists and has not expired.
|
|
||||||
* @param string $token
|
|
||||||
* @return int
|
|
||||||
* @throws UserTokenNotFoundException
|
|
||||||
* @throws UserTokenExpiredException
|
|
||||||
*/
|
|
||||||
public function checkTokenAndGetUserId(string $token) : int
|
|
||||||
{
|
|
||||||
$entry = $this->getEntryByToken($token);
|
|
||||||
|
|
||||||
if (is_null($entry)) {
|
|
||||||
throw new UserTokenNotFoundException('Token "' . $token . '" not found');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->entryExpired($entry)) {
|
|
||||||
throw new UserTokenExpiredException("Token of id {$entry->id} has expired.", $entry->user_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $entry->user_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a unique token within the email confirmation database.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function generateToken() : string
|
|
||||||
{
|
|
||||||
$token = str_random(24);
|
|
||||||
while ($this->tokenExists($token)) {
|
|
||||||
$token = str_random(25);
|
|
||||||
}
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generate and store a token for the given user.
|
|
||||||
* @param User $user
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
protected function createTokenForUser(User $user) : string
|
|
||||||
{
|
|
||||||
$token = $this->generateToken();
|
|
||||||
$this->db->table($this->tokenTable)->insert([
|
|
||||||
'user_id' => $user->id,
|
|
||||||
'token' => $token,
|
|
||||||
'created_at' => Carbon::now(),
|
|
||||||
'updated_at' => Carbon::now()
|
|
||||||
]);
|
|
||||||
return $token;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given token exists.
|
|
||||||
* @param string $token
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function tokenExists(string $token) : bool
|
|
||||||
{
|
|
||||||
return $this->db->table($this->tokenTable)
|
|
||||||
->where('token', '=', $token)->exists();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a token entry for the given token.
|
|
||||||
* @param string $token
|
|
||||||
* @return object|null
|
|
||||||
*/
|
|
||||||
protected function getEntryByToken(string $token)
|
|
||||||
{
|
|
||||||
return $this->db->table($this->tokenTable)
|
|
||||||
->where('token', '=', $token)
|
|
||||||
->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given token entry has expired.
|
|
||||||
* @param stdClass $tokenEntry
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function entryExpired(stdClass $tokenEntry) : bool
|
|
||||||
{
|
|
||||||
return Carbon::now()->subHours($this->expiryTime)
|
|
||||||
->gt(new Carbon($tokenEntry->created_at));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
<?php namespace BookStack\Auth;
|
|
||||||
|
|
||||||
use Activity;
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use BookStack\Exceptions\NotFoundException;
|
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
|
||||||
use BookStack\Uploads\Image;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Images;
|
|
||||||
|
|
||||||
class UserRepo
|
|
||||||
{
|
|
||||||
|
|
||||||
protected $user;
|
|
||||||
protected $role;
|
|
||||||
protected $entityRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserRepo constructor.
|
|
||||||
* @param User $user
|
|
||||||
* @param Role $role
|
|
||||||
* @param EntityRepo $entityRepo
|
|
||||||
*/
|
|
||||||
public function __construct(User $user, Role $role, EntityRepo $entityRepo)
|
|
||||||
{
|
|
||||||
$this->user = $user;
|
|
||||||
$this->role = $role;
|
|
||||||
$this->entityRepo = $entityRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param string $email
|
|
||||||
* @return User|null
|
|
||||||
*/
|
|
||||||
public function getByEmail($email)
|
|
||||||
{
|
|
||||||
return $this->user->where('email', '=', $email)->first();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param int $id
|
|
||||||
* @return User
|
|
||||||
*/
|
|
||||||
public function getById($id)
|
|
||||||
{
|
|
||||||
return $this->user->newQuery()->findOrFail($id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the users with their permissions.
|
|
||||||
* @return Builder|static
|
|
||||||
*/
|
|
||||||
public function getAllUsers()
|
|
||||||
{
|
|
||||||
return $this->user->with('roles', 'avatar')->orderBy('name', 'asc')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the users with their permissions in a paginated format.
|
|
||||||
* @param int $count
|
|
||||||
* @param $sortData
|
|
||||||
* @return Builder|static
|
|
||||||
*/
|
|
||||||
public function getAllUsersPaginatedAndSorted($count, $sortData)
|
|
||||||
{
|
|
||||||
$query = $this->user->with('roles', 'avatar')->orderBy($sortData['sort'], $sortData['order']);
|
|
||||||
|
|
||||||
if ($sortData['search']) {
|
|
||||||
$term = '%' . $sortData['search'] . '%';
|
|
||||||
$query->where(function ($query) use ($term) {
|
|
||||||
$query->where('name', 'like', $term)
|
|
||||||
->orWhere('email', 'like', $term);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query->paginate($count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new user and attaches a role to them.
|
|
||||||
* @param array $data
|
|
||||||
* @param boolean $verifyEmail
|
|
||||||
* @return \BookStack\Auth\User
|
|
||||||
*/
|
|
||||||
public function registerNew(array $data, $verifyEmail = false)
|
|
||||||
{
|
|
||||||
$user = $this->create($data, $verifyEmail);
|
|
||||||
$this->attachDefaultRole($user);
|
|
||||||
$this->downloadAndAssignUserAvatar($user);
|
|
||||||
|
|
||||||
return $user;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Give a user the default role. Used when creating a new user.
|
|
||||||
* @param User $user
|
|
||||||
*/
|
|
||||||
public function attachDefaultRole(User $user)
|
|
||||||
{
|
|
||||||
$roleId = setting('registration-role');
|
|
||||||
if ($roleId !== false && $user->roles()->where('id', '=', $roleId)->count() === 0) {
|
|
||||||
$user->attachRoleId($roleId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Assign a user to a system-level role.
|
|
||||||
* @param User $user
|
|
||||||
* @param $systemRoleName
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function attachSystemRole(User $user, $systemRoleName)
|
|
||||||
{
|
|
||||||
$role = $this->role->newQuery()->where('system_name', '=', $systemRoleName)->first();
|
|
||||||
if ($role === null) {
|
|
||||||
throw new NotFoundException("Role '{$systemRoleName}' not found");
|
|
||||||
}
|
|
||||||
$user->attachRole($role);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the give user is the only admin.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function isOnlyAdmin(User $user)
|
|
||||||
{
|
|
||||||
if (!$user->hasSystemRole('admin')) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$adminRole = $this->role->getSystemRole('admin');
|
|
||||||
if ($adminRole->users->count() > 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
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
|
|
||||||
* @param boolean $verifyEmail
|
|
||||||
* @return \BookStack\Auth\User
|
|
||||||
*/
|
|
||||||
public function create(array $data, $verifyEmail = false)
|
|
||||||
{
|
|
||||||
return $this->user->forceCreate([
|
|
||||||
'name' => $data['name'],
|
|
||||||
'email' => $data['email'],
|
|
||||||
'password' => bcrypt($data['password']),
|
|
||||||
'email_confirmed' => $verifyEmail
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the given user from storage, Delete all related content.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function destroy(User $user)
|
|
||||||
{
|
|
||||||
$user->socialAccounts()->delete();
|
|
||||||
$user->delete();
|
|
||||||
|
|
||||||
// Delete user profile images
|
|
||||||
$profileImages = Image::where('type', '=', 'user')->where('uploaded_to', '=', $user->id)->get();
|
|
||||||
foreach ($profileImages as $image) {
|
|
||||||
Images::destroy($image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the latest activity for a user.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getActivity(User $user, $count = 20, $page = 0)
|
|
||||||
{
|
|
||||||
return Activity::userActivity($user, $count, $page);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the recently created content for this given user.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @param int $count
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getRecentlyCreated(User $user, $count = 20)
|
|
||||||
{
|
|
||||||
$createdByUserQuery = function (Builder $query) use ($user) {
|
|
||||||
$query->where('created_by', '=', $user->id);
|
|
||||||
};
|
|
||||||
|
|
||||||
return [
|
|
||||||
'pages' => $this->entityRepo->getRecentlyCreated('page', $count, 0, $createdByUserQuery),
|
|
||||||
'chapters' => $this->entityRepo->getRecentlyCreated('chapter', $count, 0, $createdByUserQuery),
|
|
||||||
'books' => $this->entityRepo->getRecentlyCreated('book', $count, 0, $createdByUserQuery),
|
|
||||||
'shelves' => $this->entityRepo->getRecentlyCreated('bookshelf', $count, 0, $createdByUserQuery)
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get asset created counts for the give user.
|
|
||||||
* @param \BookStack\Auth\User $user
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getAssetCounts(User $user)
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'pages' => $this->entityRepo->getUserTotalCreated('page', $user),
|
|
||||||
'chapters' => $this->entityRepo->getUserTotalCreated('chapter', $user),
|
|
||||||
'books' => $this->entityRepo->getUserTotalCreated('book', $user),
|
|
||||||
'shelves' => $this->entityRepo->getUserTotalCreated('bookshelf', $user),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the roles in the system that are assignable to a user.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getAllRoles()
|
|
||||||
{
|
|
||||||
return $this->role->newQuery()->orderBy('name', 'asc')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all the roles which can be given restricted access to
|
|
||||||
* other entities in the system.
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getRestrictableRoles()
|
|
||||||
{
|
|
||||||
return $this->role->where('system_name', '!=', 'admin')->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 downloadAndAssignUserAvatar(User $user)
|
|
||||||
{
|
|
||||||
if (!Images::avatarFetchEnabled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$avatar = Images::saveUserAvatar($user);
|
|
||||||
$user->avatar()->associate($avatar);
|
|
||||||
$user->save();
|
|
||||||
return true;
|
|
||||||
} catch (Exception $e) {
|
|
||||||
\Log::error('Failed to save user avatar image');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
68
app/Book.php
Normal file
68
app/Book.php
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
class Book extends Entity
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $fillable = ['name', 'description'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the url for this book.
|
||||||
|
* @param string|bool $path
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getUrl($path = false)
|
||||||
|
{
|
||||||
|
if ($path !== false) {
|
||||||
|
return baseUrl('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||||
|
}
|
||||||
|
return baseUrl('/books/' . urlencode($this->slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Get the edit url for this book.
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getEditUrl()
|
||||||
|
{
|
||||||
|
return $this->getUrl() . '/edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all pages within this book.
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function pages()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Page::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all chapters within this book.
|
||||||
|
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
||||||
|
*/
|
||||||
|
public function chapters()
|
||||||
|
{
|
||||||
|
return $this->hasMany(Chapter::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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\\\\Book' 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,19 +1,11 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
|
||||||
class Chapter extends Entity
|
class Chapter extends Entity
|
||||||
{
|
{
|
||||||
public $searchFactor = 1.3;
|
|
||||||
|
|
||||||
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
protected $fillable = ['name', 'description', 'priority', 'book_id'];
|
||||||
|
|
||||||
/**
|
protected $with = ['book'];
|
||||||
* Get the morph class for this model.
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getMorphClass()
|
|
||||||
{
|
|
||||||
return 'BookStack\\Chapter';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the book this chapter is within.
|
* Get the book this chapter is within.
|
||||||
@@ -42,13 +34,10 @@ class Chapter extends Entity
|
|||||||
public function getUrl($path = false)
|
public function getUrl($path = false)
|
||||||
{
|
{
|
||||||
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
$bookSlug = $this->getAttribute('bookSlug') ? $this->getAttribute('bookSlug') : $this->book->slug;
|
||||||
$fullPath = '/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug);
|
|
||||||
|
|
||||||
if ($path !== false) {
|
if ($path !== false) {
|
||||||
$fullPath .= '/' . trim($path, '/');
|
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
||||||
}
|
}
|
||||||
|
return baseUrl('/books/' . urlencode($bookSlug) . '/chapter/' . urlencode($this->slug));
|
||||||
return url($fullPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -56,10 +45,10 @@ class Chapter extends Entity
|
|||||||
* @param int $length
|
* @param int $length
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getExcerpt(int $length = 100)
|
public function getExcerpt($length = 100)
|
||||||
{
|
{
|
||||||
$description = $this->text ?? $this->description;
|
$description = $this->description;
|
||||||
return mb_strlen($description) > $length ? mb_substr($description, 0, $length-3) . '...' : $description;
|
return strlen($description) > $length ? substr($description, 0, $length-3) . '...' : $description;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,12 +60,4 @@ class Chapter extends Entity
|
|||||||
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
return "'BookStack\\\\Chapter' as entity_type, id, id as entity_id, slug, name, {$this->textField} as text, '' as html, book_id, priority, '0' as chapter_id, '0' as draft, created_by, updated_by, updated_at, created_at";
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this chapter has any child pages.
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function hasChildren()
|
|
||||||
{
|
|
||||||
return count($this->pages) > 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Actions;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
use BookStack\Ownable;
|
|
||||||
|
|
||||||
class Comment extends Ownable
|
class Comment extends Ownable
|
||||||
{
|
{
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Authentication configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Method of authentication to use
|
|
||||||
// Options: standard, ldap
|
|
||||||
'method' => env('AUTH_METHOD', 'standard'),
|
|
||||||
|
|
||||||
// Authentication Defaults
|
|
||||||
// This option controls the default authentication "guard" and password
|
|
||||||
// reset options for your application.
|
|
||||||
'defaults' => [
|
|
||||||
'guard' => 'web',
|
|
||||||
'passwords' => 'users',
|
|
||||||
],
|
|
||||||
|
|
||||||
// Authentication Guards
|
|
||||||
// All authentication drivers have a user provider. This defines how the
|
|
||||||
// users are actually retrieved out of your database or other storage
|
|
||||||
// mechanisms used by this application to persist your user's data.
|
|
||||||
// Supported: "session", "token"
|
|
||||||
'guards' => [
|
|
||||||
'web' => [
|
|
||||||
'driver' => 'session',
|
|
||||||
'provider' => 'users',
|
|
||||||
],
|
|
||||||
|
|
||||||
'api' => [
|
|
||||||
'driver' => 'token',
|
|
||||||
'provider' => 'users',
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
// User Providers
|
|
||||||
// All authentication drivers have a user provider. This defines how the
|
|
||||||
// users are actually retrieved out of your database or other storage
|
|
||||||
// mechanisms used by this application to persist your user's data.
|
|
||||||
// Supported: database, eloquent, ldap
|
|
||||||
'providers' => [
|
|
||||||
'users' => [
|
|
||||||
'driver' => env('AUTH_METHOD', 'standard') === 'standard' ? 'eloquent' : env('AUTH_METHOD'),
|
|
||||||
'model' => \BookStack\Auth\User::class,
|
|
||||||
],
|
|
||||||
|
|
||||||
// 'users' => [
|
|
||||||
// 'driver' => 'database',
|
|
||||||
// 'table' => 'users',
|
|
||||||
// ],
|
|
||||||
],
|
|
||||||
|
|
||||||
// Resetting Passwords
|
|
||||||
// The expire time is the number of minutes that the reset token should be
|
|
||||||
// considered valid. This security feature keeps tokens short-lived so
|
|
||||||
// they have less time to be guessed. You may change this as needed.
|
|
||||||
'passwords' => [
|
|
||||||
'users' => [
|
|
||||||
'provider' => 'users',
|
|
||||||
'email' => 'emails.password',
|
|
||||||
'table' => 'password_resets',
|
|
||||||
'expire' => 60,
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Broadcasting configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Default Broadcaster
|
|
||||||
// This option controls the default broadcaster that will be used by the
|
|
||||||
// framework when an event needs to be broadcast. This can be set to
|
|
||||||
// any of the connections defined in the "connections" array below.
|
|
||||||
'default' => env('BROADCAST_DRIVER', 'pusher'),
|
|
||||||
|
|
||||||
// Broadcast Connections
|
|
||||||
// Here you may define all of the broadcast connections that will be used
|
|
||||||
// to broadcast events to other systems or over websockets. Samples of
|
|
||||||
// each available type of connection are provided inside this array.
|
|
||||||
'connections' => [
|
|
||||||
|
|
||||||
'pusher' => [
|
|
||||||
'driver' => 'pusher',
|
|
||||||
'key' => env('PUSHER_KEY'),
|
|
||||||
'secret' => env('PUSHER_SECRET'),
|
|
||||||
'app_id' => env('PUSHER_APP_ID'),
|
|
||||||
],
|
|
||||||
|
|
||||||
'redis' => [
|
|
||||||
'driver' => 'redis',
|
|
||||||
'connection' => 'default',
|
|
||||||
],
|
|
||||||
|
|
||||||
'log' => [
|
|
||||||
'driver' => 'log',
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,127 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// REDIS
|
|
||||||
// Split out configuration into an array
|
|
||||||
if (env('REDIS_SERVERS', false)) {
|
|
||||||
|
|
||||||
$redisDefaults = ['host' => '127.0.0.1', 'port' => '6379', 'database' => '0', 'password' => null];
|
|
||||||
$redisServers = explode(',', trim(env('REDIS_SERVERS', '127.0.0.1:6379:0'), ','));
|
|
||||||
$redisConfig = [];
|
|
||||||
$cluster = count($redisServers) > 1;
|
|
||||||
|
|
||||||
if ($cluster) {
|
|
||||||
$redisConfig['clusters'] = ['default' => []];
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($redisServers as $index => $redisServer) {
|
|
||||||
$redisServerDetails = explode(':', $redisServer);
|
|
||||||
|
|
||||||
$serverConfig = [];
|
|
||||||
$configIndex = 0;
|
|
||||||
foreach ($redisDefaults as $configKey => $configDefault) {
|
|
||||||
$serverConfig[$configKey] = ($redisServerDetails[$configIndex] ?? $configDefault);
|
|
||||||
$configIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($cluster) {
|
|
||||||
$redisConfig['clusters']['default'][] = $serverConfig;
|
|
||||||
} else {
|
|
||||||
$redisConfig['default'] = $serverConfig;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MYSQL
|
|
||||||
// Split out port from host if set
|
|
||||||
$mysql_host = env('DB_HOST', 'localhost');
|
|
||||||
$mysql_host_exploded = explode(':', $mysql_host);
|
|
||||||
$mysql_port = env('DB_PORT', 3306);
|
|
||||||
if (count($mysql_host_exploded) > 1) {
|
|
||||||
$mysql_host = $mysql_host_exploded[0];
|
|
||||||
$mysql_port = intval($mysql_host_exploded[1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Default database connection name.
|
|
||||||
// Options: mysql, mysql_testing
|
|
||||||
'default' => env('DB_CONNECTION', 'mysql'),
|
|
||||||
|
|
||||||
// Available database connections
|
|
||||||
// Many of those shown here are unsupported by BookStack.
|
|
||||||
'connections' => [
|
|
||||||
|
|
||||||
'sqlite' => [
|
|
||||||
'driver' => 'sqlite',
|
|
||||||
'database' => storage_path('database.sqlite'),
|
|
||||||
'prefix' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
'mysql' => [
|
|
||||||
'driver' => 'mysql',
|
|
||||||
'host' => $mysql_host,
|
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
|
||||||
'username' => env('DB_USERNAME', 'forge'),
|
|
||||||
'password' => env('DB_PASSWORD', ''),
|
|
||||||
'unix_socket' => env('DB_SOCKET', ''),
|
|
||||||
'port' => $mysql_port,
|
|
||||||
'charset' => 'utf8mb4',
|
|
||||||
'collation' => 'utf8mb4_unicode_ci',
|
|
||||||
'prefix' => '',
|
|
||||||
'strict' => false,
|
|
||||||
'engine' => null,
|
|
||||||
],
|
|
||||||
|
|
||||||
'mysql_testing' => [
|
|
||||||
'driver' => 'mysql',
|
|
||||||
'host' => '127.0.0.1',
|
|
||||||
'database' => 'bookstack-test',
|
|
||||||
'username' => env('MYSQL_USER', 'bookstack-test'),
|
|
||||||
'password' => env('MYSQL_PASSWORD', 'bookstack-test'),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'collation' => 'utf8_unicode_ci',
|
|
||||||
'prefix' => '',
|
|
||||||
'strict' => false,
|
|
||||||
],
|
|
||||||
|
|
||||||
'pgsql' => [
|
|
||||||
'driver' => 'pgsql',
|
|
||||||
'host' => env('DB_HOST', 'localhost'),
|
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
|
||||||
'username' => env('DB_USERNAME', 'forge'),
|
|
||||||
'password' => env('DB_PASSWORD', ''),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'prefix' => '',
|
|
||||||
'schema' => 'public',
|
|
||||||
],
|
|
||||||
|
|
||||||
'sqlsrv' => [
|
|
||||||
'driver' => 'sqlsrv',
|
|
||||||
'host' => env('DB_HOST', 'localhost'),
|
|
||||||
'database' => env('DB_DATABASE', 'forge'),
|
|
||||||
'username' => env('DB_USERNAME', 'forge'),
|
|
||||||
'password' => env('DB_PASSWORD', ''),
|
|
||||||
'charset' => 'utf8',
|
|
||||||
'prefix' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
// Migration Repository Table
|
|
||||||
// This table keeps track of all the migrations that have already run for
|
|
||||||
// your application. Using this information, we can determine which of
|
|
||||||
// the migrations on disk haven't actually been run in the database.
|
|
||||||
'migrations' => 'migrations',
|
|
||||||
|
|
||||||
// Redis configuration to use if set
|
|
||||||
'redis' => env('REDIS_SERVERS', false) ? $redisConfig : [],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Debugbar Configuration Options
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Debugbar is enabled by default, when debug is set to true in app.php.
|
|
||||||
// You can override the value by setting enable to true or false instead of null.
|
|
||||||
//
|
|
||||||
// You can provide an array of URI's that must be ignored (eg. 'api/*')
|
|
||||||
'enabled' => env('DEBUGBAR_ENABLED', false),
|
|
||||||
'except' => [
|
|
||||||
'telescope*'
|
|
||||||
],
|
|
||||||
|
|
||||||
|
|
||||||
// DebugBar stores data for session/ajax requests.
|
|
||||||
// You can disable this, so the debugbar stores data in headers/session,
|
|
||||||
// but this can cause problems with large data collectors.
|
|
||||||
// By default, file storage (in the storage folder) is used. Redis and PDO
|
|
||||||
// can also be used. For PDO, run the package migrations first.
|
|
||||||
'storage' => [
|
|
||||||
'enabled' => true,
|
|
||||||
'driver' => 'file', // redis, file, pdo, custom
|
|
||||||
'path' => storage_path('debugbar'), // For file driver
|
|
||||||
'connection' => null, // Leave null for default connection (Redis/PDO)
|
|
||||||
'provider' => '' // Instance of StorageInterface for custom driver
|
|
||||||
],
|
|
||||||
|
|
||||||
// Vendor files are included by default, but can be set to false.
|
|
||||||
// This can also be set to 'js' or 'css', to only include javascript or css vendor files.
|
|
||||||
// Vendor files are for css: font-awesome (including fonts) and highlight.js (css files)
|
|
||||||
// and for js: jquery and and highlight.js
|
|
||||||
// So if you want syntax highlighting, set it to true.
|
|
||||||
// jQuery is set to not conflict with existing jQuery scripts.
|
|
||||||
'include_vendors' => true,
|
|
||||||
|
|
||||||
// The Debugbar can capture Ajax requests and display them. If you don't want this (ie. because of errors),
|
|
||||||
// you can use this option to disable sending the data through the headers.
|
|
||||||
// Optionally, you can also send ServerTiming headers on ajax requests for the Chrome DevTools.
|
|
||||||
|
|
||||||
'capture_ajax' => true,
|
|
||||||
'add_ajax_timing' => false,
|
|
||||||
|
|
||||||
// When enabled, the Debugbar shows deprecated warnings for Symfony components
|
|
||||||
// in the Messages tab.
|
|
||||||
'error_handler' => false,
|
|
||||||
|
|
||||||
// The Debugbar can emulate the Clockwork headers, so you can use the Chrome
|
|
||||||
// Extension, without the server-side code. It uses Debugbar collectors instead.
|
|
||||||
'clockwork' => false,
|
|
||||||
|
|
||||||
// Enable/disable DataCollectors
|
|
||||||
'collectors' => [
|
|
||||||
'phpinfo' => true, // Php version
|
|
||||||
'messages' => true, // Messages
|
|
||||||
'time' => true, // Time Datalogger
|
|
||||||
'memory' => true, // Memory usage
|
|
||||||
'exceptions' => true, // Exception displayer
|
|
||||||
'log' => true, // Logs from Monolog (merged in messages if enabled)
|
|
||||||
'db' => true, // Show database (PDO) queries and bindings
|
|
||||||
'views' => true, // Views with their data
|
|
||||||
'route' => true, // Current route information
|
|
||||||
'auth' => true, // Display Laravel authentication status
|
|
||||||
'gate' => true, // Display Laravel Gate checks
|
|
||||||
'session' => true, // Display session data
|
|
||||||
'symfony_request' => true, // Only one can be enabled..
|
|
||||||
'mail' => true, // Catch mail messages
|
|
||||||
'laravel' => false, // Laravel version and environment
|
|
||||||
'events' => false, // All events fired
|
|
||||||
'default_request' => false, // Regular or special Symfony request logger
|
|
||||||
'logs' => false, // Add the latest log messages
|
|
||||||
'files' => false, // Show the included files
|
|
||||||
'config' => false, // Display config settings
|
|
||||||
'cache' => false, // Display cache events
|
|
||||||
],
|
|
||||||
|
|
||||||
// Configure some DataCollectors
|
|
||||||
'options' => [
|
|
||||||
'auth' => [
|
|
||||||
'show_name' => true, // Also show the users name/email in the debugbar
|
|
||||||
],
|
|
||||||
'db' => [
|
|
||||||
'with_params' => true, // Render SQL with the parameters substituted
|
|
||||||
'backtrace' => true, // Use a backtrace to find the origin of the query in your files.
|
|
||||||
'timeline' => false, // Add the queries to the timeline
|
|
||||||
'explain' => [ // Show EXPLAIN output on queries
|
|
||||||
'enabled' => false,
|
|
||||||
'types' => ['SELECT'], // ['SELECT', 'INSERT', 'UPDATE', 'DELETE']; for MySQL 5.6.3+
|
|
||||||
],
|
|
||||||
'hints' => true, // Show hints for common mistakes
|
|
||||||
],
|
|
||||||
'mail' => [
|
|
||||||
'full_log' => false
|
|
||||||
],
|
|
||||||
'views' => [
|
|
||||||
'data' => false, //Note: Can slow down the application, because the data can be quite large..
|
|
||||||
],
|
|
||||||
'route' => [
|
|
||||||
'label' => true // show complete route on bar
|
|
||||||
],
|
|
||||||
'logs' => [
|
|
||||||
'file' => null
|
|
||||||
],
|
|
||||||
'cache' => [
|
|
||||||
'values' => true // collect cache values
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
// Inject Debugbar into the response
|
|
||||||
// Usually, the debugbar is added just before </body>, by listening to the
|
|
||||||
// Response after the App is done. If you disable this, you have to add them
|
|
||||||
// in your template yourself. See http://phpdebugbar.com/docs/rendering.html
|
|
||||||
'inject' => true,
|
|
||||||
|
|
||||||
// DebugBar route prefix
|
|
||||||
// Sometimes you want to set route prefix to be used by DebugBar to load
|
|
||||||
// its resources from. Usually the need comes from misconfigured web server or
|
|
||||||
// from trying to overcome bugs like this: http://trac.nginx.org/nginx/ticket/97
|
|
||||||
'route_prefix' => '_debugbar',
|
|
||||||
|
|
||||||
// DebugBar route domain
|
|
||||||
// By default DebugBar route served from the same domain that request served.
|
|
||||||
// To override default domain, specify it as a non-empty value.
|
|
||||||
'route_domain' => env('APP_URL', '') === 'http://bookstack.dev' ? '' : env('APP_URL', ''),
|
|
||||||
];
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filesystem configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Default Filesystem Disk
|
|
||||||
// Options: local, local_secure, s3
|
|
||||||
'default' => env('STORAGE_TYPE', 'local'),
|
|
||||||
|
|
||||||
// Filesystem to use specifically for image uploads.
|
|
||||||
'images' => env('STORAGE_IMAGE_TYPE', env('STORAGE_TYPE', 'local')),
|
|
||||||
|
|
||||||
// Filesystem to use specifically for file attachments.
|
|
||||||
'attachments' => env('STORAGE_ATTACHMENT_TYPE', env('STORAGE_TYPE', 'local')),
|
|
||||||
|
|
||||||
// Storage URL
|
|
||||||
// This is the url to where the storage is located for when using an external
|
|
||||||
// file storage service, such as s3, to store publicly accessible assets.
|
|
||||||
'url' => env('STORAGE_URL', false),
|
|
||||||
|
|
||||||
// Default Cloud Filesystem Disk
|
|
||||||
'cloud' => 's3',
|
|
||||||
|
|
||||||
// Available filesystem disks
|
|
||||||
// Only local, local_secure & s3 are supported by BookStack
|
|
||||||
'disks' => [
|
|
||||||
|
|
||||||
'local' => [
|
|
||||||
'driver' => 'local',
|
|
||||||
'root' => public_path(),
|
|
||||||
],
|
|
||||||
|
|
||||||
'local_secure' => [
|
|
||||||
'driver' => 'local',
|
|
||||||
'root' => storage_path(),
|
|
||||||
],
|
|
||||||
|
|
||||||
'ftp' => [
|
|
||||||
'driver' => 'ftp',
|
|
||||||
'host' => 'ftp.example.com',
|
|
||||||
'username' => 'your-username',
|
|
||||||
'password' => 'your-password',
|
|
||||||
],
|
|
||||||
|
|
||||||
's3' => [
|
|
||||||
'driver' => 's3',
|
|
||||||
'key' => env('STORAGE_S3_KEY', 'your-key'),
|
|
||||||
'secret' => env('STORAGE_S3_SECRET', 'your-secret'),
|
|
||||||
'region' => env('STORAGE_S3_REGION', 'your-region'),
|
|
||||||
'bucket' => env('STORAGE_S3_BUCKET', 'your-bucket'),
|
|
||||||
'endpoint' => env('STORAGE_S3_ENDPOINT', null),
|
|
||||||
'use_path_style_endpoint' => env('STORAGE_S3_ENDPOINT', null) !== null,
|
|
||||||
],
|
|
||||||
|
|
||||||
'rackspace' => [
|
|
||||||
'driver' => 'rackspace',
|
|
||||||
'username' => 'your-username',
|
|
||||||
'key' => 'your-key',
|
|
||||||
'container' => 'your-container',
|
|
||||||
'endpoint' => 'https://identity.api.rackspacecloud.com/v2.0/',
|
|
||||||
'region' => 'IAD',
|
|
||||||
'url_type' => 'publicURL',
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mail configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Mail driver to use.
|
|
||||||
// Options: smtp, mail, sendmail, log
|
|
||||||
'driver' => env('MAIL_DRIVER', 'smtp'),
|
|
||||||
|
|
||||||
// SMTP host address
|
|
||||||
'host' => env('MAIL_HOST', 'smtp.mailgun.org'),
|
|
||||||
|
|
||||||
// SMTP host port
|
|
||||||
'port' => env('MAIL_PORT', 587),
|
|
||||||
|
|
||||||
// Global "From" address & name
|
|
||||||
'from' => [
|
|
||||||
'address' => env('MAIL_FROM', 'mail@bookstackapp.com'),
|
|
||||||
'name' => env('MAIL_FROM_NAME','BookStack')
|
|
||||||
],
|
|
||||||
|
|
||||||
// Email encryption protocol
|
|
||||||
'encryption' => env('MAIL_ENCRYPTION', 'tls'),
|
|
||||||
|
|
||||||
// SMTP server username
|
|
||||||
'username' => env('MAIL_USERNAME'),
|
|
||||||
|
|
||||||
// SMTP server password
|
|
||||||
'password' => env('MAIL_PASSWORD'),
|
|
||||||
|
|
||||||
// Sendmail application path
|
|
||||||
'sendmail' => '/usr/sbin/sendmail -bs',
|
|
||||||
|
|
||||||
// Email markdown configuration
|
|
||||||
'markdown' => [
|
|
||||||
'theme' => 'default',
|
|
||||||
'paths' => [
|
|
||||||
resource_path('views/vendor/mail'),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Queue configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Default driver to use for the queue
|
|
||||||
// Options: null, sync, redis
|
|
||||||
'default' => env('QUEUE_DRIVER', 'sync'),
|
|
||||||
|
|
||||||
// Queue connection configuration
|
|
||||||
'connections' => [
|
|
||||||
|
|
||||||
'sync' => [
|
|
||||||
'driver' => 'sync',
|
|
||||||
],
|
|
||||||
|
|
||||||
'database' => [
|
|
||||||
'driver' => 'database',
|
|
||||||
'table' => 'jobs',
|
|
||||||
'queue' => 'default',
|
|
||||||
'expire' => 60,
|
|
||||||
],
|
|
||||||
|
|
||||||
'beanstalkd' => [
|
|
||||||
'driver' => 'beanstalkd',
|
|
||||||
'host' => 'localhost',
|
|
||||||
'queue' => 'default',
|
|
||||||
'ttr' => 60,
|
|
||||||
],
|
|
||||||
|
|
||||||
'sqs' => [
|
|
||||||
'driver' => 'sqs',
|
|
||||||
'key' => 'your-public-key',
|
|
||||||
'secret' => 'your-secret-key',
|
|
||||||
'queue' => 'your-queue-url',
|
|
||||||
'region' => 'us-east-1',
|
|
||||||
],
|
|
||||||
|
|
||||||
'iron' => [
|
|
||||||
'driver' => 'iron',
|
|
||||||
'host' => 'mq-aws-us-east-1.iron.io',
|
|
||||||
'token' => 'your-token',
|
|
||||||
'project' => 'your-project-id',
|
|
||||||
'queue' => 'your-queue-name',
|
|
||||||
'encrypt' => true,
|
|
||||||
],
|
|
||||||
|
|
||||||
'redis' => [
|
|
||||||
'driver' => 'redis',
|
|
||||||
'connection' => 'default',
|
|
||||||
'queue' => 'default',
|
|
||||||
'expire' => 60,
|
|
||||||
],
|
|
||||||
|
|
||||||
],
|
|
||||||
|
|
||||||
// Failed queue job logging
|
|
||||||
'failed' => [
|
|
||||||
'database' => 'mysql', 'table' => 'failed_jobs',
|
|
||||||
],
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Third party service configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Single option to disable non-auth external services such as Gravatar and Draw.io
|
|
||||||
'disable_services' => env('DISABLE_EXTERNAL_SERVICES', false),
|
|
||||||
|
|
||||||
// Draw.io integration active
|
|
||||||
'drawio' => env('DRAWIO', !env('DISABLE_EXTERNAL_SERVICES', false)),
|
|
||||||
|
|
||||||
// URL for fetching avatars
|
|
||||||
'avatar_url' => env('AVATAR_URL', ''),
|
|
||||||
|
|
||||||
// Callback URL for social authentication methods
|
|
||||||
'callback_url' => env('APP_URL', false),
|
|
||||||
|
|
||||||
'mailgun' => [
|
|
||||||
'domain' => '',
|
|
||||||
'secret' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
'ses' => [
|
|
||||||
'key' => '',
|
|
||||||
'secret' => '',
|
|
||||||
'region' => 'us-east-1',
|
|
||||||
],
|
|
||||||
|
|
||||||
'stripe' => [
|
|
||||||
'model' => \BookStack\Auth\User::class,
|
|
||||||
'key' => '',
|
|
||||||
'secret' => '',
|
|
||||||
],
|
|
||||||
|
|
||||||
'github' => [
|
|
||||||
'client_id' => env('GITHUB_APP_ID', false),
|
|
||||||
'client_secret' => env('GITHUB_APP_SECRET', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/github/callback',
|
|
||||||
'name' => 'GitHub',
|
|
||||||
'auto_register' => env('GITHUB_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('GITHUB_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'google' => [
|
|
||||||
'client_id' => env('GOOGLE_APP_ID', false),
|
|
||||||
'client_secret' => env('GOOGLE_APP_SECRET', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/google/callback',
|
|
||||||
'name' => 'Google',
|
|
||||||
'auto_register' => env('GOOGLE_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('GOOGLE_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
'select_account' => env('GOOGLE_SELECT_ACCOUNT', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'slack' => [
|
|
||||||
'client_id' => env('SLACK_APP_ID', false),
|
|
||||||
'client_secret' => env('SLACK_APP_SECRET', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/slack/callback',
|
|
||||||
'name' => 'Slack',
|
|
||||||
'auto_register' => env('SLACK_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('SLACK_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'facebook' => [
|
|
||||||
'client_id' => env('FACEBOOK_APP_ID', false),
|
|
||||||
'client_secret' => env('FACEBOOK_APP_SECRET', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/facebook/callback',
|
|
||||||
'name' => 'Facebook',
|
|
||||||
'auto_register' => env('FACEBOOK_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('FACEBOOK_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'twitter' => [
|
|
||||||
'client_id' => env('TWITTER_APP_ID', false),
|
|
||||||
'client_secret' => env('TWITTER_APP_SECRET', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/twitter/callback',
|
|
||||||
'name' => 'Twitter',
|
|
||||||
'auto_register' => env('TWITTER_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('TWITTER_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'azure' => [
|
|
||||||
'client_id' => env('AZURE_APP_ID', false),
|
|
||||||
'client_secret' => env('AZURE_APP_SECRET', false),
|
|
||||||
'tenant' => env('AZURE_TENANT', false),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/azure/callback',
|
|
||||||
'name' => 'Microsoft Azure',
|
|
||||||
'auto_register' => env('AZURE_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('AZURE_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'okta' => [
|
|
||||||
'client_id' => env('OKTA_APP_ID'),
|
|
||||||
'client_secret' => env('OKTA_APP_SECRET'),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/okta/callback',
|
|
||||||
'base_url' => env('OKTA_BASE_URL'),
|
|
||||||
'name' => 'Okta',
|
|
||||||
'auto_register' => env('OKTA_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('OKTA_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'gitlab' => [
|
|
||||||
'client_id' => env('GITLAB_APP_ID'),
|
|
||||||
'client_secret' => env('GITLAB_APP_SECRET'),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/gitlab/callback',
|
|
||||||
'instance_uri' => env('GITLAB_BASE_URI'), // Needed only for self hosted instances
|
|
||||||
'name' => 'GitLab',
|
|
||||||
'auto_register' => env('GITLAB_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('GITLAB_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'twitch' => [
|
|
||||||
'client_id' => env('TWITCH_APP_ID'),
|
|
||||||
'client_secret' => env('TWITCH_APP_SECRET'),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/twitch/callback',
|
|
||||||
'name' => 'Twitch',
|
|
||||||
'auto_register' => env('TWITCH_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('TWITCH_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'discord' => [
|
|
||||||
'client_id' => env('DISCORD_APP_ID'),
|
|
||||||
'client_secret' => env('DISCORD_APP_SECRET'),
|
|
||||||
'redirect' => env('APP_URL') . '/login/service/discord/callback',
|
|
||||||
'name' => 'Discord',
|
|
||||||
'auto_register' => env('DISCORD_AUTO_REGISTER', false),
|
|
||||||
'auto_confirm' => env('DISCORD_AUTO_CONFIRM_EMAIL', false),
|
|
||||||
],
|
|
||||||
|
|
||||||
'ldap' => [
|
|
||||||
'server' => env('LDAP_SERVER', false),
|
|
||||||
'dn' => env('LDAP_DN', false),
|
|
||||||
'pass' => env('LDAP_PASS', false),
|
|
||||||
'base_dn' => env('LDAP_BASE_DN', false),
|
|
||||||
'user_filter' => env('LDAP_USER_FILTER', '(&(uid=${user}))'),
|
|
||||||
'version' => env('LDAP_VERSION', false),
|
|
||||||
'email_attribute' => env('LDAP_EMAIL_ATTRIBUTE', 'mail'),
|
|
||||||
'display_name_attribute' => env('LDAP_DISPLAY_NAME_ATTRIBUTE', 'cn'),
|
|
||||||
'follow_referrals' => env('LDAP_FOLLOW_REFERRALS', false),
|
|
||||||
'user_to_groups' => env('LDAP_USER_TO_GROUPS',false),
|
|
||||||
'group_attribute' => env('LDAP_GROUP_ATTRIBUTE', 'memberOf'),
|
|
||||||
'remove_from_groups' => env('LDAP_REMOVE_FROM_GROUPS',false),
|
|
||||||
'tls_insecure' => env('LDAP_TLS_INSECURE', false),
|
|
||||||
]
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Session configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// Default session driver
|
|
||||||
// Options: file, cookie, database, redis, memcached, array
|
|
||||||
'driver' => env('SESSION_DRIVER', 'file'),
|
|
||||||
|
|
||||||
// Session lifetime, in minutes
|
|
||||||
'lifetime' => env('SESSION_LIFETIME', 120),
|
|
||||||
|
|
||||||
// Expire session on browser close
|
|
||||||
'expire_on_close' => false,
|
|
||||||
|
|
||||||
// Encrypt session data
|
|
||||||
'encrypt' => false,
|
|
||||||
|
|
||||||
// Location to store session files
|
|
||||||
'files' => storage_path('framework/sessions'),
|
|
||||||
|
|
||||||
// Session Database Connection
|
|
||||||
// When using the "database" or "redis" session drivers, you can specify a
|
|
||||||
// connection that should be used to manage these sessions. This should
|
|
||||||
// correspond to a connection in your database configuration options.
|
|
||||||
'connection' => null,
|
|
||||||
|
|
||||||
// Session database table, if database driver is in use
|
|
||||||
'table' => 'sessions',
|
|
||||||
|
|
||||||
// Session Sweeping Lottery
|
|
||||||
// Some session drivers must manually sweep their storage location to get
|
|
||||||
// rid of old sessions from storage. Here are the chances that it will
|
|
||||||
// happen on a given request. By default, the odds are 2 out of 100.
|
|
||||||
'lottery' => [2, 100],
|
|
||||||
|
|
||||||
|
|
||||||
// Session Cookie Name
|
|
||||||
// Here you may change the name of the cookie used to identify a session
|
|
||||||
// instance by ID. The name specified here will get used every time a
|
|
||||||
// new session cookie is created by the framework for every driver.
|
|
||||||
'cookie' => env('SESSION_COOKIE_NAME', 'bookstack_session'),
|
|
||||||
|
|
||||||
// Session Cookie Path
|
|
||||||
// The session cookie path determines the path for which the cookie will
|
|
||||||
// be regarded as available. Typically, this will be the root path of
|
|
||||||
// your application but you are free to change this when necessary.
|
|
||||||
'path' => '/',
|
|
||||||
|
|
||||||
// Session Cookie Domain
|
|
||||||
// Here you may change the domain of the cookie used to identify a session
|
|
||||||
// in your application. This will determine which domains the cookie is
|
|
||||||
// available to in your application. A sensible default has been set.
|
|
||||||
'domain' => env('SESSION_DOMAIN', null),
|
|
||||||
|
|
||||||
// HTTPS Only Cookies
|
|
||||||
// By setting this option to true, session cookies will only be sent back
|
|
||||||
// to the server if the browser has a HTTPS connection. This will keep
|
|
||||||
// the cookie from being sent to you if it can not be done securely.
|
|
||||||
'secure' => env('SESSION_SECURE_COOKIE', false),
|
|
||||||
|
|
||||||
// HTTP Access Only
|
|
||||||
// Setting this value to true will prevent JavaScript from accessing the
|
|
||||||
// value of the cookie and the cookie will only be accessible through the HTTP protocol.
|
|
||||||
'http_only' => true,
|
|
||||||
|
|
||||||
// Same-Site Cookies
|
|
||||||
// This option determines how your cookies behave when cross-site requests
|
|
||||||
// take place, and can be used to mitigate CSRF attacks. By default, we
|
|
||||||
// do not enable this as other CSRF protection services are in place.
|
|
||||||
// Options: lax, strict
|
|
||||||
'same_site' => null,
|
|
||||||
];
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Default system settings.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
'app-name' => 'BookStack',
|
|
||||||
'app-logo' => '',
|
|
||||||
'app-name-header' => true,
|
|
||||||
'app-editor' => 'wysiwyg',
|
|
||||||
'app-color' => '#206ea7',
|
|
||||||
'app-color-light' => 'rgba(32,110,167,0.15)',
|
|
||||||
'app-custom-head' => false,
|
|
||||||
'registration-enabled' => false,
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
/**
|
|
||||||
* View configuration options.
|
|
||||||
*
|
|
||||||
* Changes to these config files are not supported by BookStack and may break upon updates.
|
|
||||||
* Configuration should be altered via the `.env` file or environment variables.
|
|
||||||
* Do not edit this file unless you're happy to maintain any changes yourself.
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Join up possible view locations
|
|
||||||
$viewPaths = [realpath(base_path('resources/views'))];
|
|
||||||
if ($theme = env('APP_THEME', false)) {
|
|
||||||
array_unshift($viewPaths, base_path('themes/' . $theme));
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
|
|
||||||
// App theme
|
|
||||||
// This option defines the theme to use for the application. When a theme
|
|
||||||
// is set there must be a `themes/<theme_name>` folder to hold the
|
|
||||||
// custom theme overrides.
|
|
||||||
'theme' => env('APP_THEME', false),
|
|
||||||
|
|
||||||
// View Storage Paths
|
|
||||||
// Most templating systems load templates from disk. Here you may specify
|
|
||||||
// an array of paths that should be checked for your views. Of course
|
|
||||||
// the usual Laravel view path has already been registered for you.
|
|
||||||
'paths' => $viewPaths,
|
|
||||||
|
|
||||||
// Compiled View Path
|
|
||||||
// This option determines where all the compiled Blade templates will be
|
|
||||||
// stored for your application. Typically, this is within the storage
|
|
||||||
// directory. However, as usual, you are free to change this value.
|
|
||||||
'compiled' => realpath(storage_path('framework/views')),
|
|
||||||
|
|
||||||
];
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
|
||||||
|
|
||||||
use BookStack\Uploads\ImageService;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
|
||||||
|
|
||||||
class CleanupImages extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'bookstack:cleanup-images
|
|
||||||
{--a|all : Include images that are used in page revisions}
|
|
||||||
{--f|force : Actually run the deletions}
|
|
||||||
';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Cleanup images and drawings';
|
|
||||||
|
|
||||||
|
|
||||||
protected $imageService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new command instance.
|
|
||||||
* @param \BookStack\Uploads\ImageService $imageService
|
|
||||||
*/
|
|
||||||
public function __construct(ImageService $imageService)
|
|
||||||
{
|
|
||||||
$this->imageService = $imageService;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$checkRevisions = $this->option('all') ? false : true;
|
|
||||||
$dryRun = $this->option('force') ? false : true;
|
|
||||||
|
|
||||||
if (!$dryRun) {
|
|
||||||
$proceed = $this->confirm("This operation is destructive and is not guaranteed to be fully accurate.\nEnsure you have a backup of your images.\nAre you sure you want to proceed?");
|
|
||||||
if (!$proceed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$deleted = $this->imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
|
||||||
$deleteCount = count($deleted);
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
$this->comment('Dry run, No images have been deleted');
|
|
||||||
$this->comment($deleteCount . ' images found that would have been deleted');
|
|
||||||
$this->showDeletedImages($deleted);
|
|
||||||
$this->comment('Run with -f or --force to perform deletions');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->showDeletedImages($deleted);
|
|
||||||
$this->comment($deleteCount . ' images deleted');
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function showDeletedImages($paths)
|
|
||||||
{
|
|
||||||
if ($this->getOutput()->getVerbosity() <= OutputInterface::VERBOSITY_NORMAL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (count($paths) > 0) {
|
|
||||||
$this->line('Images to delete:');
|
|
||||||
}
|
|
||||||
foreach ($paths as $path) {
|
|
||||||
$this->line($path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
use BookStack\Activity;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ClearActivity extends Command
|
class ClearActivity extends Command
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\PageRevision;
|
use BookStack\PageRevision;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class ClearRevisions extends Command
|
class ClearRevisions extends Command
|
||||||
|
|||||||
@@ -1,85 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
|
||||||
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class CreateAdmin extends Command
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'bookstack:create-admin
|
|
||||||
{--email= : The email address for the new admin user}
|
|
||||||
{--name= : The name of the new admin user}
|
|
||||||
{--password= : The password to assign to the new admin user}';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Add a new admin user to the system';
|
|
||||||
|
|
||||||
protected $userRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new command instance.
|
|
||||||
*
|
|
||||||
* @param UserRepo $userRepo
|
|
||||||
*/
|
|
||||||
public function __construct(UserRepo $userRepo)
|
|
||||||
{
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the console command.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$email = trim($this->option('email'));
|
|
||||||
if (empty($email)) {
|
|
||||||
$email = $this->ask('Please specify an email address for the new admin user');
|
|
||||||
}
|
|
||||||
if (mb_strlen($email) < 5 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
|
||||||
return $this->error('Invalid email address provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->userRepo->getByEmail($email) !== null) {
|
|
||||||
return $this->error('A user with the provided email already exists!');
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = trim($this->option('name'));
|
|
||||||
if (empty($name)) {
|
|
||||||
$name = $this->ask('Please specify an name for the new admin user');
|
|
||||||
}
|
|
||||||
if (mb_strlen($name) < 2) {
|
|
||||||
return $this->error('Invalid name provided');
|
|
||||||
}
|
|
||||||
|
|
||||||
$password = trim($this->option('password'));
|
|
||||||
if (empty($password)) {
|
|
||||||
$password = $this->secret('Please specify a password for the new admin user');
|
|
||||||
}
|
|
||||||
if (mb_strlen($password) < 5) {
|
|
||||||
return $this->error('Invalid password provided, Must be at least 5 characters');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$user = $this->userRepo->create(['email' => $email, 'name' => $name, 'password' => $password]);
|
|
||||||
$this->userRepo->attachSystemRole($user, 'admin');
|
|
||||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
|
||||||
$user->email_confirmed = true;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
$this->info("Admin account with email \"{$user->email}\" successfully created!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use Illuminate\Console\Command;
|
|
||||||
|
|
||||||
class DeleteUsers extends Command
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The name and signature of the console command.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature = 'bookstack:delete-users';
|
|
||||||
|
|
||||||
protected $user;
|
|
||||||
|
|
||||||
protected $userRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The console command description.
|
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $description = 'Delete users that are not "admin" or system users.';
|
|
||||||
|
|
||||||
public function __construct(User $user, UserRepo $userRepo)
|
|
||||||
{
|
|
||||||
$this->user = $user;
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$confirm = $this->ask('This will delete all users from the system that are not "admin" or system users. Are you sure you want to continue? (Type "yes" to continue)');
|
|
||||||
$numDeleted = 0;
|
|
||||||
if (strtolower(trim($confirm)) === 'yes') {
|
|
||||||
$totalUsers = $this->user->count();
|
|
||||||
$users = $this->user->where('system_name', '=', null)->with('roles')->get();
|
|
||||||
foreach ($users as $user) {
|
|
||||||
if ($user->hasSystemRole('admin')) {
|
|
||||||
// don't delete users with "admin" role
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$this->userRepo->destroy($user);
|
|
||||||
++$numDeleted;
|
|
||||||
}
|
|
||||||
$this->info("Deleted $numDeleted of $totalUsers total users.");
|
|
||||||
} else {
|
|
||||||
$this->info('Exiting...');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionService;
|
use BookStack\Services\PermissionService;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class RegeneratePermissions extends Command
|
class RegeneratePermissions extends Command
|
||||||
@@ -31,7 +31,7 @@ class RegeneratePermissions extends Command
|
|||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
*
|
||||||
* @param \BookStack\Auth\\BookStack\Auth\Permissions\PermissionService $permissionService
|
* @param PermissionService $permissionService
|
||||||
*/
|
*/
|
||||||
public function __construct(PermissionService $permissionService)
|
public function __construct(PermissionService $permissionService)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
namespace BookStack\Console\Commands;
|
namespace BookStack\Console\Commands;
|
||||||
|
|
||||||
use BookStack\Entities\SearchService;
|
use BookStack\Services\SearchService;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
class RegenerateSearch extends Command
|
class RegenerateSearch extends Command
|
||||||
@@ -19,14 +19,14 @@ class RegenerateSearch extends Command
|
|||||||
*
|
*
|
||||||
* @var string
|
* @var string
|
||||||
*/
|
*/
|
||||||
protected $description = 'Re-index all content for searching';
|
protected $description = 'Command description';
|
||||||
|
|
||||||
protected $searchService;
|
protected $searchService;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new command instance.
|
* Create a new command instance.
|
||||||
*
|
*
|
||||||
* @param \BookStack\Entities\SearchService $searchService
|
* @param SearchService $searchService
|
||||||
*/
|
*/
|
||||||
public function __construct(SearchService $searchService)
|
public function __construct(SearchService $searchService)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,7 +11,12 @@ class Kernel extends ConsoleKernel
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $commands = [
|
protected $commands = [
|
||||||
//
|
Commands\ClearViews::class,
|
||||||
|
Commands\ClearActivity::class,
|
||||||
|
Commands\ClearRevisions::class,
|
||||||
|
Commands\RegeneratePermissions::class,
|
||||||
|
Commands\RegenerateSearch::class,
|
||||||
|
Commands\UpgradeDatabaseEncoding::class
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,14 +29,4 @@ class Kernel extends ConsoleKernel
|
|||||||
{
|
{
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the commands for the application.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
protected function commands()
|
|
||||||
{
|
|
||||||
$this->load(__DIR__.'/Commands');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities;
|
|
||||||
|
|
||||||
use BookStack\Uploads\Image;
|
|
||||||
|
|
||||||
class Book extends Entity
|
|
||||||
{
|
|
||||||
public $searchFactor = 2;
|
|
||||||
|
|
||||||
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
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getUrl($path = false)
|
|
||||||
{
|
|
||||||
if ($path !== false) {
|
|
||||||
return url('/books/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
|
||||||
}
|
|
||||||
return url('/books/' . urlencode($this->slug));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns book cover image, if book cover 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 = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
||||||
if (!$this->image_id) {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$cover = $this->cover ? url($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 all pages within this book.
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
||||||
*/
|
|
||||||
public function pages()
|
|
||||||
{
|
|
||||||
return $this->hasMany(Page::class);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the direct child pages of this book.
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
||||||
*/
|
|
||||||
public function directPages()
|
|
||||||
{
|
|
||||||
return $this->pages()->where('chapter_id', '=', '0');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all chapters within this book.
|
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\HasMany
|
|
||||||
*/
|
|
||||||
public function chapters()
|
|
||||||
{
|
|
||||||
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
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getExcerpt(int $length = 100)
|
|
||||||
{
|
|
||||||
$description = $this->description;
|
|
||||||
return mb_strlen($description) > $length ? mb_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\\\\Book' 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,107 +0,0 @@
|
|||||||
<?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')
|
|
||||||
->withPivot('order')
|
|
||||||
->orderBy('order', 'asc');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the url for this bookshelf.
|
|
||||||
* @param string|bool $path
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getUrl($path = false)
|
|
||||||
{
|
|
||||||
if ($path !== false) {
|
|
||||||
return url('/shelves/' . urlencode($this->slug) . '/' . trim($path, '/'));
|
|
||||||
}
|
|
||||||
return url('/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)
|
|
||||||
{
|
|
||||||
// TODO - Make generic, focused on books right now, Perhaps set-up a better image
|
|
||||||
$default = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
|
|
||||||
if (!$this->image_id) {
|
|
||||||
return $default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$cover = $this->cover ? url($this->cover->getThumb($width, $height, false)) : $default;
|
|
||||||
} catch (\Exception $err) {
|
|
||||||
$cover = $default;
|
|
||||||
}
|
|
||||||
return $cover;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the cover image of the shelf
|
|
||||||
* @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(int $length = 100)
|
|
||||||
{
|
|
||||||
$description = $this->description;
|
|
||||||
return mb_strlen($description) > $length ? mb_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";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if this shelf contains the given book.
|
|
||||||
* @param Book $book
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
public function contains(Book $book)
|
|
||||||
{
|
|
||||||
return $this->books()->where('id', '=', $book->id)->count() > 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities;
|
|
||||||
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class BreadcrumbsViewComposer
|
|
||||||
{
|
|
||||||
|
|
||||||
protected $entityContextManager;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BreadcrumbsViewComposer constructor.
|
|
||||||
* @param EntityContextManager $entityContextManager
|
|
||||||
*/
|
|
||||||
public function __construct(EntityContextManager $entityContextManager)
|
|
||||||
{
|
|
||||||
$this->entityContextManager = $entityContextManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Modify data when the view is composed.
|
|
||||||
* @param View $view
|
|
||||||
*/
|
|
||||||
public function compose(View $view)
|
|
||||||
{
|
|
||||||
$crumbs = $view->getData()['crumbs'];
|
|
||||||
if (array_first($crumbs) instanceof Book) {
|
|
||||||
$shelf = $this->entityContextManager->getContextualShelfForBook(array_first($crumbs));
|
|
||||||
if ($shelf) {
|
|
||||||
array_unshift($crumbs, $shelf);
|
|
||||||
$view->with('crumbs', $crumbs);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities;
|
|
||||||
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use Illuminate\Session\Store;
|
|
||||||
|
|
||||||
class EntityContextManager
|
|
||||||
{
|
|
||||||
protected $session;
|
|
||||||
protected $entityRepo;
|
|
||||||
|
|
||||||
protected $KEY_SHELF_CONTEXT_ID = 'context_bookshelf_id';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* EntityContextManager constructor.
|
|
||||||
* @param Store $session
|
|
||||||
* @param EntityRepo $entityRepo
|
|
||||||
*/
|
|
||||||
public function __construct(Store $session, EntityRepo $entityRepo)
|
|
||||||
{
|
|
||||||
$this->session = $session;
|
|
||||||
$this->entityRepo = $entityRepo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current bookshelf context for the given book.
|
|
||||||
* @param Book $book
|
|
||||||
* @return Bookshelf|null
|
|
||||||
*/
|
|
||||||
public function getContextualShelfForBook(Book $book)
|
|
||||||
{
|
|
||||||
$contextBookshelfId = $this->session->get($this->KEY_SHELF_CONTEXT_ID, null);
|
|
||||||
if (is_int($contextBookshelfId)) {
|
|
||||||
|
|
||||||
/** @var Bookshelf $shelf */
|
|
||||||
$shelf = $this->entityRepo->getById('bookshelf', $contextBookshelfId);
|
|
||||||
|
|
||||||
if ($shelf && $shelf->contains($book)) {
|
|
||||||
return $shelf;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store the current contextual shelf ID.
|
|
||||||
* @param int $shelfId
|
|
||||||
*/
|
|
||||||
public function setShelfContext(int $shelfId)
|
|
||||||
{
|
|
||||||
$this->session->put($this->KEY_SHELF_CONTEXT_ID, $shelfId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear the session stored shelf context id.
|
|
||||||
*/
|
|
||||||
public function clearShelfContext()
|
|
||||||
{
|
|
||||||
$this->session->forget($this->KEY_SHELF_CONTEXT_ID);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<?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];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the morph classes, as an array, for a single or multiple types.
|
|
||||||
* @param string|array $types
|
|
||||||
* @return array<string>
|
|
||||||
*/
|
|
||||||
public function getMorphClasses($types)
|
|
||||||
{
|
|
||||||
if (is_string($types)) {
|
|
||||||
$types = [$types];
|
|
||||||
}
|
|
||||||
|
|
||||||
$morphClasses = [];
|
|
||||||
foreach ($types as $type) {
|
|
||||||
$model = $this->get($type);
|
|
||||||
$morphClasses[] = $model->getMorphClass();
|
|
||||||
}
|
|
||||||
return $morphClasses;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,924 +0,0 @@
|
|||||||
<?php namespace BookStack\Entities\Repos;
|
|
||||||
|
|
||||||
use Activity;
|
|
||||||
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 DOMNode;
|
|
||||||
use DOMXPath;
|
|
||||||
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
use Throwable;
|
|
||||||
|
|
||||||
class EntityRepo
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @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 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 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 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
|
|
||||||
* @param string $sort
|
|
||||||
* @param string $order
|
|
||||||
* @param null|callable $queryAddition
|
|
||||||
* @return LengthAwarePaginator
|
|
||||||
*/
|
|
||||||
public function getAllPaginated($type, int $count = 10, string $sort = 'name', string $order = 'asc', $queryAddition = null)
|
|
||||||
{
|
|
||||||
$query = $this->entityQuery($type);
|
|
||||||
$query = $this->addSortToQuery($query, $sort, $order);
|
|
||||||
if ($queryAddition) {
|
|
||||||
$queryAddition($query);
|
|
||||||
}
|
|
||||||
return $query->paginate($count);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add sorting operations to an entity query.
|
|
||||||
* @param Builder $query
|
|
||||||
* @param string $sort
|
|
||||||
* @param string $order
|
|
||||||
* @return Builder
|
|
||||||
*/
|
|
||||||
protected function addSortToQuery(Builder $query, string $sort = 'name', string $order = 'asc')
|
|
||||||
{
|
|
||||||
$order = ($order === 'asc') ? 'asc' : 'desc';
|
|
||||||
$propertySorts = ['name', 'created_at', 'updated_at'];
|
|
||||||
|
|
||||||
if (in_array($sort, $propertySorts)) {
|
|
||||||
return $query->orderBy($sort, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $query;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 $type
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getPopular(string $type, int $count = 10, int $page = 0)
|
|
||||||
{
|
|
||||||
return $this->viewService->getPopular($count, $page, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 Bookshelf $bookshelf
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection|static[]
|
|
||||||
*/
|
|
||||||
public function getBookshelfChildren(Bookshelf $bookshelf)
|
|
||||||
{
|
|
||||||
return $this->permissionService->enforceEntityRestrictions('book', $bookshelf->books())->get();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the direct children of a book.
|
|
||||||
* @param Book $book
|
|
||||||
* @return \Illuminate\Database\Eloquent\Collection
|
|
||||||
*/
|
|
||||||
public function getBookDirectChildren(Book $book)
|
|
||||||
{
|
|
||||||
$pages = $this->permissionService->enforceEntityRestrictions('page', $book->directPages())->get();
|
|
||||||
$chapters = $this->permissionService->enforceEntityRestrictions('chapters', $book->chapters())->get();
|
|
||||||
return collect()->concat($pages)->concat($chapters)->sortBy('priority')->sortByDesc('draft');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all child objects of a book.
|
|
||||||
* Returns a sorted collection of Pages and Chapters.
|
|
||||||
* Loads the book slug onto child elements to prevent access database access for getting the slug.
|
|
||||||
* @param 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 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 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 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 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 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 Entity $entityModel
|
|
||||||
* @param array $input
|
|
||||||
* @return 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 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Append a Book to a BookShelf.
|
|
||||||
* @param Bookshelf $shelf
|
|
||||||
* @param Book $book
|
|
||||||
*/
|
|
||||||
public function appendBookToShelf(Bookshelf $shelf, Book $book)
|
|
||||||
{
|
|
||||||
if ($shelf->contains($book)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$maxOrder = $shelf->books()->max('order');
|
|
||||||
$shelf->books()->attach($book->id, ['order' => $maxOrder + 1]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the book that an entity belongs to.
|
|
||||||
* @param string $type
|
|
||||||
* @param integer $newBookId
|
|
||||||
* @param Entity $entity
|
|
||||||
* @param bool $rebuildPermissions
|
|
||||||
* @return 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();
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc->loadHTML(mb_convert_encoding('<body>'.$matchedPage->html.'</body>', 'HTML-ENTITIES', 'UTF-8'));
|
|
||||||
$matchingElem = $doc->getElementById($splitInclude[1]);
|
|
||||||
if ($matchingElem === null) {
|
|
||||||
$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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
libxml_clear_errors();
|
|
||||||
$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
|
|
||||||
{
|
|
||||||
if ($html == '') {
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
libxml_use_internal_errors(true);
|
|
||||||
$doc = new DOMDocument();
|
|
||||||
$doc->loadHTML(mb_convert_encoding($html, 'HTML-ENTITIES', 'UTF-8'));
|
|
||||||
$xPath = new DOMXPath($doc);
|
|
||||||
|
|
||||||
// Remove standard script tags
|
|
||||||
$scriptElems = $xPath->query('//script');
|
|
||||||
foreach ($scriptElems as $scriptElem) {
|
|
||||||
$scriptElem->parentNode->removeChild($scriptElem);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove data or JavaScript iFrames
|
|
||||||
$badIframes = $xPath->query('//*[contains(@src, \'data:\')] | //*[contains(@src, \'javascript:\')] | //*[@srcdoc]');
|
|
||||||
foreach ($badIframes as $badIframe) {
|
|
||||||
$badIframe->parentNode->removeChild($badIframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove 'on*' attributes
|
|
||||||
$onAttributes = $xPath->query('//@*[starts-with(name(), \'on\')]');
|
|
||||||
foreach ($onAttributes as $attr) {
|
|
||||||
/** @var \DOMAttr $attr*/
|
|
||||||
$attrName = $attr->nodeName;
|
|
||||||
$attr->parentNode->removeAttribute($attrName);
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = '';
|
|
||||||
$topElems = $doc->documentElement->childNodes->item(0)->childNodes;
|
|
||||||
foreach ($topElems as $child) {
|
|
||||||
$html .= $doc->saveHTML($child);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Search for image usage within page content.
|
|
||||||
* @param $imageString
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function searchForImage($imageString)
|
|
||||||
{
|
|
||||||
$pages = $this->entityQuery('page')->where('html', 'like', '%' . $imageString . '%')->get(['id', 'name', 'slug', 'book_id']);
|
|
||||||
foreach ($pages as $page) {
|
|
||||||
$page->url = $page->getUrl();
|
|
||||||
$page->html = '';
|
|
||||||
$page->text = '';
|
|
||||||
}
|
|
||||||
return count($pages) > 0 ? $pages : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy a bookshelf instance
|
|
||||||
* @param Bookshelf $shelf
|
|
||||||
* @throws Throwable
|
|
||||||
*/
|
|
||||||
public function destroyBookshelf(Bookshelf $shelf)
|
|
||||||
{
|
|
||||||
$this->destroyEntityCommonRelations($shelf);
|
|
||||||
$shelf->delete();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy the provided book and all its child entities.
|
|
||||||
* @param 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 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 & remove setting if not used or throw error if active
|
|
||||||
$customHome = setting('app-homepage', '0:');
|
|
||||||
if (intval($page->id) === intval(explode(':', $customHome)[0])) {
|
|
||||||
if (setting('app-homepage-type') === 'page') {
|
|
||||||
throw new NotifyException(trans('errors.page_custom_home_deletion'), $page->getUrl());
|
|
||||||
}
|
|
||||||
setting()->remove('app-homepage');
|
|
||||||
}
|
|
||||||
|
|
||||||
$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 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 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,561 +0,0 @@
|
|||||||
<?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 DOMElement;
|
|
||||||
use DOMXPath;
|
|
||||||
use Illuminate\Support\Collection;
|
|
||||||
|
|
||||||
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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($input['template']) && userCan('templates-manage')) {
|
|
||||||
$page->template = ($input['template'] === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
$summary = $input['summary'] ?? null;
|
|
||||||
if ($oldHtml !== $input['html'] || $oldName !== $input['name'] || $summary !== null) {
|
|
||||||
$this->savePageRevision($page, $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;
|
|
||||||
|
|
||||||
// Set ids on top-level nodes
|
|
||||||
$idMap = [];
|
|
||||||
foreach ($childNodes as $index => $childNode) {
|
|
||||||
$this->setUniqueId($childNode, $idMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure no duplicate ids within child items
|
|
||||||
$xPath = new DOMXPath($doc);
|
|
||||||
$idElems = $xPath->query('//body//*//*[@id]');
|
|
||||||
foreach ($idElems as $domElem) {
|
|
||||||
$this->setUniqueId($domElem, $idMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate inner html as a string
|
|
||||||
$html = '';
|
|
||||||
foreach ($childNodes as $childNode) {
|
|
||||||
$html .= $doc->saveHTML($childNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $html;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set a unique id on the given DOMElement.
|
|
||||||
* A map for existing ID's should be passed in to check for current existence.
|
|
||||||
* @param DOMElement $element
|
|
||||||
* @param array $idMap
|
|
||||||
*/
|
|
||||||
protected function setUniqueId($element, array &$idMap)
|
|
||||||
{
|
|
||||||
if (get_class($element) !== 'DOMElement') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overwrite id if not a BookStack custom id
|
|
||||||
$existingId = $element->getAttribute('id');
|
|
||||||
if (strpos($existingId, 'bkmrk') === 0 && !isset($idMap[$existingId])) {
|
|
||||||
$idMap[$existingId] = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create an unique id for the element
|
|
||||||
// Uses the content as a basis to ensure output is the same every time
|
|
||||||
// the same content is passed through.
|
|
||||||
$contentId = 'bkmrk-' . mb_substr(strtolower(preg_replace('/\s+/', '-', trim($element->nodeValue))), 0, 20);
|
|
||||||
$newId = urlencode($contentId);
|
|
||||||
$loopIndex = 0;
|
|
||||||
|
|
||||||
while (isset($idMap[$newId])) {
|
|
||||||
$newId = urlencode($contentId . '-' . $loopIndex);
|
|
||||||
$loopIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$element->setAttribute('id', $newId);
|
|
||||||
$idMap[$newId] = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the plain text version of a page's content.
|
|
||||||
* @param \BookStack\Entities\Page $page
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
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']);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($input['template']) && userCan('templates-manage')) {
|
|
||||||
$draftPage->template = ($input['template'] === 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
$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($headers)->map(function($header) {
|
|
||||||
$text = trim(str_replace("\xc2\xa0", '', $header->nodeValue));
|
|
||||||
$text = mb_substr($text, 0, 100);
|
|
||||||
|
|
||||||
return [
|
|
||||||
'nodeName' => strtolower($header->nodeName),
|
|
||||||
'level' => intval(str_replace('h', '', $header->nodeName)),
|
|
||||||
'link' => '#' . $header->getAttribute('id'),
|
|
||||||
'text' => $text,
|
|
||||||
];
|
|
||||||
})->filter(function($header) {
|
|
||||||
return mb_strlen($header['text']) > 0;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Shift headers if only smaller headers have been used
|
|
||||||
$levelChange = ($tree->pluck('level')->min() - 1);
|
|
||||||
$tree = $tree->map(function ($header) use ($levelChange) {
|
|
||||||
$header['level'] -= ($levelChange);
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get pages that have been marked as templates.
|
|
||||||
* @param int $count
|
|
||||||
* @param int $page
|
|
||||||
* @param string $search
|
|
||||||
* @return \Illuminate\Contracts\Pagination\LengthAwarePaginator
|
|
||||||
*/
|
|
||||||
public function getPageTemplates(int $count = 10, int $page = 1, string $search = '')
|
|
||||||
{
|
|
||||||
$query = $this->entityQuery('page')
|
|
||||||
->where('template', '=', true)
|
|
||||||
->orderBy('name', 'asc')
|
|
||||||
->skip( ($page - 1) * $count)
|
|
||||||
->take($count);
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$query->where('name', 'like', '%' . $search . '%');
|
|
||||||
}
|
|
||||||
|
|
||||||
$paginator = $query->paginate($count, ['*'], 'page', $page);
|
|
||||||
$paginator->withPath('/templates');
|
|
||||||
|
|
||||||
return $paginator;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +1,13 @@
|
|||||||
<?php namespace BookStack\Entities;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
|
||||||
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;
|
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
|
class Entity extends Ownable
|
||||||
{
|
{
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string - Name of property where the main text content is found
|
|
||||||
*/
|
|
||||||
public $textField = 'description';
|
public $textField = 'description';
|
||||||
|
|
||||||
/**
|
|
||||||
* @var float - Multiplier for search indexing.
|
|
||||||
*/
|
|
||||||
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.
|
* Compares this entity to another given entity.
|
||||||
* Matches by comparing class and id.
|
* Matches by comparing class and id.
|
||||||
@@ -70,9 +28,7 @@ class Entity extends Ownable
|
|||||||
{
|
{
|
||||||
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
$matches = [get_class($this), $this->id] === [get_class($entity), $entity->id];
|
||||||
|
|
||||||
if ($matches) {
|
if ($matches) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
if (($entity->isA('chapter') || $entity->isA('page')) && $this->isA('book')) {
|
||||||
return $entity->book_id === $this->id;
|
return $entity->book_id === $this->id;
|
||||||
@@ -102,11 +58,6 @@ class Entity extends Ownable
|
|||||||
return $this->morphMany(View::class, 'viewable');
|
return $this->morphMany(View::class, 'viewable');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function viewCountQuery()
|
|
||||||
{
|
|
||||||
return $this->views()->selectRaw('viewable_id, sum(views) as view_count')->groupBy('viewable_id');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Tag models that have been user assigned to this entity.
|
* Get the Tag models that have been user assigned to this entity.
|
||||||
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
* @return \Illuminate\Database\Eloquent\Relations\MorphMany
|
||||||
@@ -192,13 +143,13 @@ class Entity extends Ownable
|
|||||||
*/
|
*/
|
||||||
public static function getEntityInstance($type)
|
public static function getEntityInstance($type)
|
||||||
{
|
{
|
||||||
$types = ['Page', 'Book', 'Chapter', 'Bookshelf'];
|
$types = ['Page', 'Book', 'Chapter'];
|
||||||
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
$className = str_replace([' ', '-', '_'], '', ucwords($type));
|
||||||
if (!in_array($className, $types)) {
|
if (!in_array($className, $types)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return app('BookStack\\Entities\\' . $className);
|
return app('BookStack\\' . $className);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -208,10 +159,8 @@ class Entity extends Ownable
|
|||||||
*/
|
*/
|
||||||
public function getShortName($length = 25)
|
public function getShortName($length = 25)
|
||||||
{
|
{
|
||||||
if (mb_strlen($this->name) <= $length) {
|
if (strlen($this->name) <= $length) return $this->name;
|
||||||
return $this->name;
|
return substr($this->name, 0, $length - 3) . '...';
|
||||||
}
|
|
||||||
return mb_substr($this->name, 0, $length - 3) . '...';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,36 +172,17 @@ class Entity extends Ownable
|
|||||||
return $this->{$this->textField};
|
return $this->{$this->textField};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an excerpt of this entity's descriptive content to the specified length.
|
|
||||||
* @param int $length
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getExcerpt(int $length = 100)
|
|
||||||
{
|
|
||||||
$text = $this->getText();
|
|
||||||
if (mb_strlen($text) > $length) {
|
|
||||||
$text = mb_substr($text, 0, $length-3) . '...';
|
|
||||||
}
|
|
||||||
return trim($text);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a generalised, common raw query that can be 'unioned' across entities.
|
* Return a generalised, common raw query that can be 'unioned' across entities.
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function entityRawQuery()
|
public function entityRawQuery(){return '';}
|
||||||
{
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the url of this entity
|
* Get the url of this entity
|
||||||
* @param $path
|
* @param $path
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
public function getUrl($path = '/')
|
public function getUrl($path){return '/';}
|
||||||
{
|
|
||||||
return $path;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
use BookStack\Model;
|
|
||||||
|
|
||||||
class EntityPermission extends Model
|
class EntityPermission extends Model
|
||||||
{
|
{
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class AuthException extends PrettyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class AuthException extends PrettyException {}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class ConfirmationEmailException extends NotifyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class ConfirmationEmailException extends NotifyException {}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class FileUploadException extends PrettyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class FileUploadException extends PrettyException {}
|
||||||
@@ -3,13 +3,12 @@
|
|||||||
namespace BookStack\Exceptions;
|
namespace BookStack\Exceptions;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Auth\Access\AuthorizationException;
|
|
||||||
use Illuminate\Auth\AuthenticationException;
|
use Illuminate\Auth\AuthenticationException;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
|
||||||
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
|
||||||
|
use Illuminate\Auth\Access\AuthorizationException;
|
||||||
|
|
||||||
class Handler extends ExceptionHandler
|
class Handler extends ExceptionHandler
|
||||||
{
|
{
|
||||||
@@ -27,11 +26,10 @@ class Handler extends ExceptionHandler
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Report or log an exception.
|
* Report or log an exception.
|
||||||
|
*
|
||||||
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
* This is a great spot to send exceptions to Sentry, Bugsnag, etc.
|
||||||
*
|
*
|
||||||
* @param \Exception $e
|
* @param \Exception $e
|
||||||
* @return mixed
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
*/
|
||||||
public function report(Exception $e)
|
public function report(Exception $e)
|
||||||
{
|
{
|
||||||
@@ -62,11 +60,6 @@ class Handler extends ExceptionHandler
|
|||||||
return response()->view('errors/' . $code, ['message' => $message], $code);
|
return response()->view('errors/' . $code, ['message' => $message], $code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle 404 errors with a loaded session to enable showing user-specific information
|
|
||||||
if ($this->isExceptionType($e, NotFoundHttpException::class)) {
|
|
||||||
return \Route::respondWithRoute('fallback');
|
|
||||||
}
|
|
||||||
|
|
||||||
return parent::render($request, $e);
|
return parent::render($request, $e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,12 +69,9 @@ class Handler extends ExceptionHandler
|
|||||||
* @param $type
|
* @param $type
|
||||||
* @return bool
|
* @return bool
|
||||||
*/
|
*/
|
||||||
protected function isExceptionType(Exception $e, $type)
|
protected function isExceptionType(Exception $e, $type) {
|
||||||
{
|
|
||||||
do {
|
do {
|
||||||
if (is_a($e, $type)) {
|
if (is_a($e, $type)) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
} while ($e = $e->getPrevious());
|
} while ($e = $e->getPrevious());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -91,8 +81,7 @@ class Handler extends ExceptionHandler
|
|||||||
* @param Exception $e
|
* @param Exception $e
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
protected function getOriginalMessage(Exception $e)
|
protected function getOriginalMessage(Exception $e) {
|
||||||
{
|
|
||||||
do {
|
do {
|
||||||
$message = $e->getMessage();
|
$message = $e->getMessage();
|
||||||
} while ($e = $e->getPrevious());
|
} while ($e = $e->getPrevious());
|
||||||
@@ -114,16 +103,4 @@ class Handler extends ExceptionHandler
|
|||||||
|
|
||||||
return redirect()->guest('login');
|
return redirect()->guest('login');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a validation exception into a JSON response.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Illuminate\Validation\ValidationException $exception
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
protected function invalidJson($request, ValidationException $exception)
|
|
||||||
{
|
|
||||||
return response()->json($exception->errors(), $exception->status);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
|
||||||
|
|
||||||
use Exception;
|
|
||||||
|
|
||||||
class HttpFetchException extends Exception
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class ImageUploadException extends PrettyException
|
class ImageUploadException extends PrettyException {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class LdapException extends PrettyException
|
class LdapException extends PrettyException {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class NotFoundException extends PrettyException
|
|
||||||
{
|
class NotFoundException extends PrettyException {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NotFoundException constructor.
|
* NotFoundException constructor.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
class NotifyException extends \Exception
|
class NotifyException extends \Exception
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -11,7 +12,7 @@ class NotifyException extends \Exception
|
|||||||
* @param string $message
|
* @param string $message
|
||||||
* @param string $redirectLocation
|
* @param string $redirectLocation
|
||||||
*/
|
*/
|
||||||
public function __construct(string $message, string $redirectLocation = "/")
|
public function __construct($message, $redirectLocation)
|
||||||
{
|
{
|
||||||
$this->message = $message;
|
$this->message = $message;
|
||||||
$this->redirectLocation = $redirectLocation;
|
$this->redirectLocation = $redirectLocation;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class PermissionsException extends Exception
|
class PermissionsException extends Exception {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class PrettyException extends \Exception
|
class PrettyException extends \Exception {}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class SocialDriverNotConfigured extends PrettyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class SocialDriverNotConfigured extends PrettyException {}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
|
||||||
|
|
||||||
class SocialSignInAccountNotUsed extends SocialSignInException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class SocialSignInException extends NotifyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class SocialSignInException extends NotifyException {}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
<?php namespace BookStack\Exceptions;
|
||||||
|
|
||||||
class UserRegistrationException extends NotifyException
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
class UserRegistrationException extends NotifyException {}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
|
||||||
|
|
||||||
class UserTokenExpiredException extends \Exception {
|
|
||||||
|
|
||||||
public $userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UserTokenExpiredException constructor.
|
|
||||||
* @param string $message
|
|
||||||
* @param int $userId
|
|
||||||
*/
|
|
||||||
public function __construct(string $message, int $userId)
|
|
||||||
{
|
|
||||||
$this->userId = $userId;
|
|
||||||
parent::__construct($message);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
|
||||||
|
|
||||||
class UserTokenNotFoundException extends \Exception {}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<?php namespace BookStack\Exceptions;
|
|
||||||
|
|
||||||
class UserUpdateException extends NotifyException
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use BookStack\Exceptions\FileUploadException;
|
use BookStack\Exceptions\FileUploadException;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Attachment;
|
||||||
use BookStack\Uploads\Attachment;
|
use BookStack\Repos\EntityRepo;
|
||||||
use BookStack\Uploads\AttachmentService;
|
use BookStack\Services\AttachmentService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class AttachmentController extends Controller
|
class AttachmentController extends Controller
|
||||||
@@ -15,7 +14,7 @@ class AttachmentController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* AttachmentController constructor.
|
* AttachmentController constructor.
|
||||||
* @param \BookStack\Uploads\AttachmentService $attachmentService
|
* @param AttachmentService $attachmentService
|
||||||
* @param Attachment $attachment
|
* @param Attachment $attachment
|
||||||
* @param EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
*/
|
*/
|
||||||
@@ -103,7 +102,7 @@ class AttachmentController extends Controller
|
|||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
'name' => 'required|string|min:1|max:255',
|
'name' => 'required|string|min:1|max:255',
|
||||||
'link' => 'string|min:1|max:255'
|
'link' => 'url|min:1|max:255'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
$pageId = $request->get('uploaded_to');
|
||||||
@@ -131,7 +130,7 @@ class AttachmentController extends Controller
|
|||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'uploaded_to' => 'required|integer|exists:pages,id',
|
'uploaded_to' => 'required|integer|exists:pages,id',
|
||||||
'name' => 'required|string|min:1|max:255',
|
'name' => 'required|string|min:1|max:255',
|
||||||
'link' => 'required|string|min:1|max:255'
|
'link' => 'required|url|min:1|max:255'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$pageId = $request->get('uploaded_to');
|
$pageId = $request->get('uploaded_to');
|
||||||
@@ -183,17 +182,11 @@ class AttachmentController extends Controller
|
|||||||
* Get an attachment from storage.
|
* Get an attachment from storage.
|
||||||
* @param $attachmentId
|
* @param $attachmentId
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Symfony\Component\HttpFoundation\Response
|
||||||
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function get($attachmentId)
|
public function get($attachmentId)
|
||||||
{
|
{
|
||||||
$attachment = $this->attachment->findOrFail($attachmentId);
|
$attachment = $this->attachment->findOrFail($attachmentId);
|
||||||
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
$page = $this->entityRepo->getById('page', $attachment->uploaded_to);
|
||||||
if ($page === null) {
|
|
||||||
throw new NotFoundException(trans('errors.attachment_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
if ($attachment->external) {
|
if ($attachment->external) {
|
||||||
@@ -201,14 +194,16 @@ class AttachmentController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
$attachmentContents = $this->attachmentService->getAttachmentFromStorage($attachment);
|
||||||
return $this->downloadResponse($attachmentContents, $attachment->getFileName());
|
return response($attachmentContents, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="'. $attachment->getFileName() .'"'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete a specific attachment in the system.
|
* Delete a specific attachment in the system.
|
||||||
* @param $attachmentId
|
* @param $attachmentId
|
||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function delete($attachmentId)
|
public function delete($attachmentId)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,118 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use BookStack\Auth\Access\EmailConfirmationService;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Routing\Redirector;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class ConfirmEmailController extends Controller
|
|
||||||
{
|
|
||||||
protected $emailConfirmationService;
|
|
||||||
protected $userRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new controller instance.
|
|
||||||
*
|
|
||||||
* @param EmailConfirmationService $emailConfirmationService
|
|
||||||
* @param UserRepo $userRepo
|
|
||||||
*/
|
|
||||||
public function __construct(EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
|
||||||
{
|
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the page to tell the user to check their email
|
|
||||||
* and confirm their address.
|
|
||||||
*/
|
|
||||||
public function show()
|
|
||||||
{
|
|
||||||
return view('auth.register-confirm');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a notice that a user's email address has not been confirmed,
|
|
||||||
* Also has the option to re-send the confirmation email.
|
|
||||||
* @return View
|
|
||||||
*/
|
|
||||||
public function showAwaiting()
|
|
||||||
{
|
|
||||||
return view('auth.user-unconfirmed');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Confirms an email via a token and logs the user into the system.
|
|
||||||
* @param $token
|
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws ConfirmationEmailException
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function confirm($token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$userId = $this->emailConfirmationService->checkTokenAndGetUserId($token);
|
|
||||||
} catch (Exception $exception) {
|
|
||||||
|
|
||||||
if ($exception instanceof UserTokenNotFoundException) {
|
|
||||||
session()->flash('error', trans('errors.email_confirmation_invalid'));
|
|
||||||
return redirect('/register');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($exception instanceof UserTokenExpiredException) {
|
|
||||||
$user = $this->userRepo->getById($exception->userId);
|
|
||||||
$this->emailConfirmationService->sendConfirmation($user);
|
|
||||||
session()->flash('error', trans('errors.email_confirmation_expired'));
|
|
||||||
return redirect('/register/confirm');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->userRepo->getById($userId);
|
|
||||||
$user->email_confirmed = true;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
auth()->login($user);
|
|
||||||
session()->flash('success', trans('auth.email_confirm_success'));
|
|
||||||
$this->emailConfirmationService->deleteByUser($user);
|
|
||||||
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resend the confirmation email
|
|
||||||
* @param Request $request
|
|
||||||
* @return View
|
|
||||||
*/
|
|
||||||
public function resend(Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'email' => 'required|email|exists:users,email'
|
|
||||||
]);
|
|
||||||
$user = $this->userRepo->getByEmail($request->get('email'));
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->emailConfirmationService->sendConfirmation($user);
|
|
||||||
} catch (Exception $e) {
|
|
||||||
session()->flash('error', trans('auth.email_confirm_send_error'));
|
|
||||||
return redirect('/register/confirm');
|
|
||||||
}
|
|
||||||
|
|
||||||
session()->flash('success', trans('auth.email_confirm_resent'));
|
|
||||||
return redirect('/register/confirm');
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -64,4 +64,5 @@ class ForgotPasswordController extends Controller
|
|||||||
['email' => trans($response)]
|
['email' => trans($response)]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
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\Exceptions\AuthException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use BookStack\Repos\UserRepo;
|
||||||
|
use BookStack\Services\SocialAuthService;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
use Illuminate\Foundation\Auth\AuthenticatesUsers;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -37,24 +36,21 @@ class LoginController extends Controller
|
|||||||
protected $redirectAfterLogout = '/login';
|
protected $redirectAfterLogout = '/login';
|
||||||
|
|
||||||
protected $socialAuthService;
|
protected $socialAuthService;
|
||||||
protected $ldapService;
|
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
*
|
*
|
||||||
* @param \BookStack\Auth\\BookStack\Auth\Access\SocialAuthService $socialAuthService
|
* @param SocialAuthService $socialAuthService
|
||||||
* @param LdapService $ldapService
|
* @param UserRepo $userRepo
|
||||||
* @param \BookStack\Auth\UserRepo $userRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(SocialAuthService $socialAuthService, LdapService $ldapService, UserRepo $userRepo)
|
public function __construct(SocialAuthService $socialAuthService, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
$this->middleware('guest', ['only' => ['getLogin', 'postLogin']]);
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialAuthService = $socialAuthService;
|
||||||
$this->ldapService = $ldapService;
|
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->redirectPath = url('/');
|
$this->redirectPath = baseUrl('/');
|
||||||
$this->redirectAfterLogout = url('/login');
|
$this->redirectAfterLogout = baseUrl('/login');
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,26 +66,24 @@ class LoginController extends Controller
|
|||||||
* @param Authenticatable $user
|
* @param Authenticatable $user
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
* @return \Illuminate\Http\RedirectResponse
|
||||||
* @throws AuthException
|
* @throws AuthException
|
||||||
* @throws \BookStack\Exceptions\LdapException
|
|
||||||
*/
|
*/
|
||||||
protected function authenticated(Request $request, Authenticatable $user)
|
protected function authenticated(Request $request, Authenticatable $user)
|
||||||
{
|
{
|
||||||
// Explicitly log them out for now if they do no exist.
|
// Explicitly log them out for now if they do no exist.
|
||||||
if (!$user->exists) {
|
if (!$user->exists) auth()->logout($user);
|
||||||
auth()->logout($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$user->exists && $user->email === null && !$request->filled('email')) {
|
if (!$user->exists && $user->email === null && !$request->has('email')) {
|
||||||
$request->flash();
|
$request->flash();
|
||||||
session()->flash('request-email', true);
|
session()->flash('request-email', true);
|
||||||
return redirect('/login');
|
return redirect('/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$user->exists && $user->email === null && $request->filled('email')) {
|
if (!$user->exists && $user->email === null && $request->has('email')) {
|
||||||
$user->email = $request->get('email');
|
$user->email = $request->get('email');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$user->exists) {
|
if (!$user->exists) {
|
||||||
|
|
||||||
// Check for users with same email already
|
// Check for users with same email already
|
||||||
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
$alreadyUser = $user->newQuery()->where('email', '=', $user->email)->count() > 0;
|
||||||
if ($alreadyUser) {
|
if ($alreadyUser) {
|
||||||
@@ -101,39 +95,26 @@ class LoginController extends Controller
|
|||||||
auth()->login($user);
|
auth()->login($user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync LDAP groups if required
|
$path = session()->pull('url.intended', '/');
|
||||||
if ($this->ldapService->shouldSyncGroups()) {
|
$path = baseUrl($path, true);
|
||||||
$this->ldapService->syncGroups($user, $request->get($this->username()));
|
return redirect($path);
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->intended('/');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the application login form.
|
* Show the application login form.
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\Response
|
* @return \Illuminate\Http\Response
|
||||||
*/
|
*/
|
||||||
public function getLogin(Request $request)
|
public function getLogin()
|
||||||
{
|
{
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
|
return view('auth/login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
||||||
if ($request->has('email')) {
|
|
||||||
session()->flashInput([
|
|
||||||
'email' => $request->get('email'),
|
|
||||||
'password' => (config('app.env') === 'demo') ? $request->get('password', '') : ''
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('auth.login', ['socialDrivers' => $socialDrivers, 'authMethod' => $authMethod]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the relevant social site.
|
* Redirect to the relevant social site.
|
||||||
* @param $socialDriver
|
* @param $socialDriver
|
||||||
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
* @return \Symfony\Component\HttpFoundation\RedirectResponse
|
||||||
* @throws \BookStack\Exceptions\SocialDriverNotConfigured
|
|
||||||
*/
|
*/
|
||||||
public function getSocialLogin($socialDriver)
|
public function getSocialLogin($socialDriver)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,25 +2,20 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Http\Controllers\Auth;
|
||||||
|
|
||||||
use BookStack\Auth\Access\EmailConfirmationService;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
|
||||||
use BookStack\Auth\SocialAccount;
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
|
||||||
use BookStack\Exceptions\SocialSignInException;
|
use BookStack\Exceptions\SocialSignInException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Repos\UserRepo;
|
||||||
|
use BookStack\Services\EmailConfirmationService;
|
||||||
|
use BookStack\Services\SocialAuthService;
|
||||||
|
use BookStack\SocialAccount;
|
||||||
|
use BookStack\User;
|
||||||
use Exception;
|
use Exception;
|
||||||
use GuzzleHttp\Client;
|
|
||||||
use Illuminate\Foundation\Auth\RegistersUsers;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Illuminate\Routing\Redirector;
|
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
|
||||||
use Validator;
|
use Validator;
|
||||||
|
use BookStack\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Foundation\Auth\RegistersUsers;
|
||||||
|
|
||||||
class RegisterController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
@@ -58,12 +53,12 @@ class RegisterController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
public function __construct(SocialAuthService $socialAuthService, EmailConfirmationService $emailConfirmationService, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->middleware('guest')->only(['getRegister', 'postRegister', 'socialRegister']);
|
$this->middleware('guest')->except(['socialCallback', 'detachSocialAccount']);
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialAuthService = $socialAuthService;
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
$this->emailConfirmationService = $emailConfirmationService;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->redirectTo = url('/');
|
$this->redirectTo = baseUrl('/');
|
||||||
$this->redirectPath = url('/');
|
$this->redirectPath = baseUrl('/');
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,7 +71,7 @@ class RegisterController extends Controller
|
|||||||
protected function validator(array $data)
|
protected function validator(array $data)
|
||||||
{
|
{
|
||||||
return Validator::make($data, [
|
return Validator::make($data, [
|
||||||
'name' => 'required|min:2|max:255',
|
'name' => 'required|max:255',
|
||||||
'email' => 'required|email|max:255|unique:users',
|
'email' => 'required|email|max:255|unique:users',
|
||||||
'password' => 'required|min:6',
|
'password' => 'required|min:6',
|
||||||
]);
|
]);
|
||||||
@@ -96,7 +91,6 @@ class RegisterController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Show the application registration form.
|
* Show the application registration form.
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws UserRegistrationException
|
|
||||||
*/
|
*/
|
||||||
public function getRegister()
|
public function getRegister()
|
||||||
{
|
{
|
||||||
@@ -107,27 +101,20 @@ class RegisterController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle a registration request for the application.
|
* Handle a registration request for the application.
|
||||||
* @param Request|Request $request
|
* @param Request|\Illuminate\Http\Request $request
|
||||||
* @return RedirectResponse|Redirector
|
* @return Response
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
|
* @throws \Illuminate\Validation\ValidationException
|
||||||
*/
|
*/
|
||||||
public function postRegister(Request $request)
|
public function postRegister(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkRegistrationAllowed();
|
$this->checkRegistrationAllowed();
|
||||||
$this->validator($request->all())->validate();
|
$validator = $this->validator($request->all());
|
||||||
|
|
||||||
$captcha = $request->get('g-recaptcha-response');
|
if ($validator->fails()) {
|
||||||
$resp = (new Client())->post('https://www.google.com/recaptcha/api/siteverify', [
|
$this->throwValidationException(
|
||||||
'form_params' => [
|
$request, $validator
|
||||||
'response' => $captcha,
|
);
|
||||||
'secret' => '%%secret_key%%',
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
$respBody = json_decode($resp->getBody());
|
|
||||||
if (!$respBody->success) {
|
|
||||||
return redirect()->back()->withInput()->withErrors([
|
|
||||||
'g-recaptcha-response' => 'Did not pass captcha',
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$userData = $request->all();
|
$userData = $request->all();
|
||||||
@@ -152,28 +139,26 @@ class RegisterController extends Controller
|
|||||||
* The registrations flow for all users.
|
* The registrations flow for all users.
|
||||||
* @param array $userData
|
* @param array $userData
|
||||||
* @param bool|false|SocialAccount $socialAccount
|
* @param bool|false|SocialAccount $socialAccount
|
||||||
* @param bool $emailVerified
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
|
* @throws ConfirmationEmailException
|
||||||
*/
|
*/
|
||||||
protected function registerUser(array $userData, $socialAccount = false, $emailVerified = false)
|
protected function registerUser(array $userData, $socialAccount = false)
|
||||||
{
|
{
|
||||||
$registrationRestrict = setting('registration-restrict');
|
if (setting('registration-restrict')) {
|
||||||
|
$restrictedEmailDomains = explode(',', str_replace(' ', '', setting('registration-restrict')));
|
||||||
if ($registrationRestrict) {
|
$userEmailDomain = $domain = substr(strrchr($userData['email'], "@"), 1);
|
||||||
$restrictedEmailDomains = explode(',', str_replace(' ', '', $registrationRestrict));
|
|
||||||
$userEmailDomain = $domain = mb_substr(mb_strrchr($userData['email'], "@"), 1);
|
|
||||||
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
if (!in_array($userEmailDomain, $restrictedEmailDomains)) {
|
||||||
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
throw new UserRegistrationException(trans('auth.registration_email_domain_invalid'), '/register');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$newUser = $this->userRepo->registerNew($userData, $emailVerified);
|
$newUser = $this->userRepo->registerNew($userData);
|
||||||
if ($socialAccount) {
|
if ($socialAccount) {
|
||||||
$newUser->socialAccounts()->save($socialAccount);
|
$newUser->socialAccounts()->save($socialAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->emailConfirmationService->confirmationRequired() && !$emailVerified) {
|
if (setting('registration-confirmation') || setting('registration-restrict')) {
|
||||||
$newUser->save();
|
$newUser->save();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -190,12 +175,70 @@ class RegisterController extends Controller
|
|||||||
return redirect($this->redirectPath());
|
return redirect($this->redirectPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the page to tell the user to check their email
|
||||||
|
* and confirm their address.
|
||||||
|
*/
|
||||||
|
public function getRegisterConfirmation()
|
||||||
|
{
|
||||||
|
return view('auth/register-confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Confirms an email via a token and logs the user into the system.
|
||||||
|
* @param $token
|
||||||
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
|
* @throws UserRegistrationException
|
||||||
|
*/
|
||||||
|
public function confirmEmail($token)
|
||||||
|
{
|
||||||
|
$confirmation = $this->emailConfirmationService->getEmailConfirmationFromToken($token);
|
||||||
|
$user = $confirmation->user;
|
||||||
|
$user->email_confirmed = true;
|
||||||
|
$user->save();
|
||||||
|
auth()->login($user);
|
||||||
|
session()->flash('success', trans('auth.email_confirm_success'));
|
||||||
|
$this->emailConfirmationService->deleteConfirmationsByUser($user);
|
||||||
|
return redirect($this->redirectPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows a notice that a user's email address has not been confirmed,
|
||||||
|
* Also has the option to re-send the confirmation email.
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function showAwaitingConfirmation()
|
||||||
|
{
|
||||||
|
return view('auth/user-unconfirmed');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resend the confirmation email
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function resendConfirmation(Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'email' => 'required|email|exists:users,email'
|
||||||
|
]);
|
||||||
|
$user = $this->userRepo->getByEmail($request->get('email'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->emailConfirmationService->sendConfirmation($user);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
session()->flash('error', trans('auth.email_confirm_send_error'));
|
||||||
|
return redirect('/register/confirm');
|
||||||
|
}
|
||||||
|
|
||||||
|
session()->flash('success', trans('auth.email_confirm_resent'));
|
||||||
|
return redirect('/register/confirm');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Redirect to the social site for authentication intended to register.
|
* Redirect to the social site for authentication intended to register.
|
||||||
* @param $socialDriver
|
* @param $socialDriver
|
||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws UserRegistrationException
|
|
||||||
* @throws SocialDriverNotConfigured
|
|
||||||
*/
|
*/
|
||||||
public function socialRegister($socialDriver)
|
public function socialRegister($socialDriver)
|
||||||
{
|
{
|
||||||
@@ -207,52 +250,25 @@ class RegisterController extends Controller
|
|||||||
/**
|
/**
|
||||||
* The callback for social login services.
|
* The callback for social login services.
|
||||||
* @param $socialDriver
|
* @param $socialDriver
|
||||||
* @param Request $request
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws SocialSignInException
|
* @throws SocialSignInException
|
||||||
* @throws UserRegistrationException
|
|
||||||
* @throws SocialDriverNotConfigured
|
|
||||||
*/
|
*/
|
||||||
public function socialCallback($socialDriver, Request $request)
|
public function socialCallback($socialDriver)
|
||||||
{
|
{
|
||||||
if (!session()->has('social-callback')) {
|
if (!session()->has('social-callback')) {
|
||||||
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
throw new SocialSignInException(trans('errors.social_no_action_defined'), '/login');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check request for error information
|
|
||||||
if ($request->has('error') && $request->has('error_description')) {
|
|
||||||
throw new SocialSignInException(trans('errors.social_login_bad_response', [
|
|
||||||
'socialAccount' => $socialDriver,
|
|
||||||
'error' => $request->get('error_description'),
|
|
||||||
]), '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
$action = session()->pull('social-callback');
|
$action = session()->pull('social-callback');
|
||||||
|
if ($action == 'login') return $this->socialAuthService->handleLoginCallback($socialDriver);
|
||||||
// Attempt login or fall-back to register if allowed.
|
if ($action == 'register') return $this->socialRegisterCallback($socialDriver);
|
||||||
$socialUser = $this->socialAuthService->getSocialUser($socialDriver);
|
|
||||||
if ($action == 'login') {
|
|
||||||
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, $socialUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detach a social account from a user.
|
* Detach a social account from a user.
|
||||||
* @param $socialDriver
|
* @param $socialDriver
|
||||||
* @return RedirectResponse|Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
*/
|
*/
|
||||||
public function detachSocialAccount($socialDriver)
|
public function detachSocialAccount($socialDriver)
|
||||||
{
|
{
|
||||||
@@ -261,16 +277,14 @@ class RegisterController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a new user after a registration callback.
|
* Register a new user after a registration callback.
|
||||||
* @param string $socialDriver
|
* @param $socialDriver
|
||||||
* @param SocialUser $socialUser
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws UserRegistrationException
|
* @throws UserRegistrationException
|
||||||
*/
|
*/
|
||||||
protected function socialRegisterCallback(string $socialDriver, SocialUser $socialUser)
|
protected function socialRegisterCallback($socialDriver)
|
||||||
{
|
{
|
||||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver);
|
||||||
$socialAccount = $this->socialAuthService->fillSocialAccount($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
|
// Create an array of the user data to create a new user instance
|
||||||
$userData = [
|
$userData = [
|
||||||
@@ -278,6 +292,7 @@ class RegisterController extends Controller
|
|||||||
'email' => $socialUser->getEmail(),
|
'email' => $socialUser->getEmail(),
|
||||||
'password' => str_random(30)
|
'password' => str_random(30)
|
||||||
];
|
];
|
||||||
return $this->registerUser($userData, $socialAccount, $emailVerified);
|
return $this->registerUser($userData, $socialAccount);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
|
||||||
|
|
||||||
use BookStack\Auth\Access\UserInviteService;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
use Exception;
|
|
||||||
use Illuminate\Contracts\View\Factory;
|
|
||||||
use Illuminate\Http\RedirectResponse;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Routing\Redirector;
|
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class UserInviteController extends Controller
|
|
||||||
{
|
|
||||||
protected $inviteService;
|
|
||||||
protected $userRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new controller instance.
|
|
||||||
*
|
|
||||||
* @param UserInviteService $inviteService
|
|
||||||
* @param UserRepo $userRepo
|
|
||||||
*/
|
|
||||||
public function __construct(UserInviteService $inviteService, UserRepo $userRepo)
|
|
||||||
{
|
|
||||||
$this->inviteService = $inviteService;
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
$this->middleware('guest');
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the page for the user to set the password for their account.
|
|
||||||
* @param string $token
|
|
||||||
* @return Factory|View|RedirectResponse
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function showSetPassword(string $token)
|
|
||||||
{
|
|
||||||
try {
|
|
||||||
$this->inviteService->checkTokenAndGetUserId($token);
|
|
||||||
} catch (Exception $exception) {
|
|
||||||
return $this->handleTokenException($exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('auth.invite-set-password', [
|
|
||||||
'token' => $token,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets the password for an invited user and then grants them access.
|
|
||||||
* @param string $token
|
|
||||||
* @param Request $request
|
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
public function setPassword(string $token, Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'password' => 'required|min:6'
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$userId = $this->inviteService->checkTokenAndGetUserId($token);
|
|
||||||
} catch (Exception $exception) {
|
|
||||||
return $this->handleTokenException($exception);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->userRepo->getById($userId);
|
|
||||||
$user->password = bcrypt($request->get('password'));
|
|
||||||
$user->email_confirmed = true;
|
|
||||||
$user->save();
|
|
||||||
|
|
||||||
auth()->login($user);
|
|
||||||
session()->flash('success', trans('auth.user_invite_success', ['appName' => setting('app-name')]));
|
|
||||||
$this->inviteService->deleteByUser($user);
|
|
||||||
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check and validate the exception thrown when checking an invite token.
|
|
||||||
* @param Exception $exception
|
|
||||||
* @return RedirectResponse|Redirector
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
|
||||||
protected function handleTokenException(Exception $exception)
|
|
||||||
{
|
|
||||||
if ($exception instanceof UserTokenNotFoundException) {
|
|
||||||
return redirect('/');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($exception instanceof UserTokenExpiredException) {
|
|
||||||
session()->flash('error', trans('errors.invite_token_expired'));
|
|
||||||
return redirect('/password/email');
|
|
||||||
}
|
|
||||||
|
|
||||||
throw $exception;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Book;
|
||||||
use BookStack\Entities\Book;
|
use BookStack\Repos\EntityRepo;
|
||||||
use BookStack\Entities\EntityContextManager;
|
use BookStack\Repos\UserRepo;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Services\ExportService;
|
||||||
use BookStack\Entities\ExportService;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
@@ -17,29 +15,18 @@ class BookController extends Controller
|
|||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
protected $exportService;
|
protected $exportService;
|
||||||
protected $entityContextManager;
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* BookController constructor.
|
* BookController constructor.
|
||||||
* @param EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
* @param ExportService $exportService
|
* @param ExportService $exportService
|
||||||
* @param EntityContextManager $entityContextManager
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||||
EntityRepo $entityRepo,
|
{
|
||||||
UserRepo $userRepo,
|
|
||||||
ExportService $exportService,
|
|
||||||
EntityContextManager $entityContextManager,
|
|
||||||
ImageRepo $imageRepo
|
|
||||||
) {
|
|
||||||
$this->entityRepo = $entityRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->exportService = $exportService;
|
$this->exportService = $exportService;
|
||||||
$this->entityContextManager = $entityContextManager;
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,117 +36,65 @@ class BookController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$view = setting()->getUser($this->currentUser, 'books_view_type', config('app.views.books'));
|
$books = $this->entityRepo->getAllPaginated('book', 20);
|
||||||
$sort = setting()->getUser($this->currentUser, 'books_sort', 'name');
|
|
||||||
$order = setting()->getUser($this->currentUser, 'books_sort_order', 'asc');
|
|
||||||
$sortOptions = [
|
|
||||||
'name' => trans('common.sort_name'),
|
|
||||||
'created_at' => trans('common.sort_created_at'),
|
|
||||||
'updated_at' => trans('common.sort_updated_at'),
|
|
||||||
];
|
|
||||||
|
|
||||||
$books = $this->entityRepo->getAllPaginated('book', 18, $sort, $order);
|
|
||||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('book', 4, 0) : false;
|
||||||
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
$popular = $this->entityRepo->getPopular('book', 4, 0);
|
||||||
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
$new = $this->entityRepo->getRecentlyCreated('book', 4, 0);
|
||||||
|
$this->setPageTitle('Books');
|
||||||
$this->entityContextManager->clearShelfContext();
|
return view('books/index', [
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.books'));
|
|
||||||
return view('books.index', [
|
|
||||||
'books' => $books,
|
'books' => $books,
|
||||||
'recents' => $recents,
|
'recents' => $recents,
|
||||||
'popular' => $popular,
|
'popular' => $popular,
|
||||||
'new' => $new,
|
'new' => $new
|
||||||
'view' => $view,
|
|
||||||
'sort' => $sort,
|
|
||||||
'order' => $order,
|
|
||||||
'sortOptions' => $sortOptions,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the form for creating a new book.
|
* Show the form for creating a new book.
|
||||||
* @param string $shelfSlug
|
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function create(string $shelfSlug = null)
|
public function create()
|
||||||
{
|
{
|
||||||
$bookshelf = null;
|
|
||||||
if ($shelfSlug !== null) {
|
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
|
|
||||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->checkPermission('book-create-all');
|
$this->checkPermission('book-create-all');
|
||||||
$this->setPageTitle(trans('entities.books_create'));
|
$this->setPageTitle(trans('entities.books_create'));
|
||||||
return view('books.create', [
|
return view('books/create');
|
||||||
'bookshelf' => $bookshelf
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created book in storage.
|
* Store a newly created book in storage.
|
||||||
*
|
*
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param string $shelfSlug
|
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
*/
|
||||||
public function store(Request $request, string $shelfSlug = null)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
$this->checkPermission('book-create-all');
|
$this->checkPermission('book-create-all');
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000'
|
||||||
'image' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$bookshelf = null;
|
|
||||||
if ($shelfSlug !== null) {
|
|
||||||
$bookshelf = $this->entityRepo->getBySlug('bookshelf', $shelfSlug);
|
|
||||||
$this->checkOwnablePermission('bookshelf-update', $bookshelf);
|
|
||||||
}
|
|
||||||
|
|
||||||
$book = $this->entityRepo->createFromInput('book', $request->all());
|
$book = $this->entityRepo->createFromInput('book', $request->all());
|
||||||
$this->bookUpdateActions($book, $request);
|
|
||||||
Activity::add($book, 'book_create', $book->id);
|
Activity::add($book, 'book_create', $book->id);
|
||||||
|
|
||||||
if ($bookshelf) {
|
|
||||||
$this->entityRepo->appendBookToShelf($bookshelf, $book);
|
|
||||||
Activity::add($bookshelf, 'bookshelf_update');
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the specified book.
|
* Display the specified book.
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @param Request $request
|
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function show($slug, Request $request)
|
public function show($slug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-view', $book);
|
$this->checkOwnablePermission('book-view', $book);
|
||||||
|
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||||
|
|
||||||
Views::add($book);
|
Views::add($book);
|
||||||
if ($request->has('shelf')) {
|
|
||||||
$this->entityContextManager->setShelfContext(intval($request->get('shelf')));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->setPageTitle($book->getShortName());
|
$this->setPageTitle($book->getShortName());
|
||||||
return view('books.show', [
|
return view('books/show', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'current' => $book,
|
'current' => $book,
|
||||||
'bookChildren' => $bookChildren,
|
'bookChildren' => $bookChildren,
|
||||||
'activity' => Activity::entityActivity($book, 20, 1)
|
'activity' => Activity::entityActivity($book, 20, 0)
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,34 +107,27 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->setPageTitle(trans('entities.books_edit_named', ['bookName'=>$book->getShortName()]));
|
$this->setPageTitle(trans('entities.books_edit_named',['bookName'=>$book->getShortName()]));
|
||||||
return view('books.edit', ['book' => $book, 'current' => $book]);
|
return view('books/edit', ['book' => $book, 'current' => $book]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified book in storage.
|
* Update the specified book in storage.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param $slug
|
* @param $slug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, string $slug)
|
public function update(Request $request, $slug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $slug);
|
$book = $this->entityRepo->getBySlug('book', $slug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255',
|
'name' => 'required|string|max:255',
|
||||||
'description' => 'string|max:1000',
|
'description' => 'string|max:1000'
|
||||||
'image' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
]);
|
||||||
|
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
||||||
$book = $this->entityRepo->updateFromInput('book', $book, $request->all());
|
Activity::add($book, 'book_update', $book->id);
|
||||||
$this->bookUpdateActions($book, $request);
|
return redirect($book->getUrl());
|
||||||
|
|
||||||
Activity::add($book, 'book_update', $book->id);
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -212,24 +140,22 @@ class BookController extends Controller
|
|||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
|
$this->setPageTitle(trans('entities.books_delete_named', ['bookName'=>$book->getShortName()]));
|
||||||
return view('books.delete', ['book' => $book, 'current' => $book]);
|
return view('books/delete', ['book' => $book, 'current' => $book]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the view which allows pages to be re-ordered and sorted.
|
* Shows the view which allows pages to be re-ordered and sorted.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function sort($bookSlug)
|
public function sort($bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
$bookChildren = $this->entityRepo->getBookChildren($book, true);
|
||||||
|
$books = $this->entityRepo->getAll('book', false);
|
||||||
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
$this->setPageTitle(trans('entities.books_sort_named', ['bookName'=>$book->getShortName()]));
|
||||||
return view('books.sort', ['book' => $book, 'current' => $book, 'bookChildren' => $bookChildren]);
|
return view('books/sort', ['book' => $book, 'current' => $book, 'books' => $books, 'bookChildren' => $bookChildren]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -242,7 +168,7 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$bookChildren = $this->entityRepo->getBookChildren($book);
|
$bookChildren = $this->entityRepo->getBookChildren($book);
|
||||||
return view('books.sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
return view('books/sort-box', ['book' => $book, 'bookChildren' => $bookChildren]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -257,61 +183,47 @@ class BookController extends Controller
|
|||||||
$this->checkOwnablePermission('book-update', $book);
|
$this->checkOwnablePermission('book-update', $book);
|
||||||
|
|
||||||
// Return if no map sent
|
// Return if no map sent
|
||||||
if (!$request->filled('sort-tree')) {
|
if (!$request->has('sort-tree')) {
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort pages and chapters
|
// Sort pages and chapters
|
||||||
$sortMap = collect(json_decode($request->get('sort-tree')));
|
$sortedBooks = [];
|
||||||
$bookIdsInvolved = collect([$book->id]);
|
$updatedModels = collect();
|
||||||
|
$sortMap = json_decode($request->get('sort-tree'));
|
||||||
|
$defaultBookId = $book->id;
|
||||||
|
|
||||||
// Load models into map
|
// Loop through contents of provided map and update entities accordingly
|
||||||
$sortMap->each(function ($mapItem) use ($bookIdsInvolved) {
|
foreach ($sortMap as $bookChild) {
|
||||||
$mapItem->type = ($mapItem->type === 'page' ? 'page' : 'chapter');
|
$priority = $bookChild->sort;
|
||||||
$mapItem->model = $this->entityRepo->getById($mapItem->type, $mapItem->id);
|
$id = intval($bookChild->id);
|
||||||
// Store source and target books
|
$isPage = $bookChild->type == 'page';
|
||||||
$bookIdsInvolved->push(intval($mapItem->model->book_id));
|
$bookId = $this->entityRepo->exists('book', $bookChild->book) ? intval($bookChild->book) : $defaultBookId;
|
||||||
$bookIdsInvolved->push(intval($mapItem->book));
|
$chapterId = ($isPage && $bookChild->parentChapter === false) ? 0 : intval($bookChild->parentChapter);
|
||||||
});
|
$model = $this->entityRepo->getById($isPage?'page':'chapter', $id);
|
||||||
|
|
||||||
// Get the books involved in the sort
|
// Update models only if there's a change in parent chain or ordering.
|
||||||
$bookIdsInvolved = $bookIdsInvolved->unique()->toArray();
|
if ($model->priority !== $priority || $model->book_id !== $bookId || ($isPage && $model->chapter_id !== $chapterId)) {
|
||||||
$booksInvolved = $this->entityRepo->getManyById('book', $bookIdsInvolved, false, true);
|
$this->entityRepo->changeBook($isPage?'page':'chapter', $bookId, $model);
|
||||||
// Throw permission error if invalid ids or inaccessible books given.
|
$model->priority = $priority;
|
||||||
if (count($bookIdsInvolved) !== count($booksInvolved)) {
|
if ($isPage) $model->chapter_id = $chapterId;
|
||||||
$this->showPermissionError();
|
$model->save();
|
||||||
|
$updatedModels->push($model);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store involved books to be sorted later
|
||||||
|
if (!in_array($bookId, $sortedBooks)) {
|
||||||
|
$sortedBooks[] = $bookId;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Check permissions of involved books
|
|
||||||
$booksInvolved->each(function (Book $book) {
|
|
||||||
$this->checkOwnablePermission('book-update', $book);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Perform the sort
|
// Add activity for books
|
||||||
$sortMap->each(function ($mapItem) {
|
foreach ($sortedBooks as $bookId) {
|
||||||
$model = $mapItem->model;
|
/** @var Book $updatedBook */
|
||||||
|
$updatedBook = $this->entityRepo->getById('book', $bookId);
|
||||||
$priorityChanged = intval($model->priority) !== intval($mapItem->sort);
|
$this->entityRepo->buildJointPermissionsForBook($updatedBook);
|
||||||
$bookChanged = intval($model->book_id) !== intval($mapItem->book);
|
Activity::add($updatedBook, 'book_sort', $updatedBook->id);
|
||||||
$chapterChanged = ($mapItem->type === 'page') && intval($model->chapter_id) !== $mapItem->parentChapter;
|
}
|
||||||
|
|
||||||
if ($bookChanged) {
|
|
||||||
$this->entityRepo->changeBook($mapItem->type, $mapItem->book, $model);
|
|
||||||
}
|
|
||||||
if ($chapterChanged) {
|
|
||||||
$model->chapter_id = intval($mapItem->parentChapter);
|
|
||||||
$model->save();
|
|
||||||
}
|
|
||||||
if ($priorityChanged) {
|
|
||||||
$model->priority = intval($mapItem->sort);
|
|
||||||
$model->save();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Rebuild permissions and add activity for involved books.
|
|
||||||
$booksInvolved->each(function (Book $book) {
|
|
||||||
$this->entityRepo->buildJointPermissionsForBook($book);
|
|
||||||
Activity::add($book, 'book_sort', $book->id);
|
|
||||||
});
|
|
||||||
|
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
@@ -326,12 +238,7 @@ class BookController extends Controller
|
|||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('book-delete', $book);
|
$this->checkOwnablePermission('book-delete', $book);
|
||||||
Activity::addMessage('book_delete', 0, $book->name);
|
Activity::addMessage('book_delete', 0, $book->name);
|
||||||
|
|
||||||
if ($book->cover) {
|
|
||||||
$this->imageRepo->destroyImage($book->cover);
|
|
||||||
}
|
|
||||||
$this->entityRepo->destroyBook($book);
|
$this->entityRepo->destroyBook($book);
|
||||||
|
|
||||||
return redirect('/books');
|
return redirect('/books');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,12 +247,12 @@ class BookController extends Controller
|
|||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
*/
|
*/
|
||||||
public function showPermissions($bookSlug)
|
public function showRestrict($bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
return view('books.permissions', [
|
return view('books/restrictions', [
|
||||||
'book' => $book,
|
'book' => $book,
|
||||||
'roles' => $roles
|
'roles' => $roles
|
||||||
]);
|
]);
|
||||||
@@ -354,12 +261,11 @@ class BookController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Set the restrictions for this book.
|
* Set the restrictions for this book.
|
||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
|
* @param $bookSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
*/
|
||||||
public function permissions($bookSlug, Request $request)
|
public function restrict($bookSlug, Request $request)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $book);
|
$this->checkOwnablePermission('restrictions-manage', $book);
|
||||||
@@ -377,7 +283,10 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$pdfContent = $this->exportService->bookToPdf($book);
|
$pdfContent = $this->exportService->bookToPdf($book);
|
||||||
return $this->downloadResponse($pdfContent, $bookSlug . '.pdf');
|
return response()->make($pdfContent, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.pdf'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -389,7 +298,10 @@ class BookController extends Controller
|
|||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
$htmlContent = $this->exportService->bookToContainedHtml($book);
|
||||||
return $this->downloadResponse($htmlContent, $bookSlug . '.html');
|
return response()->make($htmlContent, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.html'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -400,32 +312,10 @@ class BookController extends Controller
|
|||||||
public function exportPlainText($bookSlug)
|
public function exportPlainText($bookSlug)
|
||||||
{
|
{
|
||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$textContent = $this->exportService->bookToPlainText($book);
|
$htmlContent = $this->exportService->bookToPlainText($book);
|
||||||
return $this->downloadResponse($textContent, $bookSlug . '.txt');
|
return response()->make($htmlContent, 200, [
|
||||||
}
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $bookSlug . '.txt'
|
||||||
/**
|
]);
|
||||||
* Common actions to run on book update.
|
|
||||||
* Handles updating the cover image.
|
|
||||||
* @param Book $book
|
|
||||||
* @param Request $request
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
|
||||||
protected function bookUpdateActions(Book $book, Request $request)
|
|
||||||
{
|
|
||||||
// Update the cover image if in request
|
|
||||||
if ($request->has('image')) {
|
|
||||||
$this->imageRepo->destroyImage($book->cover);
|
|
||||||
$newImage = $request->file('image');
|
|
||||||
$image = $this->imageRepo->saveNew($newImage, 'cover_book', $book->id, 512, 512, true);
|
|
||||||
$book->image_id = $image->id;
|
|
||||||
$book->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->has('image_reset')) {
|
|
||||||
$this->imageRepo->destroyImage($book->cover);
|
|
||||||
$book->image_id = 0;
|
|
||||||
$book->save();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,303 +0,0 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use Activity;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Entities\Bookshelf;
|
|
||||||
use BookStack\Entities\EntityContextManager;
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Http\Response;
|
|
||||||
use Views;
|
|
||||||
|
|
||||||
class BookshelfController extends Controller
|
|
||||||
{
|
|
||||||
|
|
||||||
protected $entityRepo;
|
|
||||||
protected $userRepo;
|
|
||||||
protected $entityContextManager;
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* BookController constructor.
|
|
||||||
* @param EntityRepo $entityRepo
|
|
||||||
* @param UserRepo $userRepo
|
|
||||||
* @param EntityContextManager $entityContextManager
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, EntityContextManager $entityContextManager, ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->entityRepo = $entityRepo;
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
$this->entityContextManager = $entityContextManager;
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display a listing of the book.
|
|
||||||
* @return Response
|
|
||||||
*/
|
|
||||||
public function index()
|
|
||||||
{
|
|
||||||
$view = setting()->getUser($this->currentUser, 'bookshelves_view_type', config('app.views.bookshelves', 'grid'));
|
|
||||||
$sort = setting()->getUser($this->currentUser, 'bookshelves_sort', 'name');
|
|
||||||
$order = setting()->getUser($this->currentUser, 'bookshelves_sort_order', 'asc');
|
|
||||||
$sortOptions = [
|
|
||||||
'name' => trans('common.sort_name'),
|
|
||||||
'created_at' => trans('common.sort_created_at'),
|
|
||||||
'updated_at' => trans('common.sort_updated_at'),
|
|
||||||
];
|
|
||||||
|
|
||||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $sort, $order);
|
|
||||||
foreach ($shelves as $shelf) {
|
|
||||||
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
|
|
||||||
}
|
|
||||||
|
|
||||||
$recents = $this->signedIn ? $this->entityRepo->getRecentlyViewed('bookshelf', 4, 0) : false;
|
|
||||||
$popular = $this->entityRepo->getPopular('bookshelf', 4, 0);
|
|
||||||
$new = $this->entityRepo->getRecentlyCreated('bookshelf', 4, 0);
|
|
||||||
|
|
||||||
$this->entityContextManager->clearShelfContext();
|
|
||||||
$this->setPageTitle(trans('entities.shelves'));
|
|
||||||
return view('shelves.index', [
|
|
||||||
'shelves' => $shelves,
|
|
||||||
'recents' => $recents,
|
|
||||||
'popular' => $popular,
|
|
||||||
'new' => $new,
|
|
||||||
'view' => $view,
|
|
||||||
'sort' => $sort,
|
|
||||||
'order' => $order,
|
|
||||||
'sortOptions' => $sortOptions,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
|
||||||
public function store(Request $request)
|
|
||||||
{
|
|
||||||
$this->checkPermission('bookshelf-create-all');
|
|
||||||
$this->validate($request, [
|
|
||||||
'name' => 'required|string|max:255',
|
|
||||||
'description' => 'string|max:1000',
|
|
||||||
'image' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$shelf = $this->entityRepo->createFromInput('bookshelf', $request->all());
|
|
||||||
$this->shelfUpdateActions($shelf, $request);
|
|
||||||
|
|
||||||
Activity::add($shelf, 'bookshelf_create');
|
|
||||||
return redirect($shelf->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display the specified bookshelf.
|
|
||||||
* @param String $slug
|
|
||||||
* @return Response
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
|
||||||
public function show(string $slug)
|
|
||||||
{
|
|
||||||
/** @var Bookshelf $shelf */
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
|
||||||
$this->checkOwnablePermission('book-view', $shelf);
|
|
||||||
|
|
||||||
$books = $this->entityRepo->getBookshelfChildren($shelf);
|
|
||||||
Views::add($shelf);
|
|
||||||
$this->entityContextManager->setShelfContext($shelf->id);
|
|
||||||
|
|
||||||
$this->setPageTitle($shelf->getShortName());
|
|
||||||
|
|
||||||
return view('shelves.show', [
|
|
||||||
'shelf' => $shelf,
|
|
||||||
'books' => $books,
|
|
||||||
'activity' => Activity::entityActivity($shelf, 20, 1)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the form for editing the specified bookshelf.
|
|
||||||
* @param $slug
|
|
||||||
* @return Response
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
|
||||||
public function edit(string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
|
||||||
$this->checkOwnablePermission('bookshelf-update', $shelf);
|
|
||||||
|
|
||||||
$shelfBooks = $this->entityRepo->getBookshelfChildren($shelf);
|
|
||||||
$shelfBookIds = $shelfBooks->pluck('id');
|
|
||||||
$books = $this->entityRepo->getAll('book', false, 'update');
|
|
||||||
$books = $books->filter(function ($book) use ($shelfBookIds) {
|
|
||||||
return !$shelfBookIds->contains($book->id);
|
|
||||||
});
|
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.shelves_edit_named', ['name' => $shelf->getShortName()]));
|
|
||||||
return view('shelves.edit', [
|
|
||||||
'shelf' => $shelf,
|
|
||||||
'books' => $books,
|
|
||||||
'shelfBooks' => $shelfBooks,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the specified bookshelf in storage.
|
|
||||||
* @param Request $request
|
|
||||||
* @param string $slug
|
|
||||||
* @return Response
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
|
||||||
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',
|
|
||||||
'image' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
$shelf = $this->entityRepo->updateFromInput('bookshelf', $shelf, $request->all());
|
|
||||||
$this->shelfUpdateActions($shelf, $request);
|
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
|
||||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
|
||||||
|
|
||||||
$this->setPageTitle(trans('entities.shelves_delete_named', ['name' => $shelf->getShortName()]));
|
|
||||||
return view('shelves.delete', ['shelf' => $shelf]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove the specified bookshelf from storage.
|
|
||||||
* @param string $slug
|
|
||||||
* @return Response
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
|
||||||
public function destroy(string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug); /** @var $shelf Bookshelf */
|
|
||||||
$this->checkOwnablePermission('bookshelf-delete', $shelf);
|
|
||||||
Activity::addMessage('bookshelf_delete', 0, $shelf->name);
|
|
||||||
|
|
||||||
if ($shelf->cover) {
|
|
||||||
$this->imageRepo->destroyImage($shelf->cover);
|
|
||||||
}
|
|
||||||
$this->entityRepo->destroyBookshelf($shelf);
|
|
||||||
|
|
||||||
return redirect('/shelves');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the permissions view.
|
|
||||||
* @param string $slug
|
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
|
||||||
public function showPermissions(string $slug)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
|
||||||
return view('shelves.permissions', [
|
|
||||||
'shelf' => $shelf,
|
|
||||||
'roles' => $roles
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the permissions for this bookshelf.
|
|
||||||
* @param string $slug
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
|
||||||
public function permissions(string $slug, Request $request)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
$this->entityRepo->updateEntityPermissionsFromRequest($request, $shelf);
|
|
||||||
session()->flash('success', trans('entities.shelves_permissions_updated'));
|
|
||||||
return redirect($shelf->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)
|
|
||||||
{
|
|
||||||
$shelf = $this->entityRepo->getBySlug('bookshelf', $slug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $shelf);
|
|
||||||
|
|
||||||
$updateCount = $this->entityRepo->copyBookshelfPermissions($shelf);
|
|
||||||
session()->flash('success', trans('entities.shelves_copy_permission_success', ['count' => $updateCount]));
|
|
||||||
return redirect($shelf->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Common actions to run on bookshelf update.
|
|
||||||
* @param Bookshelf $shelf
|
|
||||||
* @param Request $request
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
|
||||||
protected function shelfUpdateActions(Bookshelf $shelf, Request $request)
|
|
||||||
{
|
|
||||||
// Update the books that the shelf references
|
|
||||||
$this->entityRepo->updateShelfBooks($shelf, $request->get('books', ''));
|
|
||||||
|
|
||||||
// Update the cover image if in request
|
|
||||||
if ($request->has('image')) {
|
|
||||||
$newImage = $request->file('image');
|
|
||||||
$this->imageRepo->destroyImage($shelf->cover);
|
|
||||||
$image = $this->imageRepo->saveNew($newImage, 'cover_shelf', $shelf->id, 512, 512, true);
|
|
||||||
$shelf->image_id = $image->id;
|
|
||||||
$shelf->save();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->has('image_reset')) {
|
|
||||||
$this->imageRepo->destroyImage($shelf->cover);
|
|
||||||
$shelf->image_id = 0;
|
|
||||||
$shelf->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Auth\UserRepo;
|
use BookStack\Repos\EntityRepo;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Repos\UserRepo;
|
||||||
use BookStack\Entities\ExportService;
|
use BookStack\Services\ExportService;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
@@ -19,7 +19,7 @@ class ChapterController extends Controller
|
|||||||
* ChapterController constructor.
|
* ChapterController constructor.
|
||||||
* @param EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
* @param \BookStack\Entities\ExportService $exportService
|
* @param ExportService $exportService
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
public function __construct(EntityRepo $entityRepo, UserRepo $userRepo, ExportService $exportService)
|
||||||
{
|
{
|
||||||
@@ -39,7 +39,7 @@ class ChapterController extends Controller
|
|||||||
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-create', $book);
|
$this->checkOwnablePermission('chapter-create', $book);
|
||||||
$this->setPageTitle(trans('entities.chapters_create'));
|
$this->setPageTitle(trans('entities.chapters_create'));
|
||||||
return view('chapters.create', ['book' => $book, 'current' => $book]);
|
return view('chapters/create', ['book' => $book, 'current' => $book]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +78,7 @@ class ChapterController extends Controller
|
|||||||
Views::add($chapter);
|
Views::add($chapter);
|
||||||
$this->setPageTitle($chapter->getShortName());
|
$this->setPageTitle($chapter->getShortName());
|
||||||
$pages = $this->entityRepo->getChapterChildren($chapter);
|
$pages = $this->entityRepo->getChapterChildren($chapter);
|
||||||
return view('chapters.show', [
|
return view('chapters/show', [
|
||||||
'book' => $chapter->book,
|
'book' => $chapter->book,
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'current' => $chapter,
|
'current' => $chapter,
|
||||||
@@ -98,7 +98,7 @@ class ChapterController extends Controller
|
|||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_edit_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
return view('chapters.edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
return view('chapters/edit', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -107,14 +107,17 @@ class ChapterController extends Controller
|
|||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @param $chapterSlug
|
* @param $chapterSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $bookSlug, $chapterSlug)
|
public function update(Request $request, $bookSlug, $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
|
if ($chapter->name !== $request->get('name')) {
|
||||||
$this->entityRepo->updateFromInput('chapter', $chapter, $request->all());
|
$chapter->slug = $this->entityRepo->findSuitableSlug('chapter', $request->get('name'), $chapter->id, $chapter->book->id);
|
||||||
|
}
|
||||||
|
$chapter->fill($request->all());
|
||||||
|
$chapter->updated_by = user()->id;
|
||||||
|
$chapter->save();
|
||||||
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
Activity::add($chapter, 'chapter_update', $chapter->book->id);
|
||||||
return redirect($chapter->getUrl());
|
return redirect($chapter->getUrl());
|
||||||
}
|
}
|
||||||
@@ -130,7 +133,7 @@ class ChapterController extends Controller
|
|||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
$this->checkOwnablePermission('chapter-delete', $chapter);
|
||||||
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_delete_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
return view('chapters.delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
return view('chapters/delete', ['book' => $chapter->book, 'chapter' => $chapter, 'current' => $chapter]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -156,13 +159,11 @@ class ChapterController extends Controller
|
|||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function showMove($bookSlug, $chapterSlug)
|
public function showMove($bookSlug, $chapterSlug) {
|
||||||
{
|
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
$this->setPageTitle(trans('entities.chapters_move_named', ['chapterName' => $chapter->getShortName()]));
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
return view('chapters/move', [
|
||||||
return view('chapters.move', [
|
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'book' => $chapter->book
|
'book' => $chapter->book
|
||||||
]);
|
]);
|
||||||
@@ -176,11 +177,9 @@ class ChapterController extends Controller
|
|||||||
* @return mixed
|
* @return mixed
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
* @throws \BookStack\Exceptions\NotFoundException
|
||||||
*/
|
*/
|
||||||
public function move($bookSlug, $chapterSlug, Request $request)
|
public function move($bookSlug, $chapterSlug, Request $request) {
|
||||||
{
|
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('chapter-update', $chapter);
|
$this->checkOwnablePermission('chapter-update', $chapter);
|
||||||
$this->checkOwnablePermission('chapter-delete', $chapter);
|
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->get('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
@@ -214,14 +213,13 @@ class ChapterController extends Controller
|
|||||||
* @param $bookSlug
|
* @param $bookSlug
|
||||||
* @param $chapterSlug
|
* @param $chapterSlug
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function showPermissions($bookSlug, $chapterSlug)
|
public function showRestrict($bookSlug, $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
return view('chapters.permissions', [
|
return view('chapters/restrictions', [
|
||||||
'chapter' => $chapter,
|
'chapter' => $chapter,
|
||||||
'roles' => $roles
|
'roles' => $roles
|
||||||
]);
|
]);
|
||||||
@@ -233,10 +231,8 @@ class ChapterController extends Controller
|
|||||||
* @param $chapterSlug
|
* @param $chapterSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws \BookStack\Exceptions\NotFoundException
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
*/
|
||||||
public function permissions($bookSlug, $chapterSlug, Request $request)
|
public function restrict($bookSlug, $chapterSlug, Request $request)
|
||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
$this->checkOwnablePermission('restrictions-manage', $chapter);
|
||||||
@@ -255,7 +251,10 @@ class ChapterController extends Controller
|
|||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
$pdfContent = $this->exportService->chapterToPdf($chapter);
|
||||||
return $this->downloadResponse($pdfContent, $chapterSlug . '.pdf');
|
return response()->make($pdfContent, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.pdf'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -268,7 +267,10 @@ class ChapterController extends Controller
|
|||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
$containedHtml = $this->exportService->chapterToContainedHtml($chapter);
|
||||||
return $this->downloadResponse($containedHtml, $chapterSlug . '.html');
|
return response()->make($containedHtml, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.html'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -280,7 +282,10 @@ class ChapterController extends Controller
|
|||||||
public function exportPlainText($bookSlug, $chapterSlug)
|
public function exportPlainText($bookSlug, $chapterSlug)
|
||||||
{
|
{
|
||||||
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
||||||
$chapterText = $this->exportService->chapterToPlainText($chapter);
|
$containedHtml = $this->exportService->chapterToPlainText($chapter);
|
||||||
return $this->downloadResponse($chapterText, $chapterSlug . '.txt');
|
return response()->make($containedHtml, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $chapterSlug . '.txt'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Actions\CommentRepo;
|
use BookStack\Repos\CommentRepo;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Repos\EntityRepo;
|
||||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ class CommentController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* CommentController constructor.
|
* CommentController constructor.
|
||||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param \BookStack\Actions\CommentRepo $commentRepo
|
* @param CommentRepo $commentRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
public function __construct(EntityRepo $entityRepo, CommentRepo $commentRepo)
|
||||||
{
|
{
|
||||||
@@ -54,7 +54,7 @@ class CommentController extends Controller
|
|||||||
$this->checkPermission('comment-create-all');
|
$this->checkPermission('comment-create-all');
|
||||||
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
$comment = $this->commentRepo->create($page, $request->only(['html', 'text', 'parent_id']));
|
||||||
Activity::add($page, 'commented_on', $page->book->id);
|
Activity::add($page, 'commented_on', $page->book->id);
|
||||||
return view('comments.comment', ['comment' => $comment]);
|
return view('comments/comment', ['comment' => $comment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -75,7 +75,7 @@ class CommentController extends Controller
|
|||||||
$this->checkOwnablePermission('comment-update', $comment);
|
$this->checkOwnablePermission('comment-update', $comment);
|
||||||
|
|
||||||
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
$comment = $this->commentRepo->update($comment, $request->only(['html', 'text']));
|
||||||
return view('comments.comment', ['comment' => $comment]);
|
return view('comments/comment', ['comment' => $comment]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Ownable;
|
use BookStack\Ownable;
|
||||||
use Illuminate\Foundation\Bus\DispatchesJobs;
|
use Illuminate\Foundation\Bus\DispatchesJobs;
|
||||||
use Illuminate\Foundation\Validation\ValidatesRequests;
|
|
||||||
use Illuminate\Http\Exceptions\HttpResponseException;
|
use Illuminate\Http\Exceptions\HttpResponseException;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Routing\Controller as BaseController;
|
use Illuminate\Routing\Controller as BaseController;
|
||||||
|
use Illuminate\Foundation\Validation\ValidatesRequests;
|
||||||
|
use BookStack\User;
|
||||||
|
|
||||||
abstract class Controller extends BaseController
|
abstract class Controller extends BaseController
|
||||||
{
|
{
|
||||||
@@ -51,9 +51,7 @@ abstract class Controller extends BaseController
|
|||||||
*/
|
*/
|
||||||
protected function preventAccessForDemoUsers()
|
protected function preventAccessForDemoUsers()
|
||||||
{
|
{
|
||||||
if (config('app.env') === 'demo') {
|
if (config('app.env') === 'demo') $this->showPermissionError();
|
||||||
$this->showPermissionError();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -102,9 +100,7 @@ abstract class Controller extends BaseController
|
|||||||
*/
|
*/
|
||||||
protected function checkOwnablePermission($permission, Ownable $ownable)
|
protected function checkOwnablePermission($permission, Ownable $ownable)
|
||||||
{
|
{
|
||||||
if (userCan($permission, $ownable)) {
|
if (userCan($permission, $ownable)) return true;
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return $this->showPermissionError();
|
return $this->showPermissionError();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,26 +113,10 @@ abstract class Controller extends BaseController
|
|||||||
protected function checkPermissionOr($permissionName, $callback)
|
protected function checkPermissionOr($permissionName, $callback)
|
||||||
{
|
{
|
||||||
$callbackResult = $callback();
|
$callbackResult = $callback();
|
||||||
if ($callbackResult === false) {
|
if ($callbackResult === false) $this->checkPermission($permissionName);
|
||||||
$this->checkPermission($permissionName);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current user has a permission or bypass if the provided user
|
|
||||||
* id matches the current user.
|
|
||||||
* @param string $permissionName
|
|
||||||
* @param int $userId
|
|
||||||
* @return bool
|
|
||||||
*/
|
|
||||||
protected function checkPermissionOrCurrentUser(string $permissionName, int $userId)
|
|
||||||
{
|
|
||||||
return $this->checkPermissionOr($permissionName, function () use ($userId) {
|
|
||||||
return $userId === $this->currentUser->id;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send back a json error message.
|
* Send back a json error message.
|
||||||
* @param string $messageText
|
* @param string $messageText
|
||||||
@@ -150,6 +130,7 @@ abstract class Controller extends BaseController
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the response for when a request fails validation.
|
* Create the response for when a request fails validation.
|
||||||
|
*
|
||||||
* @param \Illuminate\Http\Request $request
|
* @param \Illuminate\Http\Request $request
|
||||||
* @param array $errors
|
* @param array $errors
|
||||||
* @return \Symfony\Component\HttpFoundation\Response
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
@@ -165,17 +146,4 @@ abstract class Controller extends BaseController
|
|||||||
->withErrors($errors, $this->errorBag());
|
->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,7 +1,7 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
use Activity;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Repos\EntityRepo;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
|
|
||||||
@@ -19,6 +19,7 @@ class HomeController extends Controller
|
|||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the homepage.
|
* Display the homepage.
|
||||||
* @return Response
|
* @return Response
|
||||||
@@ -31,96 +32,58 @@ class HomeController extends Controller
|
|||||||
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
$recents = $this->signedIn ? Views::getUserRecentlyViewed(12*$recentFactor, 0) : $this->entityRepo->getRecentlyCreated('book', 12*$recentFactor);
|
||||||
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
$recentlyUpdatedPages = $this->entityRepo->getRecentlyUpdated('page', 12);
|
||||||
|
|
||||||
$homepageOptions = ['default', 'books', 'bookshelves', 'page'];
|
// Custom homepage
|
||||||
$homepageOption = setting('app-homepage-type', 'default');
|
$customHomepage = false;
|
||||||
if (!in_array($homepageOption, $homepageOptions)) {
|
$homepageSetting = setting('app-homepage');
|
||||||
$homepageOption = 'default';
|
if ($homepageSetting) {
|
||||||
|
$id = intval(explode(':', $homepageSetting)[0]);
|
||||||
|
$customHomepage = $this->entityRepo->getById('page', $id, false, true);
|
||||||
|
$this->entityRepo->renderPage($customHomepage, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
$commonData = [
|
$view = $customHomepage ? 'home-custom' : 'home';
|
||||||
|
return view($view, [
|
||||||
'activity' => $activity,
|
'activity' => $activity,
|
||||||
'recents' => $recents,
|
'recents' => $recents,
|
||||||
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
'recentlyUpdatedPages' => $recentlyUpdatedPages,
|
||||||
'draftPages' => $draftPages,
|
'draftPages' => $draftPages,
|
||||||
];
|
'customHomepage' => $customHomepage
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
// Add required list ordering & sorting for books & shelves views.
|
/**
|
||||||
if ($homepageOption === 'bookshelves' || $homepageOption === 'books') {
|
* Get a js representation of the current translations
|
||||||
$key = $homepageOption;
|
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
||||||
$view = setting()->getUser($this->currentUser, $key . '_view_type', config('app.views.' . $key));
|
*/
|
||||||
$sort = setting()->getUser($this->currentUser, $key . '_sort', 'name');
|
public function getTranslations() {
|
||||||
$order = setting()->getUser($this->currentUser, $key . '_sort_order', 'asc');
|
$locale = app()->getLocale();
|
||||||
|
$cacheKey = 'GLOBAL_TRANSLATIONS_' . $locale;
|
||||||
$sortOptions = [
|
if (cache()->has($cacheKey) && config('app.env') !== 'development') {
|
||||||
'name' => trans('common.sort_name'),
|
$resp = cache($cacheKey);
|
||||||
'created_at' => trans('common.sort_created_at'),
|
} else {
|
||||||
'updated_at' => trans('common.sort_updated_at'),
|
$translations = [
|
||||||
|
// Get only translations which might be used in JS
|
||||||
|
'common' => trans('common'),
|
||||||
|
'components' => trans('components'),
|
||||||
|
'entities' => trans('entities'),
|
||||||
|
'errors' => trans('errors')
|
||||||
];
|
];
|
||||||
|
if ($locale !== 'en') {
|
||||||
$commonData = array_merge($commonData, [
|
$enTrans = [
|
||||||
'view' => $view,
|
'common' => trans('common', [], 'en'),
|
||||||
'sort' => $sort,
|
'components' => trans('components', [], 'en'),
|
||||||
'order' => $order,
|
'entities' => trans('entities', [], 'en'),
|
||||||
'sortOptions' => $sortOptions,
|
'errors' => trans('errors', [], 'en')
|
||||||
]);
|
];
|
||||||
}
|
$translations = array_replace_recursive($enTrans, $translations);
|
||||||
|
|
||||||
if ($homepageOption === 'bookshelves') {
|
|
||||||
$shelves = $this->entityRepo->getAllPaginated('bookshelf', 18, $commonData['sort'], $commonData['order']);
|
|
||||||
foreach ($shelves as $shelf) {
|
|
||||||
$shelf->books = $this->entityRepo->getBookshelfChildren($shelf);
|
|
||||||
}
|
}
|
||||||
$data = array_merge($commonData, ['shelves' => $shelves]);
|
$resp = 'window.translations = ' . json_encode($translations);
|
||||||
return view('common.home-shelves', $data);
|
cache()->put($cacheKey, $resp, 120);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($homepageOption === 'books') {
|
return response($resp, 200, [
|
||||||
$books = $this->entityRepo->getAllPaginated('book', 18, $commonData['sort'], $commonData['order']);
|
'Content-Type' => 'application/javascript'
|
||||||
$data = array_merge($commonData, ['books' => $books]);
|
]);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get custom head HTML, Used in ajax calls to show in editor.
|
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
*/
|
|
||||||
public function customHeadContent()
|
|
||||||
{
|
|
||||||
return view('partials.custom-head-content');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the view for /robots.txt
|
|
||||||
* @return $this
|
|
||||||
*/
|
|
||||||
public function getRobots()
|
|
||||||
{
|
|
||||||
$sitePublic = setting('app-public', false);
|
|
||||||
$allowRobots = config('app.allow_robots');
|
|
||||||
if ($allowRobots === null) {
|
|
||||||
$allowRobots = $sitePublic;
|
|
||||||
}
|
|
||||||
return response()
|
|
||||||
->view('common.robots', ['allowRobots' => $allowRobots])
|
|
||||||
->header('Content-Type', 'text/plain');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the route for 404 responses.
|
|
||||||
*/
|
|
||||||
public function getNotFound()
|
|
||||||
{
|
|
||||||
return response()->view('errors.404', [], 404);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
178
app/Http/Controllers/ImageController.php
Normal file
178
app/Http/Controllers/ImageController.php
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\ImageUploadException;
|
||||||
|
use BookStack\Repos\EntityRepo;
|
||||||
|
use BookStack\Repos\ImageRepo;
|
||||||
|
use Illuminate\Filesystem\Filesystem as File;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use BookStack\Image;
|
||||||
|
use BookStack\Repos\PageRepo;
|
||||||
|
|
||||||
|
class ImageController extends Controller
|
||||||
|
{
|
||||||
|
protected $image;
|
||||||
|
protected $file;
|
||||||
|
protected $imageRepo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageController constructor.
|
||||||
|
* @param Image $image
|
||||||
|
* @param File $file
|
||||||
|
* @param ImageRepo $imageRepo
|
||||||
|
*/
|
||||||
|
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
||||||
|
{
|
||||||
|
$this->image = $image;
|
||||||
|
$this->file = $file;
|
||||||
|
$this->imageRepo = $imageRepo;
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images for a specific type, Paginated
|
||||||
|
* @param string $type
|
||||||
|
* @param int $page
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function getAllByType($type, $page = 0)
|
||||||
|
{
|
||||||
|
$imgData = $this->imageRepo->getPaginatedByType($type, $page);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search through images within a particular type.
|
||||||
|
* @param $type
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function searchByType($type, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'term' => 'required|string'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$searchTerm = $request->get('term');
|
||||||
|
$imgData = $this->imageRepo->searchPaginatedByType($type, $page, 24, $searchTerm);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all images for a user.
|
||||||
|
* @param int $page
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function getAllForUserType($page = 0)
|
||||||
|
{
|
||||||
|
$imgData = $this->imageRepo->getPaginatedByType('user', $page, 24, $this->currentUser->id);
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get gallery images with a specific filter such as book or page
|
||||||
|
* @param $filter
|
||||||
|
* @param int $page
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Illuminate\Http\JsonResponse|\Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
public function getGalleryFiltered($filter, $page = 0, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'page_id' => 'required|integer'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$validFilters = collect(['page', 'book']);
|
||||||
|
if (!$validFilters->contains($filter)) return response('Invalid filter', 500);
|
||||||
|
|
||||||
|
$pageId = $request->get('page_id');
|
||||||
|
$imgData = $this->imageRepo->getGalleryFiltered($page, 24, strtolower($filter), $pageId);
|
||||||
|
|
||||||
|
return response()->json($imgData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles image uploads for use on pages.
|
||||||
|
* @param string $type
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function uploadByType($type, Request $request)
|
||||||
|
{
|
||||||
|
$this->checkPermission('image-create-all');
|
||||||
|
$this->validate($request, [
|
||||||
|
'file' => 'is_image'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$imageUpload = $request->file('file');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$uploadedTo = $request->has('uploaded_to') ? $request->get('uploaded_to') : 0;
|
||||||
|
$image = $this->imageRepo->saveNew($imageUpload, $type, $uploadedTo);
|
||||||
|
} catch (ImageUploadException $e) {
|
||||||
|
return response($e->getMessage(), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->json($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a sized thumbnail for an image.
|
||||||
|
* @param $id
|
||||||
|
* @param $width
|
||||||
|
* @param $height
|
||||||
|
* @param $crop
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function getThumbnail($id, $width, $height, $crop)
|
||||||
|
{
|
||||||
|
$this->checkPermission('image-create-all');
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$thumbnailUrl = $this->imageRepo->getThumbnail($image, $width, $height, $crop == 'false');
|
||||||
|
return response()->json(['url' => $thumbnailUrl]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update image details
|
||||||
|
* @param integer $imageId
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function update($imageId, Request $request)
|
||||||
|
{
|
||||||
|
$this->validate($request, [
|
||||||
|
'name' => 'required|min:2|string'
|
||||||
|
]);
|
||||||
|
$image = $this->imageRepo->getById($imageId);
|
||||||
|
$this->checkOwnablePermission('image-update', $image);
|
||||||
|
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
||||||
|
return response()->json($image);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an image and all thumbnail/image files
|
||||||
|
* @param EntityRepo $entityRepo
|
||||||
|
* @param Request $request
|
||||||
|
* @param int $id
|
||||||
|
* @return \Illuminate\Http\JsonResponse
|
||||||
|
*/
|
||||||
|
public function destroy(EntityRepo $entityRepo, Request $request, $id)
|
||||||
|
{
|
||||||
|
$image = $this->imageRepo->getById($id);
|
||||||
|
$this->checkOwnablePermission('image-delete', $image);
|
||||||
|
|
||||||
|
// Check if this image is used on any pages
|
||||||
|
$isForced = ($request->has('force') && ($request->get('force') === 'true') || $request->get('force') === true);
|
||||||
|
if (!$isForced) {
|
||||||
|
$pageSearch = $entityRepo->searchForImage($image->url);
|
||||||
|
if ($pageSearch !== false) {
|
||||||
|
return response()->json($pageSearch, 400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->imageRepo->destroyImage($image);
|
||||||
|
return response()->json(trans('components.images_deleted'));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Images;
|
|
||||||
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class DrawioImageController extends Controller
|
|
||||||
{
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* DrawioImageController constructor.
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of gallery images, in a list.
|
|
||||||
* Can be paged and filtered by entity.
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = $request->get('page', 1);
|
|
||||||
$searchTerm = $request->get('search', null);
|
|
||||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
|
||||||
$parentTypeFilter = $request->get('filter_type', null);
|
|
||||||
|
|
||||||
$imgData = $this->imageRepo->getEntityFiltered('drawio', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a new gallery image in the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @return Illuminate\Http\JsonResponse
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function create(Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'image' => 'required|string',
|
|
||||||
'uploaded_to' => 'required|integer'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$this->checkPermission('image-create-all');
|
|
||||||
$imageBase64Data = $request->get('image');
|
|
||||||
|
|
||||||
try {
|
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
|
||||||
$image = $this->imageRepo->saveDrawing($imageBase64Data, $uploadedTo);
|
|
||||||
} catch (ImageUploadException $e) {
|
|
||||||
return response($e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content of an image based64 encoded.
|
|
||||||
* @param $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse|mixed
|
|
||||||
*/
|
|
||||||
public function getAsBase64($id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$page = $image->getPage();
|
|
||||||
if ($image === null || $image->type !== 'drawio' || !userCan('page-view', $page)) {
|
|
||||||
return $this->jsonError("Image data could not be found");
|
|
||||||
}
|
|
||||||
|
|
||||||
$imageData = $this->imageRepo->getImageData($image);
|
|
||||||
if ($imageData === null) {
|
|
||||||
return $this->jsonError("Image data could not be found");
|
|
||||||
}
|
|
||||||
return response()->json([
|
|
||||||
'content' => base64_encode($imageData)
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Images;
|
|
||||||
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
|
|
||||||
class GalleryImageController extends Controller
|
|
||||||
{
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* GalleryImageController constructor.
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of gallery images, in a list.
|
|
||||||
* Can be paged and filtered by entity.
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = $request->get('page', 1);
|
|
||||||
$searchTerm = $request->get('search', null);
|
|
||||||
$uploadedToFilter = $request->get('uploaded_to', null);
|
|
||||||
$parentTypeFilter = $request->get('filter_type', null);
|
|
||||||
|
|
||||||
$imgData = $this->imageRepo->getEntityFiltered('gallery', $parentTypeFilter, $page, 24, $uploadedToFilter, $searchTerm);
|
|
||||||
return response()->json($imgData);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Store a new gallery image in the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @return Illuminate\Http\JsonResponse
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function create(Request $request)
|
|
||||||
{
|
|
||||||
$this->checkPermission('image-create-all');
|
|
||||||
$this->validate($request, [
|
|
||||||
'file' => $this->imageRepo->getImageValidationRules()
|
|
||||||
]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$imageUpload = $request->file('file');
|
|
||||||
$uploadedTo = $request->get('uploaded_to', 0);
|
|
||||||
$image = $this->imageRepo->saveNew($imageUpload, 'gallery', $uploadedTo);
|
|
||||||
} catch (ImageUploadException $e) {
|
|
||||||
return response($e->getMessage(), 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
<?php namespace BookStack\Http\Controllers\Images;
|
|
||||||
|
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
|
||||||
use BookStack\Exceptions\ImageUploadException;
|
|
||||||
use BookStack\Http\Controllers\Controller;
|
|
||||||
use BookStack\Repos\PageRepo;
|
|
||||||
use BookStack\Uploads\Image;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Filesystem\Filesystem as File;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class ImageController extends Controller
|
|
||||||
{
|
|
||||||
protected $image;
|
|
||||||
protected $file;
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ImageController constructor.
|
|
||||||
* @param Image $image
|
|
||||||
* @param File $file
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(Image $image, File $file, ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->image = $image;
|
|
||||||
$this->file = $file;
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provide an image file from storage.
|
|
||||||
* @param string $path
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function showImage(string $path)
|
|
||||||
{
|
|
||||||
$path = storage_path('uploads/images/' . $path);
|
|
||||||
if (!file_exists($path)) {
|
|
||||||
abort(404);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->file($path);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update image details
|
|
||||||
* @param integer $id
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws ImageUploadException
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function update($id, Request $request)
|
|
||||||
{
|
|
||||||
$this->validate($request, [
|
|
||||||
'name' => 'required|min:2|string'
|
|
||||||
]);
|
|
||||||
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$this->checkImagePermission($image);
|
|
||||||
$this->checkOwnablePermission('image-update', $image);
|
|
||||||
|
|
||||||
$image = $this->imageRepo->updateImageDetails($image, $request->all());
|
|
||||||
return response()->json($image);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the usage of an image on pages.
|
|
||||||
* @param \BookStack\Entities\Repos\EntityRepo $entityRepo
|
|
||||||
* @param $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
*/
|
|
||||||
public function usage(EntityRepo $entityRepo, $id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$this->checkImagePermission($image);
|
|
||||||
$pageSearch = $entityRepo->searchForImage($image->url);
|
|
||||||
return response()->json($pageSearch);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an image and all thumbnail/image files
|
|
||||||
* @param int $id
|
|
||||||
* @return \Illuminate\Http\JsonResponse
|
|
||||||
* @throws \Exception
|
|
||||||
*/
|
|
||||||
public function destroy($id)
|
|
||||||
{
|
|
||||||
$image = $this->imageRepo->getById($id);
|
|
||||||
$this->checkOwnablePermission('image-delete', $image);
|
|
||||||
$this->checkImagePermission($image);
|
|
||||||
|
|
||||||
$this->imageRepo->destroyImage($image);
|
|
||||||
return response()->json(trans('components.images_deleted'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check related page permission and ensure type is drawio or gallery.
|
|
||||||
* @param Image $image
|
|
||||||
*/
|
|
||||||
protected function checkImagePermission(Image $image)
|
|
||||||
{
|
|
||||||
if ($image->type !== 'drawio' && $image->type !== 'gallery') {
|
|
||||||
$this->showPermissionError();
|
|
||||||
}
|
|
||||||
|
|
||||||
$relatedPage = $image->getPage();
|
|
||||||
if ($relatedPage) {
|
|
||||||
$this->checkOwnablePermission('page-view', $relatedPage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +1,32 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use Activity;
|
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\Exceptions\NotFoundException;
|
||||||
use GatherContent\Htmldiff\Htmldiff;
|
use BookStack\Repos\EntityRepo;
|
||||||
|
use BookStack\Repos\UserRepo;
|
||||||
|
use BookStack\Services\ExportService;
|
||||||
|
use Carbon\Carbon;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Views;
|
use Views;
|
||||||
|
use GatherContent\Htmldiff\Htmldiff;
|
||||||
|
|
||||||
class PageController extends Controller
|
class PageController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $pageRepo;
|
protected $entityRepo;
|
||||||
protected $exportService;
|
protected $exportService;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* PageController constructor.
|
* PageController constructor.
|
||||||
* @param \BookStack\Entities\Repos\PageRepo $pageRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param \BookStack\Entities\ExportService $exportService
|
* @param ExportService $exportService
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(PageRepo $pageRepo, ExportService $exportService, UserRepo $userRepo)
|
public function __construct(EntityRepo $entityRepo, ExportService $exportService, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->pageRepo = $pageRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
$this->exportService = $exportService;
|
$this->exportService = $exportService;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
@@ -38,30 +38,23 @@ class PageController extends Controller
|
|||||||
* @param string $chapterSlug
|
* @param string $chapterSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @internal param bool $pageSlug
|
* @internal param bool $pageSlug
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function create($bookSlug, $chapterSlug = null)
|
public function create($bookSlug, $chapterSlug = null)
|
||||||
{
|
{
|
||||||
if ($chapterSlug !== null) {
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||||
$book = $chapter->book;
|
|
||||||
} else {
|
|
||||||
$chapter = null;
|
|
||||||
$book = $this->pageRepo->getBySlug('book', $bookSlug);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent = $chapter ? $chapter : $book;
|
$parent = $chapter ? $chapter : $book;
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
// Redirect to draft edit screen if signed in
|
// Redirect to draft edit screen if signed in
|
||||||
if ($this->signedIn) {
|
if ($this->signedIn) {
|
||||||
$draft = $this->pageRepo->getDraftPage($book, $chapter);
|
$draft = $this->entityRepo->getDraftPage($book, $chapter);
|
||||||
return redirect($draft->getUrl());
|
return redirect($draft->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise show the edit view if they're a guest
|
// Otherwise show edit view
|
||||||
$this->setPageTitle(trans('entities.pages_new'));
|
$this->setPageTitle(trans('entities.pages_new'));
|
||||||
return view('pages.guest-create', ['parent' => $parent]);
|
return view('pages/guest-create', ['parent' => $parent]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,19 +71,13 @@ class PageController extends Controller
|
|||||||
'name' => 'required|string|max:255'
|
'name' => 'required|string|max:255'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if ($chapterSlug !== null) {
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$chapter = $this->pageRepo->getBySlug('chapter', $chapterSlug, $bookSlug);
|
$chapter = $chapterSlug ? $this->entityRepo->getBySlug('chapter', $chapterSlug, $bookSlug) : null;
|
||||||
$book = $chapter->book;
|
|
||||||
} else {
|
|
||||||
$chapter = null;
|
|
||||||
$book = $this->pageRepo->getBySlug('book', $bookSlug);
|
|
||||||
}
|
|
||||||
|
|
||||||
$parent = $chapter ? $chapter : $book;
|
$parent = $chapter ? $chapter : $book;
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
$page = $this->pageRepo->getDraftPage($book, $chapter);
|
$page = $this->entityRepo->getDraftPage($book, $chapter);
|
||||||
$this->pageRepo->publishPageDraft($page, [
|
$this->entityRepo->publishPageDraft($page, [
|
||||||
'name' => $request->get('name'),
|
'name' => $request->get('name'),
|
||||||
'html' => ''
|
'html' => ''
|
||||||
]);
|
]);
|
||||||
@@ -105,19 +92,16 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function editDraft($bookSlug, $pageId)
|
public function editDraft($bookSlug, $pageId)
|
||||||
{
|
{
|
||||||
$draft = $this->pageRepo->getById('page', $pageId, true);
|
$draft = $this->entityRepo->getById('page', $pageId, true);
|
||||||
$this->checkOwnablePermission('page-create', $draft->parent);
|
$this->checkOwnablePermission('page-create', $draft->book);
|
||||||
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
$this->setPageTitle(trans('entities.pages_edit_draft'));
|
||||||
|
|
||||||
$draftsEnabled = $this->signedIn;
|
$draftsEnabled = $this->signedIn;
|
||||||
$templates = $this->pageRepo->getPageTemplates(10);
|
return view('pages/edit', [
|
||||||
|
|
||||||
return view('pages.edit', [
|
|
||||||
'page' => $draft,
|
'page' => $draft,
|
||||||
'book' => $draft->book,
|
'book' => $draft->book,
|
||||||
'isDraft' => true,
|
'isDraft' => true,
|
||||||
'draftsEnabled' => $draftsEnabled,
|
'draftsEnabled' => $draftsEnabled
|
||||||
'templates' => $templates,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,19 +119,21 @@ class PageController extends Controller
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
$input = $request->all();
|
$input = $request->all();
|
||||||
$draftPage = $this->pageRepo->getById('page', $pageId, true);
|
$book = $this->entityRepo->getBySlug('book', $bookSlug);
|
||||||
$book = $draftPage->book;
|
|
||||||
|
|
||||||
$parent = $draftPage->parent;
|
$draftPage = $this->entityRepo->getById('page', $pageId, true);
|
||||||
|
|
||||||
|
$chapterId = intval($draftPage->chapter_id);
|
||||||
|
$parent = $chapterId !== 0 ? $this->entityRepo->getById('chapter', $chapterId) : $book;
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->checkOwnablePermission('page-create', $parent);
|
||||||
|
|
||||||
if ($parent->isA('chapter')) {
|
if ($parent->isA('chapter')) {
|
||||||
$input['priority'] = $this->pageRepo->getNewChapterPriority($parent);
|
$input['priority'] = $this->entityRepo->getNewChapterPriority($parent);
|
||||||
} else {
|
} else {
|
||||||
$input['priority'] = $this->pageRepo->getNewBookPriority($parent);
|
$input['priority'] = $this->entityRepo->getNewBookPriority($parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
$page = $this->pageRepo->publishPageDraft($draftPage, $input);
|
$page = $this->entityRepo->publishPageDraft($draftPage, $input);
|
||||||
|
|
||||||
Activity::add($page, 'page_create', $book->id);
|
Activity::add($page, 'page_create', $book->id);
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
@@ -159,41 +145,30 @@ class PageController extends Controller
|
|||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @param string $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function show($bookSlug, $pageSlug)
|
public function show($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
} catch (NotFoundException $e) {
|
} catch (NotFoundException $e) {
|
||||||
$page = $this->pageRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getPageByOldSlug($pageSlug, $bookSlug);
|
||||||
if ($page === null) {
|
if ($page === null) abort(404);
|
||||||
throw $e;
|
|
||||||
}
|
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
$this->checkOwnablePermission('page-view', $page);
|
||||||
|
|
||||||
$page->html = $this->pageRepo->renderPage($page);
|
$pageContent = $this->entityRepo->renderPage($page);
|
||||||
$sidebarTree = $this->pageRepo->getBookChildren($page->book);
|
$sidebarTree = $this->entityRepo->getBookChildren($page->book);
|
||||||
$pageNav = $this->pageRepo->getPageNav($page->html);
|
$pageNav = $this->entityRepo->getPageNav($pageContent);
|
||||||
|
$page->load(['comments.createdBy']);
|
||||||
// check if the comment's are enabled
|
|
||||||
$commentsEnabled = !setting('app-disable-comments');
|
|
||||||
if ($commentsEnabled) {
|
|
||||||
$page->load(['comments.createdBy']);
|
|
||||||
}
|
|
||||||
|
|
||||||
Views::add($page);
|
Views::add($page);
|
||||||
$this->setPageTitle($page->getShortName());
|
$this->setPageTitle($page->getShortName());
|
||||||
return view('pages.show', [
|
return view('pages/show', [
|
||||||
'page' => $page,'book' => $page->book,
|
'page' => $page,'book' => $page->book,
|
||||||
'current' => $page,
|
'current' => $page, 'sidebarTree' => $sidebarTree,
|
||||||
'sidebarTree' => $sidebarTree,
|
'pageNav' => $pageNav]);
|
||||||
'commentsEnabled' => $commentsEnabled,
|
|
||||||
'pageNav' => $pageNav
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -203,7 +178,7 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getPageAjax($pageId)
|
public function getPageAjax($pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById('page', $pageId);
|
$page = $this->entityRepo->getById('page', $pageId);
|
||||||
return response()->json($page);
|
return response()->json($page);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,44 +187,38 @@ class PageController extends Controller
|
|||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @param string $pageSlug
|
* @param string $pageSlug
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function edit($bookSlug, $pageSlug)
|
public function edit($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_editing_named', ['pageName'=>$page->getShortName()]));
|
||||||
$page->isDraft = false;
|
$page->isDraft = false;
|
||||||
|
|
||||||
// Check for active editing
|
// Check for active editing
|
||||||
$warnings = [];
|
$warnings = [];
|
||||||
if ($this->pageRepo->isPageEditingActive($page, 60)) {
|
if ($this->entityRepo->isPageEditingActive($page, 60)) {
|
||||||
$warnings[] = $this->pageRepo->getPageEditingActiveMessage($page, 60);
|
$warnings[] = $this->entityRepo->getPageEditingActiveMessage($page, 60);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a current draft version for this user
|
// Check for a current draft version for this user
|
||||||
$userPageDraft = $this->pageRepo->getUserPageDraft($page, $this->currentUser->id);
|
if ($this->entityRepo->hasUserGotPageDraft($page, $this->currentUser->id)) {
|
||||||
if ($userPageDraft !== null) {
|
$draft = $this->entityRepo->getUserPageDraft($page, $this->currentUser->id);
|
||||||
$page->name = $userPageDraft->name;
|
$page->name = $draft->name;
|
||||||
$page->html = $userPageDraft->html;
|
$page->html = $draft->html;
|
||||||
$page->markdown = $userPageDraft->markdown;
|
$page->markdown = $draft->markdown;
|
||||||
$page->isDraft = true;
|
$page->isDraft = true;
|
||||||
$warnings [] = $this->pageRepo->getUserPageDraftMessage($userPageDraft);
|
$warnings [] = $this->entityRepo->getUserPageDraftMessage($draft);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($warnings) > 0) {
|
if (count($warnings) > 0) session()->flash('warning', implode("\n", $warnings));
|
||||||
session()->flash('warning', implode("\n", $warnings));
|
|
||||||
}
|
|
||||||
|
|
||||||
$draftsEnabled = $this->signedIn;
|
$draftsEnabled = $this->signedIn;
|
||||||
$templates = $this->pageRepo->getPageTemplates(10);
|
return view('pages/edit', [
|
||||||
|
|
||||||
return view('pages.edit', [
|
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'current' => $page,
|
'current' => $page,
|
||||||
'draftsEnabled' => $draftsEnabled,
|
'draftsEnabled' => $draftsEnabled
|
||||||
'templates' => $templates,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,9 +234,9 @@ class PageController extends Controller
|
|||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'required|string|max:255'
|
'name' => 'required|string|max:255'
|
||||||
]);
|
]);
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->pageRepo->updatePage($page, $page->book->id, $request->all());
|
$this->entityRepo->updatePage($page, $page->book->id, $request->all());
|
||||||
Activity::add($page, 'page_update', $page->book->id);
|
Activity::add($page, 'page_update', $page->book->id);
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
@@ -280,7 +249,7 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function saveDraft(Request $request, $pageId)
|
public function saveDraft(Request $request, $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById('page', $pageId, true);
|
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
|
|
||||||
if (!$this->signedIn) {
|
if (!$this->signedIn) {
|
||||||
@@ -290,13 +259,14 @@ class PageController extends Controller
|
|||||||
], 500);
|
], 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
$draft = $this->pageRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
$draft = $this->entityRepo->updatePageDraft($page, $request->only(['name', 'html', 'markdown']));
|
||||||
|
|
||||||
$updateTime = $draft->updated_at->timestamp;
|
$updateTime = $draft->updated_at->timestamp;
|
||||||
|
$utcUpdateTimestamp = $updateTime + Carbon::createFromTimestamp(0)->offset;
|
||||||
return response()->json([
|
return response()->json([
|
||||||
'status' => 'success',
|
'status' => 'success',
|
||||||
'message' => trans('entities.pages_edit_draft_save_at'),
|
'message' => trans('entities.pages_edit_draft_save_at'),
|
||||||
'timestamp' => $updateTime
|
'timestamp' => $utcUpdateTimestamp
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -308,7 +278,7 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function redirectFromLink($pageId)
|
public function redirectFromLink($pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById('page', $pageId);
|
$page = $this->entityRepo->getById('page', $pageId);
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,10 +290,10 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showDelete($bookSlug, $pageSlug)
|
public function showDelete($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_delete_named', ['pageName'=>$page->getShortName()]));
|
||||||
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -336,10 +306,10 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showDeleteDraft($bookSlug, $pageId)
|
public function showDeleteDraft($bookSlug, $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById('page', $pageId, true);
|
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_delete_draft_named', ['pageName'=>$page->getShortName()]));
|
||||||
return view('pages.delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
return view('pages/delete', ['book' => $page->book, 'page' => $page, 'current' => $page]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -351,13 +321,12 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroy($bookSlug, $pageSlug)
|
public function destroy($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$book = $page->book;
|
$book = $page->book;
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
$this->checkOwnablePermission('page-delete', $page);
|
||||||
$this->pageRepo->destroyPage($page);
|
|
||||||
|
|
||||||
Activity::addMessage('page_delete', $book->id, $page->name);
|
Activity::addMessage('page_delete', $book->id, $page->name);
|
||||||
session()->flash('success', trans('entities.pages_delete_success'));
|
session()->flash('success', trans('entities.pages_delete_success'));
|
||||||
|
$this->entityRepo->destroyPage($page);
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,11 +339,11 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function destroyDraft($bookSlug, $pageId)
|
public function destroyDraft($bookSlug, $pageId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getById('page', $pageId, true);
|
$page = $this->entityRepo->getById('page', $pageId, true);
|
||||||
$book = $page->book;
|
$book = $page->book;
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
session()->flash('success', trans('entities.pages_delete_draft_success'));
|
session()->flash('success', trans('entities.pages_delete_draft_success'));
|
||||||
$this->pageRepo->destroyPage($page);
|
$this->entityRepo->destroyPage($page);
|
||||||
return redirect($book->getUrl());
|
return redirect($book->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -383,13 +352,12 @@ class PageController extends Controller
|
|||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @param string $pageSlug
|
* @param string $pageSlug
|
||||||
* @return \Illuminate\View\View
|
* @return \Illuminate\View\View
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
*/
|
||||||
public function showRevisions($bookSlug, $pageSlug)
|
public function showRevisions($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_revisions_named', ['pageName'=>$page->getShortName()]));
|
||||||
return view('pages.revisions', ['page' => $page, 'current' => $page]);
|
return view('pages/revisions', ['page' => $page, 'book' => $page->book, 'current' => $page]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -401,7 +369,7 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
public function showRevision($bookSlug, $pageSlug, $revisionId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
if ($revision === null) {
|
if ($revision === null) {
|
||||||
abort(404);
|
abort(404);
|
||||||
@@ -410,10 +378,9 @@ class PageController extends Controller
|
|||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName' => $page->getShortName()]));
|
||||||
|
|
||||||
return view('pages.revision', [
|
return view('pages/revision', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'diff' => null,
|
|
||||||
'revision' => $revision
|
'revision' => $revision
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -427,7 +394,7 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
public function showRevisionChanges($bookSlug, $pageSlug, $revisionId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
$revision = $page->revisions()->where('id', '=', $revisionId)->first();
|
||||||
if ($revision === null) {
|
if ($revision === null) {
|
||||||
abort(404);
|
abort(404);
|
||||||
@@ -440,7 +407,7 @@ class PageController extends Controller
|
|||||||
$page->fill($revision->toArray());
|
$page->fill($revision->toArray());
|
||||||
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
$this->setPageTitle(trans('entities.pages_revision_named', ['pageName'=>$page->getShortName()]));
|
||||||
|
|
||||||
return view('pages.revision', [
|
return view('pages/revision', [
|
||||||
'page' => $page,
|
'page' => $page,
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'diff' => $diff,
|
'diff' => $diff,
|
||||||
@@ -457,47 +424,13 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
public function restoreRevision($bookSlug, $pageSlug, $revisionId)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$page = $this->pageRepo->restorePageRevision($page, $page->book, $revisionId);
|
$page = $this->entityRepo->restorePageRevision($page, $page->book, $revisionId);
|
||||||
Activity::add($page, 'page_restore', $page->book->id);
|
Activity::add($page, 'page_restore', $page->book->id);
|
||||||
return redirect($page->getUrl());
|
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 redirect($page->getUrl('/revisions'));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exports a page to a PDF.
|
* Exports a page to a PDF.
|
||||||
* https://github.com/barryvdh/laravel-dompdf
|
* https://github.com/barryvdh/laravel-dompdf
|
||||||
@@ -507,10 +440,12 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function exportPdf($bookSlug, $pageSlug)
|
public function exportPdf($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$page->html = $this->pageRepo->renderPage($page);
|
|
||||||
$pdfContent = $this->exportService->pageToPdf($page);
|
$pdfContent = $this->exportService->pageToPdf($page);
|
||||||
return $this->downloadResponse($pdfContent, $pageSlug . '.pdf');
|
return response()->make($pdfContent, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.pdf'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -521,10 +456,12 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function exportHtml($bookSlug, $pageSlug)
|
public function exportHtml($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$page->html = $this->pageRepo->renderPage($page);
|
|
||||||
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
$containedHtml = $this->exportService->pageToContainedHtml($page);
|
||||||
return $this->downloadResponse($containedHtml, $pageSlug . '.html');
|
return response()->make($containedHtml, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.html'
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -535,9 +472,25 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function exportPlainText($bookSlug, $pageSlug)
|
public function exportPlainText($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$pageText = $this->exportService->pageToPlainText($page);
|
$containedHtml = $this->exportService->pageToPlainText($page);
|
||||||
return $this->downloadResponse($pageText, $pageSlug . '.txt');
|
return response()->make($containedHtml, 200, [
|
||||||
|
'Content-Type' => 'application/octet-stream',
|
||||||
|
'Content-Disposition' => 'attachment; filename="' . $pageSlug . '.txt'
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a listing of recently created pages
|
||||||
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function showRecentlyCreated()
|
||||||
|
{
|
||||||
|
$pages = $this->entityRepo->getRecentlyCreatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-created'));
|
||||||
|
return view('pages/detailed-listing', [
|
||||||
|
'title' => trans('entities.recently_created_pages'),
|
||||||
|
'pages' => $pages
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -546,14 +499,30 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showRecentlyUpdated()
|
public function showRecentlyUpdated()
|
||||||
{
|
{
|
||||||
// TODO - Still exist?
|
$pages = $this->entityRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(baseUrl('/pages/recently-updated'));
|
||||||
$pages = $this->pageRepo->getRecentlyUpdatedPaginated('page', 20)->setPath(url('/pages/recently-updated'));
|
return view('pages/detailed-listing', [
|
||||||
return view('pages.detailed-listing', [
|
|
||||||
'title' => trans('entities.recently_updated_pages'),
|
'title' => trans('entities.recently_updated_pages'),
|
||||||
'pages' => $pages
|
'pages' => $pages
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the Restrictions view.
|
||||||
|
* @param string $bookSlug
|
||||||
|
* @param string $pageSlug
|
||||||
|
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
||||||
|
*/
|
||||||
|
public function showRestrict($bookSlug, $pageSlug)
|
||||||
|
{
|
||||||
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
|
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||||
|
$roles = $this->userRepo->getRestrictableRoles();
|
||||||
|
return view('pages/restrictions', [
|
||||||
|
'page' => $page,
|
||||||
|
'roles' => $roles
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show the view to choose a new parent to move a page into.
|
* Show the view to choose a new parent to move a page into.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
@@ -563,10 +532,9 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function showMove($bookSlug, $pageSlug)
|
public function showMove($bookSlug, $pageSlug)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
return view('pages/move', [
|
||||||
return view('pages.move', [
|
|
||||||
'book' => $page->book,
|
'book' => $page->book,
|
||||||
'page' => $page
|
'page' => $page
|
||||||
]);
|
]);
|
||||||
@@ -582,9 +550,8 @@ class PageController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function move($bookSlug, $pageSlug, Request $request)
|
public function move($bookSlug, $pageSlug, Request $request)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('page-update', $page);
|
$this->checkOwnablePermission('page-update', $page);
|
||||||
$this->checkOwnablePermission('page-delete', $page);
|
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
$entitySelection = $request->get('entity_selection', null);
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
if ($entitySelection === null || $entitySelection === '') {
|
||||||
@@ -597,111 +564,33 @@ class PageController extends Controller
|
|||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$parent = $this->pageRepo->getById($entityType, $entityId);
|
$parent = $this->entityRepo->getById($entityType, $entityId);
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
||||||
return redirect()->back();
|
return redirect()->back();
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
$this->entityRepo->changePageParent($page, $parent);
|
||||||
|
|
||||||
$this->pageRepo->changePageParent($page, $parent);
|
|
||||||
Activity::add($page, 'page_move', $page->book->id);
|
Activity::add($page, 'page_move', $page->book->id);
|
||||||
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
session()->flash('success', trans('entities.pages_move_success', ['parentName' => $parent->name]));
|
||||||
|
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the view to copy a page.
|
|
||||||
* @param string $bookSlug
|
|
||||||
* @param string $pageSlug
|
|
||||||
* @return mixed
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function showCopy($bookSlug, $pageSlug)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
|
||||||
session()->flashInput(['name' => $page->name]);
|
|
||||||
return view('pages.copy', [
|
|
||||||
'book' => $page->book,
|
|
||||||
'page' => $page
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a copy of a page within the requested target destination.
|
|
||||||
* @param string $bookSlug
|
|
||||||
* @param string $pageSlug
|
|
||||||
* @param Request $request
|
|
||||||
* @return mixed
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function copy($bookSlug, $pageSlug, Request $request)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
|
||||||
$this->checkOwnablePermission('page-view', $page);
|
|
||||||
|
|
||||||
$entitySelection = $request->get('entity_selection', null);
|
|
||||||
if ($entitySelection === null || $entitySelection === '') {
|
|
||||||
$parent = $page->chapter ? $page->chapter : $page->book;
|
|
||||||
} else {
|
|
||||||
$stringExploded = explode(':', $entitySelection);
|
|
||||||
$entityType = $stringExploded[0];
|
|
||||||
$entityId = intval($stringExploded[1]);
|
|
||||||
|
|
||||||
try {
|
|
||||||
$parent = $this->pageRepo->getById($entityType, $entityId);
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
session()->flash(trans('entities.selected_book_chapter_not_found'));
|
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->checkOwnablePermission('page-create', $parent);
|
|
||||||
|
|
||||||
$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'));
|
|
||||||
|
|
||||||
return redirect($pageCopy->getUrl());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the Permissions view.
|
|
||||||
* @param string $bookSlug
|
|
||||||
* @param string $pageSlug
|
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function showPermissions($bookSlug, $pageSlug)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
|
||||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
|
||||||
$roles = $this->userRepo->getRestrictableRoles();
|
|
||||||
return view('pages.permissions', [
|
|
||||||
'page' => $page,
|
|
||||||
'roles' => $roles
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the permissions for this page.
|
* Set the permissions for this page.
|
||||||
* @param string $bookSlug
|
* @param string $bookSlug
|
||||||
* @param string $pageSlug
|
* @param string $pageSlug
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws NotFoundException
|
|
||||||
* @throws \Throwable
|
|
||||||
*/
|
*/
|
||||||
public function permissions($bookSlug, $pageSlug, Request $request)
|
public function restrict($bookSlug, $pageSlug, Request $request)
|
||||||
{
|
{
|
||||||
$page = $this->pageRepo->getPageBySlug($pageSlug, $bookSlug);
|
$page = $this->entityRepo->getBySlug('page', $pageSlug, $bookSlug);
|
||||||
$this->checkOwnablePermission('restrictions-manage', $page);
|
$this->checkOwnablePermission('restrictions-manage', $page);
|
||||||
$this->pageRepo->updateEntityPermissionsFromRequest($request, $page);
|
$this->entityRepo->updateEntityPermissionsFromRequest($request, $page);
|
||||||
session()->flash('success', trans('entities.pages_permissions_success'));
|
session()->flash('success', trans('entities.pages_permissions_success'));
|
||||||
return redirect($page->getUrl());
|
return redirect($page->getUrl());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,63 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
|
||||||
|
|
||||||
use BookStack\Entities\Repos\PageRepo;
|
|
||||||
use BookStack\Exceptions\NotFoundException;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class PageTemplateController extends Controller
|
|
||||||
{
|
|
||||||
protected $pageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* PageTemplateController constructor.
|
|
||||||
* @param $pageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(PageRepo $pageRepo)
|
|
||||||
{
|
|
||||||
$this->pageRepo = $pageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Fetch a list of templates from the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
*/
|
|
||||||
public function list(Request $request)
|
|
||||||
{
|
|
||||||
$page = $request->get('page', 1);
|
|
||||||
$search = $request->get('search', '');
|
|
||||||
$templates = $this->pageRepo->getPageTemplates(10, $page, $search);
|
|
||||||
|
|
||||||
if ($search) {
|
|
||||||
$templates->appends(['search' => $search]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('pages.template-manager-list', [
|
|
||||||
'templates' => $templates
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the content of a template.
|
|
||||||
* @param $templateId
|
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
|
||||||
* @throws NotFoundException
|
|
||||||
*/
|
|
||||||
public function get($templateId)
|
|
||||||
{
|
|
||||||
$page = $this->pageRepo->getById('page', $templateId);
|
|
||||||
|
|
||||||
if (!$page->template) {
|
|
||||||
throw new NotFoundException();
|
|
||||||
}
|
|
||||||
|
|
||||||
return response()->json([
|
|
||||||
'html' => $page->html,
|
|
||||||
'markdown' => $page->markdown,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionsRepo;
|
|
||||||
use BookStack\Exceptions\PermissionsException;
|
use BookStack\Exceptions\PermissionsException;
|
||||||
|
use BookStack\Repos\PermissionsRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class PermissionController extends Controller
|
class PermissionController extends Controller
|
||||||
@@ -11,7 +11,7 @@ class PermissionController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* PermissionController constructor.
|
* PermissionController constructor.
|
||||||
* @param \BookStack\Auth\Permissions\PermissionsRepo $permissionsRepo
|
* @param PermissionsRepo $permissionsRepo
|
||||||
*/
|
*/
|
||||||
public function __construct(PermissionsRepo $permissionsRepo)
|
public function __construct(PermissionsRepo $permissionsRepo)
|
||||||
{
|
{
|
||||||
@@ -26,7 +26,7 @@ class PermissionController extends Controller
|
|||||||
{
|
{
|
||||||
$this->checkPermission('user-roles-manage');
|
$this->checkPermission('user-roles-manage');
|
||||||
$roles = $this->permissionsRepo->getAllRoles();
|
$roles = $this->permissionsRepo->getAllRoles();
|
||||||
return view('settings.roles.index', ['roles' => $roles]);
|
return view('settings/roles/index', ['roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -36,7 +36,7 @@ class PermissionController extends Controller
|
|||||||
public function createRole()
|
public function createRole()
|
||||||
{
|
{
|
||||||
$this->checkPermission('user-roles-manage');
|
$this->checkPermission('user-roles-manage');
|
||||||
return view('settings.roles.create');
|
return view('settings/roles/create');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,10 +67,8 @@ class PermissionController extends Controller
|
|||||||
{
|
{
|
||||||
$this->checkPermission('user-roles-manage');
|
$this->checkPermission('user-roles-manage');
|
||||||
$role = $this->permissionsRepo->getRoleById($id);
|
$role = $this->permissionsRepo->getRoleById($id);
|
||||||
if ($role->hidden) {
|
if ($role->hidden) throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
||||||
throw new PermissionsException(trans('errors.role_cannot_be_edited'));
|
return view('settings/roles/edit', ['role' => $role]);
|
||||||
}
|
|
||||||
return view('settings.roles.edit', ['role' => $role]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +76,6 @@ class PermissionController extends Controller
|
|||||||
* @param $id
|
* @param $id
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
||||||
* @throws PermissionsException
|
|
||||||
*/
|
*/
|
||||||
public function updateRole($id, Request $request)
|
public function updateRole($id, Request $request)
|
||||||
{
|
{
|
||||||
@@ -106,7 +103,7 @@ class PermissionController extends Controller
|
|||||||
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
$roles = $this->permissionsRepo->getAllRolesExcept($role);
|
||||||
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
$blankRole = $role->newInstance(['display_name' => trans('settings.role_delete_no_migration')]);
|
||||||
$roles->prepend($blankRole);
|
$roles->prepend($blankRole);
|
||||||
return view('settings.roles.delete', ['role' => $role, 'roles' => $roles]);
|
return view('settings/roles/delete', ['role' => $role, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,45 +1,34 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ViewService;
|
use BookStack\Repos\EntityRepo;
|
||||||
use BookStack\Entities\EntityContextManager;
|
use BookStack\Services\SearchService;
|
||||||
use BookStack\Entities\Repos\EntityRepo;
|
use BookStack\Services\ViewService;
|
||||||
use BookStack\Entities\SearchService;
|
|
||||||
use BookStack\Exceptions\NotFoundException;
|
|
||||||
use Illuminate\Contracts\View\Factory;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\View\View;
|
|
||||||
|
|
||||||
class SearchController extends Controller
|
class SearchController extends Controller
|
||||||
{
|
{
|
||||||
protected $entityRepo;
|
protected $entityRepo;
|
||||||
protected $viewService;
|
protected $viewService;
|
||||||
protected $searchService;
|
protected $searchService;
|
||||||
protected $entityContextManager;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchController constructor.
|
* SearchController constructor.
|
||||||
* @param EntityRepo $entityRepo
|
* @param EntityRepo $entityRepo
|
||||||
* @param ViewService $viewService
|
* @param ViewService $viewService
|
||||||
* @param SearchService $searchService
|
* @param SearchService $searchService
|
||||||
* @param EntityContextManager $entityContextManager
|
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(EntityRepo $entityRepo, ViewService $viewService, SearchService $searchService)
|
||||||
EntityRepo $entityRepo,
|
{
|
||||||
ViewService $viewService,
|
|
||||||
SearchService $searchService,
|
|
||||||
EntityContextManager $entityContextManager
|
|
||||||
) {
|
|
||||||
$this->entityRepo = $entityRepo;
|
$this->entityRepo = $entityRepo;
|
||||||
$this->viewService = $viewService;
|
$this->viewService = $viewService;
|
||||||
$this->searchService = $searchService;
|
$this->searchService = $searchService;
|
||||||
$this->entityContextManager = $entityContextManager;
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches all entities.
|
* Searches all entities.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return View
|
* @return \Illuminate\View\View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function search(Request $request)
|
public function search(Request $request)
|
||||||
@@ -47,16 +36,17 @@ class SearchController extends Controller
|
|||||||
$searchTerm = $request->get('term');
|
$searchTerm = $request->get('term');
|
||||||
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
$this->setPageTitle(trans('entities.search_for_term', ['term' => $searchTerm]));
|
||||||
|
|
||||||
$page = intval($request->get('page', '0')) ?: 1;
|
$page = $request->has('page') && is_int(intval($request->get('page'))) ? intval($request->get('page')) : 1;
|
||||||
$nextPageLink = url('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
$nextPageLink = baseUrl('/search?term=' . urlencode($searchTerm) . '&page=' . ($page+1));
|
||||||
|
|
||||||
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
$results = $this->searchService->searchEntities($searchTerm, 'all', $page, 20);
|
||||||
|
$hasNextPage = $this->searchService->searchEntities($searchTerm, 'all', $page+1, 20)['count'] > 0;
|
||||||
|
|
||||||
return view('search.all', [
|
return view('search/all', [
|
||||||
'entities' => $results['results'],
|
'entities' => $results['results'],
|
||||||
'totalResults' => $results['total'],
|
'totalResults' => $results['total'],
|
||||||
'searchTerm' => $searchTerm,
|
'searchTerm' => $searchTerm,
|
||||||
'hasNextPage' => $results['has_more'],
|
'hasNextPage' => $hasNextPage,
|
||||||
'nextPageLink' => $nextPageLink
|
'nextPageLink' => $nextPageLink
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -66,28 +56,28 @@ class SearchController extends Controller
|
|||||||
* Searches all entities within a book.
|
* Searches all entities within a book.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param integer $bookId
|
* @param integer $bookId
|
||||||
* @return View
|
* @return \Illuminate\View\View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function searchBook(Request $request, $bookId)
|
public function searchBook(Request $request, $bookId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->get('term', '');
|
||||||
$results = $this->searchService->searchBook($bookId, $term);
|
$results = $this->searchService->searchBook($bookId, $term);
|
||||||
return view('partials.entity-list', ['entities' => $results]);
|
return view('partials/entity-list', ['entities' => $results]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches all entities within a chapter.
|
* Searches all entities within a chapter.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param integer $chapterId
|
* @param integer $chapterId
|
||||||
* @return View
|
* @return \Illuminate\View\View
|
||||||
* @internal param string $searchTerm
|
* @internal param string $searchTerm
|
||||||
*/
|
*/
|
||||||
public function searchChapter(Request $request, $chapterId)
|
public function searchChapter(Request $request, $chapterId)
|
||||||
{
|
{
|
||||||
$term = $request->get('term', '');
|
$term = $request->get('term', '');
|
||||||
$results = $this->searchService->searchChapter($chapterId, $term);
|
$results = $this->searchService->searchChapter($chapterId, $term);
|
||||||
return view('partials.entity-list', ['entities' => $results]);
|
return view('partials/entity-list', ['entities' => $results]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -98,64 +88,23 @@ class SearchController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function searchEntitiesAjax(Request $request)
|
public function searchEntitiesAjax(Request $request)
|
||||||
{
|
{
|
||||||
$entityTypes = $request->filled('types') ? explode(',', $request->get('types')) : ['page', 'chapter', 'book'];
|
$entityTypes = $request->has('types') ? collect(explode(',', $request->get('types'))) : collect(['page', 'chapter', 'book']);
|
||||||
$searchTerm = $request->get('term', false);
|
$searchTerm = ($request->has('term') && trim($request->get('term')) !== '') ? $request->get('term') : false;
|
||||||
$permission = $request->get('permission', 'view');
|
|
||||||
|
|
||||||
// Search for entities otherwise show most popular
|
// Search for entities otherwise show most popular
|
||||||
if ($searchTerm !== false) {
|
if ($searchTerm !== false) {
|
||||||
$searchTerm .= ' {type:'. implode('|', $entityTypes) .'}';
|
$searchTerm .= ' {type:'. implode('|', $entityTypes->toArray()) .'}';
|
||||||
$entities = $this->searchService->searchEntities($searchTerm, 'all', 1, 20, $permission)['results'];
|
$entities = $this->searchService->searchEntities($searchTerm)['results'];
|
||||||
} else {
|
} else {
|
||||||
$entities = $this->viewService->getPopular(20, 0, $entityTypes, $permission);
|
$entityNames = $entityTypes->map(function ($type) {
|
||||||
|
return 'BookStack\\' . ucfirst($type);
|
||||||
|
})->toArray();
|
||||||
|
$entities = $this->viewService->getPopular(20, 0, $entityNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
return view('search.entity-ajax-list', ['entities' => $entities]);
|
return view('search/entity-ajax-list', ['entities' => $entities]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Search siblings items in the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @return Factory|View|mixed
|
|
||||||
*/
|
|
||||||
public function searchSiblings(Request $request)
|
|
||||||
{
|
|
||||||
$type = $request->get('entity_type', null);
|
|
||||||
$id = $request->get('entity_id', null);
|
|
||||||
|
|
||||||
$entity = $this->entityRepo->getById($type, $id);
|
|
||||||
if (!$entity) {
|
|
||||||
return $this->jsonError(trans('errors.entity_not_found'), 404);
|
|
||||||
}
|
|
||||||
|
|
||||||
$entities = [];
|
|
||||||
|
|
||||||
// Page in chapter
|
|
||||||
if ($entity->isA('page') && $entity->chapter) {
|
|
||||||
$entities = $this->entityRepo->getChapterChildren($entity->chapter);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Page in book or chapter
|
|
||||||
if (($entity->isA('page') && !$entity->chapter) || $entity->isA('chapter')) {
|
|
||||||
$entities = $this->entityRepo->getBookDirectChildren($entity->book);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Book
|
|
||||||
// Gets just the books in a shelf if shelf is in context
|
|
||||||
if ($entity->isA('book')) {
|
|
||||||
$contextShelf = $this->entityContextManager->getContextualShelfForBook($entity);
|
|
||||||
if ($contextShelf) {
|
|
||||||
$entities = $this->entityRepo->getBookshelfChildren($contextShelf);
|
|
||||||
} else {
|
|
||||||
$entities = $this->entityRepo->getAll('book');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shelve
|
|
||||||
if ($entity->isA('bookshelf')) {
|
|
||||||
$entities = $this->entityRepo->getAll('bookshelf');
|
|
||||||
}
|
|
||||||
|
|
||||||
return view('partials.entity-list-basic', ['entities' => $entities, 'style' => 'compact']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,11 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use BookStack\Uploads\ImageService;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
use Setting;
|
use Setting;
|
||||||
|
|
||||||
class SettingController extends Controller
|
class SettingController extends Controller
|
||||||
{
|
{
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SettingController constructor.
|
|
||||||
* @param $imageRepo
|
|
||||||
*/
|
|
||||||
public function __construct(ImageRepo $imageRepo)
|
|
||||||
{
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the settings.
|
* Display a listing of the settings.
|
||||||
* @return Response
|
* @return Response
|
||||||
@@ -29,15 +13,12 @@ class SettingController extends Controller
|
|||||||
public function index()
|
public function index()
|
||||||
{
|
{
|
||||||
$this->checkPermission('settings-manage');
|
$this->checkPermission('settings-manage');
|
||||||
$this->setPageTitle(trans('settings.settings'));
|
$this->setPageTitle('Settings');
|
||||||
|
|
||||||
// Get application version
|
// Get application version
|
||||||
$version = trim(file_get_contents(base_path('version')));
|
$version = trim(file_get_contents(base_path('version')));
|
||||||
|
|
||||||
return view('settings.index', [
|
return view('settings/index', ['version' => $version]);
|
||||||
'version' => $version,
|
|
||||||
'guestUser' => User::getDefault()
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,78 +30,16 @@ class SettingController extends Controller
|
|||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermission('settings-manage');
|
$this->checkPermission('settings-manage');
|
||||||
$this->validate($request, [
|
|
||||||
'app_logo' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Cycles through posted settings and update them
|
// Cycles through posted settings and update them
|
||||||
foreach ($request->all() as $name => $value) {
|
foreach ($request->all() as $name => $value) {
|
||||||
if (strpos($name, 'setting-') !== 0) {
|
if (strpos($name, 'setting-') !== 0) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$key = str_replace('setting-', '', trim($name));
|
$key = str_replace('setting-', '', trim($name));
|
||||||
setting()->put($key, $value);
|
Setting::put($key, $value);
|
||||||
}
|
|
||||||
|
|
||||||
// Update logo image if set
|
|
||||||
if ($request->has('app_logo')) {
|
|
||||||
$logoFile = $request->file('app_logo');
|
|
||||||
$this->imageRepo->destroyByType('system');
|
|
||||||
$image = $this->imageRepo->saveNew($logoFile, 'system', 0, null, 86);
|
|
||||||
setting()->put('app-logo', $image->url);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear logo image if requested
|
|
||||||
if ($request->get('app_logo_reset', null)) {
|
|
||||||
$this->imageRepo->destroyByType('system');
|
|
||||||
setting()->remove('app-logo');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session()->flash('success', trans('settings.settings_save_success'));
|
session()->flash('success', trans('settings.settings_save_success'));
|
||||||
return redirect('/settings');
|
return redirect('/settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the page for application maintenance.
|
|
||||||
* @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
|
|
||||||
*/
|
|
||||||
public function showMaintenance()
|
|
||||||
{
|
|
||||||
$this->checkPermission('settings-manage');
|
|
||||||
$this->setPageTitle(trans('settings.maint'));
|
|
||||||
|
|
||||||
// Get application version
|
|
||||||
$version = trim(file_get_contents(base_path('version')));
|
|
||||||
|
|
||||||
return view('settings.maintenance', ['version' => $version]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Action to clean-up images in the system.
|
|
||||||
* @param Request $request
|
|
||||||
* @param ImageService $imageService
|
|
||||||
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
|
|
||||||
*/
|
|
||||||
public function cleanupImages(Request $request, ImageService $imageService)
|
|
||||||
{
|
|
||||||
$this->checkPermission('settings-manage');
|
|
||||||
|
|
||||||
$checkRevisions = !($request->get('ignore_revisions', 'false') === 'true');
|
|
||||||
$dryRun = !($request->has('confirm'));
|
|
||||||
|
|
||||||
$imagesToDelete = $imageService->deleteUnusedImages($checkRevisions, $dryRun);
|
|
||||||
$deleteCount = count($imagesToDelete);
|
|
||||||
if ($deleteCount === 0) {
|
|
||||||
session()->flash('warning', trans('settings.maint_image_cleanup_nothing_found'));
|
|
||||||
return redirect('/settings/maintenance')->withInput();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($dryRun) {
|
|
||||||
session()->flash('cleanup-images-warning', trans('settings.maint_image_cleanup_warning', ['count' => $deleteCount]));
|
|
||||||
} else {
|
|
||||||
session()->flash('success', trans('settings.maint_image_cleanup_success', ['count' => $deleteCount]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect('/settings/maintenance#image-cleanup')->withInput();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Repos\TagRepo;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class TagController extends Controller
|
class TagController extends Controller
|
||||||
@@ -37,7 +37,7 @@ class TagController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getNameSuggestions(Request $request)
|
public function getNameSuggestions(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('search', false);
|
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||||
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
$suggestions = $this->tagRepo->getNameSuggestions($searchTerm);
|
||||||
return response()->json($suggestions);
|
return response()->json($suggestions);
|
||||||
}
|
}
|
||||||
@@ -49,9 +49,10 @@ class TagController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getValueSuggestions(Request $request)
|
public function getValueSuggestions(Request $request)
|
||||||
{
|
{
|
||||||
$searchTerm = $request->get('search', false);
|
$searchTerm = $request->has('search') ? $request->get('search') : false;
|
||||||
$tagName = $request->get('name', false);
|
$tagName = $request->has('name') ? $request->get('name') : false;
|
||||||
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
$suggestions = $this->tagRepo->getValueSuggestions($searchTerm, $tagName);
|
||||||
return response()->json($suggestions);
|
return response()->json($suggestions);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +1,27 @@
|
|||||||
<?php namespace BookStack\Http\Controllers;
|
<?php namespace BookStack\Http\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use Exception;
|
||||||
use BookStack\Auth\Access\UserInviteService;
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\UserUpdateException;
|
|
||||||
use BookStack\Uploads\ImageRepo;
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Http\Response;
|
use Illuminate\Http\Response;
|
||||||
|
use BookStack\Repos\UserRepo;
|
||||||
|
use BookStack\Services\SocialAuthService;
|
||||||
|
use BookStack\User;
|
||||||
|
|
||||||
class UserController extends Controller
|
class UserController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
protected $user;
|
protected $user;
|
||||||
protected $userRepo;
|
protected $userRepo;
|
||||||
protected $inviteService;
|
|
||||||
protected $imageRepo;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* UserController constructor.
|
* UserController constructor.
|
||||||
* @param User $user
|
* @param User $user
|
||||||
* @param UserRepo $userRepo
|
* @param UserRepo $userRepo
|
||||||
* @param UserInviteService $inviteService
|
|
||||||
* @param ImageRepo $imageRepo
|
|
||||||
*/
|
*/
|
||||||
public function __construct(User $user, UserRepo $userRepo, UserInviteService $inviteService, ImageRepo $imageRepo)
|
public function __construct(User $user, UserRepo $userRepo)
|
||||||
{
|
{
|
||||||
$this->user = $user;
|
$this->user = $user;
|
||||||
$this->userRepo = $userRepo;
|
$this->userRepo = $userRepo;
|
||||||
$this->inviteService = $inviteService;
|
|
||||||
$this->imageRepo = $imageRepo;
|
|
||||||
parent::__construct();
|
parent::__construct();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,14 +34,14 @@ class UserController extends Controller
|
|||||||
{
|
{
|
||||||
$this->checkPermission('users-manage');
|
$this->checkPermission('users-manage');
|
||||||
$listDetails = [
|
$listDetails = [
|
||||||
'order' => $request->get('order', 'asc'),
|
'order' => $request->has('order') ? $request->get('order') : 'asc',
|
||||||
'search' => $request->get('search', ''),
|
'search' => $request->has('search') ? $request->get('search') : '',
|
||||||
'sort' => $request->get('sort', 'name'),
|
'sort' => $request->has('sort') ? $request->get('sort') : 'name',
|
||||||
];
|
];
|
||||||
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
$users = $this->userRepo->getAllUsersPaginatedAndSorted(20, $listDetails);
|
||||||
$this->setPageTitle(trans('settings.users'));
|
$this->setPageTitle(trans('settings.users'));
|
||||||
$users->appends($listDetails);
|
$users->appends($listDetails);
|
||||||
return view('users.index', ['users' => $users, 'listDetails' => $listDetails]);
|
return view('users/index', ['users' => $users, 'listDetails' => $listDetails]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,14 +53,13 @@ class UserController extends Controller
|
|||||||
$this->checkPermission('users-manage');
|
$this->checkPermission('users-manage');
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$roles = $this->userRepo->getAllRoles();
|
$roles = $this->userRepo->getAllRoles();
|
||||||
return view('users.create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
return view('users/create', ['authMethod' => $authMethod, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store a newly created user in storage.
|
* Store a newly created user in storage.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws UserUpdateException
|
|
||||||
*/
|
*/
|
||||||
public function store(Request $request)
|
public function store(Request $request)
|
||||||
{
|
{
|
||||||
@@ -79,10 +70,8 @@ class UserController extends Controller
|
|||||||
];
|
];
|
||||||
|
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$sendInvite = ($request->get('send_invite', 'false') === 'true');
|
if ($authMethod === 'standard') {
|
||||||
|
$validationRules['password'] = 'required|min:5';
|
||||||
if ($authMethod === 'standard' && !$sendInvite) {
|
|
||||||
$validationRules['password'] = 'required|min:6';
|
|
||||||
$validationRules['password-confirm'] = 'required|same:password';
|
$validationRules['password-confirm'] = 'required|same:password';
|
||||||
} elseif ($authMethod === 'ldap') {
|
} elseif ($authMethod === 'ldap') {
|
||||||
$validationRules['external_auth_id'] = 'required';
|
$validationRules['external_auth_id'] = 'required';
|
||||||
@@ -92,23 +81,29 @@ class UserController extends Controller
|
|||||||
$user = $this->user->fill($request->all());
|
$user = $this->user->fill($request->all());
|
||||||
|
|
||||||
if ($authMethod === 'standard') {
|
if ($authMethod === 'standard') {
|
||||||
$user->password = bcrypt($request->get('password', str_random(32)));
|
$user->password = bcrypt($request->get('password'));
|
||||||
} elseif ($authMethod === 'ldap') {
|
} elseif ($authMethod === 'ldap') {
|
||||||
$user->external_auth_id = $request->get('external_auth_id');
|
$user->external_auth_id = $request->get('external_auth_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
|
|
||||||
if ($sendInvite) {
|
if ($request->has('roles')) {
|
||||||
$this->inviteService->sendInvitation($user);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($request->filled('roles')) {
|
|
||||||
$roles = $request->get('roles');
|
$roles = $request->get('roles');
|
||||||
$this->userRepo->setUserRoles($user, $roles);
|
$user->roles()->sync($roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->userRepo->downloadAndAssignUserAvatar($user);
|
// Get avatar from gravatar and save
|
||||||
|
if (!config('services.disable_services')) {
|
||||||
|
try {
|
||||||
|
$avatar = \Images::saveUserGravatar($user);
|
||||||
|
$user->avatar()->associate($avatar);
|
||||||
|
$user->save();
|
||||||
|
} catch (Exception $e) {
|
||||||
|
\Log::error('Failed to save user gravatar image');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return redirect('/settings/users');
|
return redirect('/settings/users');
|
||||||
}
|
}
|
||||||
@@ -116,12 +111,14 @@ class UserController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Show the form for editing the specified user.
|
* Show the form for editing the specified user.
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @param \BookStack\Auth\Access\SocialAuthService $socialAuthService
|
* @param SocialAuthService $socialAuthService
|
||||||
* @return Response
|
* @return Response
|
||||||
*/
|
*/
|
||||||
public function edit($id, SocialAuthService $socialAuthService)
|
public function edit($id, SocialAuthService $socialAuthService)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||||
|
return $this->currentUser->id == $id;
|
||||||
|
});
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
$user = $this->user->findOrFail($id);
|
||||||
|
|
||||||
@@ -130,80 +127,61 @@ class UserController extends Controller
|
|||||||
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
$activeSocialDrivers = $socialAuthService->getActiveDrivers();
|
||||||
$this->setPageTitle(trans('settings.user_profile'));
|
$this->setPageTitle(trans('settings.user_profile'));
|
||||||
$roles = $this->userRepo->getAllRoles();
|
$roles = $this->userRepo->getAllRoles();
|
||||||
return view('users.edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
return view('users/edit', ['user' => $user, 'activeSocialDrivers' => $activeSocialDrivers, 'authMethod' => $authMethod, 'roles' => $roles]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the specified user in storage.
|
* Update the specified user in storage.
|
||||||
* @param Request $request
|
* @param Request $request
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws UserUpdateException
|
|
||||||
* @throws \BookStack\Exceptions\ImageUploadException
|
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, $id)
|
public function update(Request $request, $id)
|
||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||||
|
return $this->currentUser->id == $id;
|
||||||
|
});
|
||||||
|
|
||||||
$this->validate($request, [
|
$this->validate($request, [
|
||||||
'name' => 'min:2',
|
'name' => 'min:2',
|
||||||
'email' => 'min:2|email|unique:users,email,' . $id,
|
'email' => 'min:2|email|unique:users,email,' . $id,
|
||||||
'password' => 'min:6|required_with:password_confirm',
|
'password' => 'min:5|required_with:password_confirm',
|
||||||
'password-confirm' => 'same:password|required_with:password',
|
'password-confirm' => 'same:password|required_with:password',
|
||||||
'setting' => 'array',
|
'setting' => 'array'
|
||||||
'profile_image' => $this->imageRepo->getImageValidationRules(),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->user->findOrFail($id);
|
||||||
$user->fill($request->except(['email']));
|
$user->fill($request->all());
|
||||||
|
|
||||||
// Email updates
|
|
||||||
if (userCan('users-manage') && $request->filled('email')) {
|
|
||||||
$user->email = $request->get('email');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Role updates
|
// Role updates
|
||||||
if (userCan('users-manage') && $request->filled('roles')) {
|
if (userCan('users-manage') && $request->has('roles')) {
|
||||||
$roles = $request->get('roles');
|
$roles = $request->get('roles');
|
||||||
$this->userRepo->setUserRoles($user, $roles);
|
$user->roles()->sync($roles);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Password updates
|
// Password updates
|
||||||
if ($request->filled('password')) {
|
if ($request->has('password') && $request->get('password') != '') {
|
||||||
$password = $request->get('password');
|
$password = $request->get('password');
|
||||||
$user->password = bcrypt($password);
|
$user->password = bcrypt($password);
|
||||||
}
|
}
|
||||||
|
|
||||||
// External auth id updates
|
// External auth id updates
|
||||||
if ($this->currentUser->can('users-manage') && $request->filled('external_auth_id')) {
|
if ($this->currentUser->can('users-manage') && $request->has('external_auth_id')) {
|
||||||
$user->external_auth_id = $request->get('external_auth_id');
|
$user->external_auth_id = $request->get('external_auth_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save an user-specific settings
|
// Save an user-specific settings
|
||||||
if ($request->filled('setting')) {
|
if ($request->has('setting')) {
|
||||||
foreach ($request->get('setting') as $key => $value) {
|
foreach ($request->get('setting') as $key => $value) {
|
||||||
setting()->putUser($user, $key, $value);
|
setting()->putUser($user, $key, $value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save profile image if in request
|
|
||||||
if ($request->has('profile_image')) {
|
|
||||||
$imageUpload = $request->file('profile_image');
|
|
||||||
$this->imageRepo->destroyImage($user->avatar);
|
|
||||||
$image = $this->imageRepo->saveNew($imageUpload, 'user', $user->id);
|
|
||||||
$user->image_id = $image->id;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete the profile image if set to
|
|
||||||
if ($request->has('profile_image_reset')) {
|
|
||||||
$this->imageRepo->destroyImage($user->avatar);
|
|
||||||
}
|
|
||||||
|
|
||||||
$user->save();
|
$user->save();
|
||||||
session()->flash('success', trans('settings.users_edit_success'));
|
session()->flash('success', trans('settings.users_edit_success'));
|
||||||
|
|
||||||
$redirectUrl = userCan('users-manage') ? '/settings/users' : ('/settings/users/' . $user->id);
|
$redirectUrl = userCan('users-manage') ? '/settings/users' : '/settings/users/' . $user->id;
|
||||||
return redirect($redirectUrl);
|
return redirect($redirectUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,23 +192,26 @@ class UserController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function delete($id)
|
public function delete($id)
|
||||||
{
|
{
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||||
|
return $this->currentUser->id == $id;
|
||||||
|
});
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->user->findOrFail($id);
|
||||||
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
$this->setPageTitle(trans('settings.users_delete_named', ['userName' => $user->name]));
|
||||||
return view('users.delete', ['user' => $user]);
|
return view('users/delete', ['user' => $user]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the specified user from storage.
|
* Remove the specified user from storage.
|
||||||
* @param int $id
|
* @param int $id
|
||||||
* @return Response
|
* @return Response
|
||||||
* @throws \Exception
|
|
||||||
*/
|
*/
|
||||||
public function destroy($id)
|
public function destroy($id)
|
||||||
{
|
{
|
||||||
$this->preventAccessForDemoUsers();
|
$this->preventAccessForDemoUsers();
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
$this->checkPermissionOr('users-manage', function () use ($id) {
|
||||||
|
return $this->currentUser->id == $id;
|
||||||
|
});
|
||||||
|
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
|
|
||||||
@@ -258,129 +239,14 @@ class UserController extends Controller
|
|||||||
public function showProfilePage($id)
|
public function showProfilePage($id)
|
||||||
{
|
{
|
||||||
$user = $this->userRepo->getById($id);
|
$user = $this->userRepo->getById($id);
|
||||||
|
|
||||||
$userActivity = $this->userRepo->getActivity($user);
|
$userActivity = $this->userRepo->getActivity($user);
|
||||||
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
$recentlyCreated = $this->userRepo->getRecentlyCreated($user, 5, 0);
|
||||||
$assetCounts = $this->userRepo->getAssetCounts($user);
|
$assetCounts = $this->userRepo->getAssetCounts($user);
|
||||||
|
return view('users/profile', [
|
||||||
return view('users.profile', [
|
|
||||||
'user' => $user,
|
'user' => $user,
|
||||||
'activity' => $userActivity,
|
'activity' => $userActivity,
|
||||||
'recentlyCreated' => $recentlyCreated,
|
'recentlyCreated' => $recentlyCreated,
|
||||||
'assetCounts' => $assetCounts
|
'assetCounts' => $assetCounts
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the user's preferred book-list display setting.
|
|
||||||
* @param $id
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
|
||||||
public function switchBookView($id, Request $request)
|
|
||||||
{
|
|
||||||
return $this->switchViewType($id, $request, 'books');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the user's preferred shelf-list display setting.
|
|
||||||
* @param $id
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
|
||||||
public function switchShelfView($id, Request $request)
|
|
||||||
{
|
|
||||||
return $this->switchViewType($id, $request, 'bookshelves');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For a type of list, switch with stored view type for a user.
|
|
||||||
* @param integer $userId
|
|
||||||
* @param Request $request
|
|
||||||
* @param string $listName
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
|
||||||
protected function switchViewType($userId, Request $request, string $listName)
|
|
||||||
{
|
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
|
||||||
|
|
||||||
$viewType = $request->get('view_type');
|
|
||||||
if (!in_array($viewType, ['grid', 'list'])) {
|
|
||||||
$viewType = 'list';
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->userRepo->getById($userId);
|
|
||||||
$key = $listName . '_view_type';
|
|
||||||
setting()->putUser($user, $key, $viewType);
|
|
||||||
|
|
||||||
return redirect()->back(302, [], "/settings/users/$userId");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change the stored sort type for a particular view.
|
|
||||||
* @param string $id
|
|
||||||
* @param string $type
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
|
||||||
public function changeSort(string $id, string $type, Request $request)
|
|
||||||
{
|
|
||||||
$validSortTypes = ['books', 'bookshelves'];
|
|
||||||
if (!in_array($type, $validSortTypes)) {
|
|
||||||
return redirect()->back(500);
|
|
||||||
}
|
|
||||||
return $this->changeListSort($id, $request, $type);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the stored section expansion preference for the given user.
|
|
||||||
* @param string $id
|
|
||||||
* @param string $key
|
|
||||||
* @param Request $request
|
|
||||||
* @return \Illuminate\Contracts\Routing\ResponseFactory|\Symfony\Component\HttpFoundation\Response
|
|
||||||
*/
|
|
||||||
public function updateExpansionPreference(string $id, string $key, Request $request)
|
|
||||||
{
|
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $id);
|
|
||||||
$keyWhitelist = ['home-details'];
|
|
||||||
if (!in_array($key, $keyWhitelist)) {
|
|
||||||
return response("Invalid key", 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
$newState = $request->get('expand', 'false');
|
|
||||||
|
|
||||||
$user = $this->user->findOrFail($id);
|
|
||||||
setting()->putUser($user, 'section_expansion#' . $key, $newState);
|
|
||||||
return response("", 204);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Changed the stored preference for a list sort order.
|
|
||||||
* @param int $userId
|
|
||||||
* @param Request $request
|
|
||||||
* @param string $listName
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
|
||||||
protected function changeListSort(int $userId, Request $request, string $listName)
|
|
||||||
{
|
|
||||||
$this->checkPermissionOrCurrentUser('users-manage', $userId);
|
|
||||||
|
|
||||||
$sort = $request->get('sort');
|
|
||||||
if (!in_array($sort, ['name', 'created_at', 'updated_at'])) {
|
|
||||||
$sort = 'name';
|
|
||||||
}
|
|
||||||
|
|
||||||
$order = $request->get('order');
|
|
||||||
if (!in_array($order, ['asc', 'desc'])) {
|
|
||||||
$order = 'asc';
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = $this->user->findOrFail($userId);
|
|
||||||
$sortKey = $listName . '_sort';
|
|
||||||
$orderKey = $listName . '_sort_order';
|
|
||||||
setting()->putUser($user, $sortKey, $sort);
|
|
||||||
setting()->putUser($user, $orderKey, $order);
|
|
||||||
|
|
||||||
return redirect()->back(302, [], "/settings/users/$userId");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,9 +13,8 @@ class Kernel extends HttpKernel
|
|||||||
*/
|
*/
|
||||||
protected $middleware = [
|
protected $middleware = [
|
||||||
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
\Illuminate\Foundation\Http\Middleware\CheckForMaintenanceMode::class,
|
||||||
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
|
\Illuminate\Session\Middleware\StartSession::class,
|
||||||
\BookStack\Http\Middleware\TrimStrings::class,
|
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
||||||
\BookStack\Http\Middleware\TrustProxies::class,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -27,8 +26,6 @@ class Kernel extends HttpKernel
|
|||||||
'web' => [
|
'web' => [
|
||||||
\BookStack\Http\Middleware\EncryptCookies::class,
|
\BookStack\Http\Middleware\EncryptCookies::class,
|
||||||
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
|
||||||
\Illuminate\Session\Middleware\StartSession::class,
|
|
||||||
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
|
|
||||||
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
\BookStack\Http\Middleware\VerifyCsrfToken::class,
|
||||||
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
\Illuminate\Routing\Middleware\SubstituteBindings::class,
|
||||||
\BookStack\Http\Middleware\Localization::class
|
\BookStack\Http\Middleware\Localization::class
|
||||||
@@ -45,11 +42,10 @@ class Kernel extends HttpKernel
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $routeMiddleware = [
|
protected $routeMiddleware = [
|
||||||
|
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
||||||
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
'auth' => \BookStack\Http\Middleware\Authenticate::class,
|
||||||
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
|
||||||
'can' => \Illuminate\Auth\Middleware\Authorize::class,
|
|
||||||
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
'guest' => \BookStack\Http\Middleware\RedirectIfAuthenticated::class,
|
||||||
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
|
|
||||||
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
|
'perm' => \BookStack\Http\Middleware\PermissionMiddleware::class
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,18 +30,15 @@ class Authenticate
|
|||||||
*/
|
*/
|
||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
if ($this->auth->check()) {
|
if ($this->auth->check() && setting('registration-confirmation') && !$this->auth->user()->email_confirmed) {
|
||||||
$requireConfirmation = (setting('registration-confirmation') || setting('registration-restrict'));
|
return redirect(baseUrl('/register/confirm/awaiting'));
|
||||||
if ($requireConfirmation && !$this->auth->user()->email_confirmed) {
|
|
||||||
return redirect('/register/confirm/awaiting');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!hasAppAccess()) {
|
if ($this->auth->guest() && !setting('app-public')) {
|
||||||
if ($request->ajax()) {
|
if ($request->ajax()) {
|
||||||
return response('Unauthorized.', 401);
|
return response('Unauthorized.', 401);
|
||||||
} else {
|
} else {
|
||||||
return redirect()->guest(url('/login'));
|
return redirect()->guest(baseUrl('/login'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Middleware;
|
namespace BookStack\Http\Middleware;
|
||||||
|
|
||||||
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
|
use Illuminate\Cookie\Middleware\EncryptCookies as BaseEncrypter;
|
||||||
|
|
||||||
class EncryptCookies extends Middleware
|
class EncryptCookies extends BaseEncrypter
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The names of the cookies that should not be encrypted.
|
* The names of the cookies that should not be encrypted.
|
||||||
|
|||||||
@@ -2,43 +2,9 @@
|
|||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Closure;
|
use Closure;
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class Localization
|
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',
|
|
||||||
'ru' => 'ru',
|
|
||||||
'sk' => 'sk_SK',
|
|
||||||
'sv' => 'sv_SE',
|
|
||||||
'uk' => 'uk_UA',
|
|
||||||
'zh_CN' => 'zh_CN',
|
|
||||||
'zh_TW' => 'zh_TW',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle an incoming request.
|
* Handle an incoming request.
|
||||||
*
|
*
|
||||||
@@ -49,66 +15,19 @@ class Localization
|
|||||||
public function handle($request, Closure $next)
|
public function handle($request, Closure $next)
|
||||||
{
|
{
|
||||||
$defaultLang = config('app.locale');
|
$defaultLang = config('app.locale');
|
||||||
config()->set('app.default_locale', $defaultLang);
|
if (user()->isDefault()) {
|
||||||
|
$locale = $defaultLang;
|
||||||
if (user()->isDefault() && config('app.auto_detect_locale')) {
|
$availableLocales = config('app.locales');
|
||||||
$locale = $this->autoDetectLocale($request, $defaultLang);
|
foreach ($request->getLanguages() as $lang) {
|
||||||
|
if (!in_array($lang, $availableLocales)) continue;
|
||||||
|
$locale = $lang;
|
||||||
|
break;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
$locale = setting()->getUser(user(), 'language', $defaultLang);
|
||||||
}
|
}
|
||||||
|
|
||||||
config()->set('app.lang', str_replace('_', '-', $this->getLocaleIso($locale)));
|
|
||||||
|
|
||||||
// Set text direction
|
|
||||||
if (in_array($locale, $this->rtlLocales)) {
|
|
||||||
config()->set('app.rtl', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
app()->setLocale($locale);
|
app()->setLocale($locale);
|
||||||
Carbon::setLocale($locale);
|
Carbon::setLocale($locale);
|
||||||
$this->setSystemDateLocale($locale);
|
|
||||||
return $next($request);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ISO version of a BookStack language name
|
|
||||||
* @param string $locale
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function getLocaleIso(string $locale)
|
|
||||||
{
|
|
||||||
return $this->localeMap[$locale] ?? $locale;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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->getLocaleIso($locale);
|
|
||||||
$set = setlocale(LC_TIME, $systemLocale);
|
|
||||||
if ($set === false) {
|
|
||||||
setlocale(LC_TIME, $systemLocale . '.utf8');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Middleware;
|
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
|
|
||||||
|
|
||||||
class TrimStrings extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The names of the attributes that should not be trimmed.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $except = [
|
|
||||||
'password',
|
|
||||||
'password_confirmation',
|
|
||||||
'password-confirm',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Http\Middleware;
|
|
||||||
|
|
||||||
use Closure;
|
|
||||||
use Fideloper\Proxy\TrustProxies as Middleware;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
|
|
||||||
class TrustProxies extends Middleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* The trusted proxies for this application.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $proxies;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The current proxy header mappings.
|
|
||||||
*
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $headers = [
|
|
||||||
Request::HEADER_FORWARDED => 'FORWARDED',
|
|
||||||
Request::HEADER_X_FORWARDED_FOR => 'X_FORWARDED_FOR',
|
|
||||||
Request::HEADER_X_FORWARDED_HOST => 'X_FORWARDED_HOST',
|
|
||||||
Request::HEADER_X_FORWARDED_PORT => 'X_FORWARDED_PORT',
|
|
||||||
Request::HEADER_X_FORWARDED_PROTO => 'X_FORWARDED_PROTO',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the request, Set the correct user-configured proxy information.
|
|
||||||
* @param Request $request
|
|
||||||
* @param Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle($request, Closure $next)
|
|
||||||
{
|
|
||||||
$setProxies = config('app.proxies');
|
|
||||||
if ($setProxies !== '**' && $setProxies !== '*' && $setProxies !== '') {
|
|
||||||
$setProxies = explode(',', $setProxies);
|
|
||||||
}
|
|
||||||
$this->proxies = $setProxies;
|
|
||||||
|
|
||||||
return parent::handle($request, $next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
namespace BookStack\Http\Middleware;
|
namespace BookStack\Http\Middleware;
|
||||||
|
|
||||||
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
|
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as BaseVerifier;
|
||||||
|
|
||||||
class VerifyCsrfToken extends Middleware
|
class VerifyCsrfToken extends BaseVerifier
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The URIs that should be excluded from CSRF verification.
|
* The URIs that should be excluded from CSRF verification.
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
<?php namespace BookStack\Http;
|
|
||||||
|
|
||||||
use Illuminate\Http\Request as LaravelRequest;
|
|
||||||
|
|
||||||
class Request extends LaravelRequest
|
|
||||||
{
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Override the default request methods to get the scheme and host
|
|
||||||
* to set the custom APP_URL, if set.
|
|
||||||
* @return \Illuminate\Config\Repository|mixed|string
|
|
||||||
*/
|
|
||||||
public function getSchemeAndHttpHost()
|
|
||||||
{
|
|
||||||
$base = config('app.url', null);
|
|
||||||
|
|
||||||
if ($base) {
|
|
||||||
$base = trim($base, '/');
|
|
||||||
} else {
|
|
||||||
$base = $this->getScheme().'://'.$this->getHttpHost();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $base;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
21
app/Image.php
Normal file
21
app/Image.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php namespace BookStack;
|
||||||
|
|
||||||
|
use Images;
|
||||||
|
|
||||||
|
class Image extends Ownable
|
||||||
|
{
|
||||||
|
|
||||||
|
protected $fillable = ['name'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a thumbnail for this image.
|
||||||
|
* @param int $width
|
||||||
|
* @param int $height
|
||||||
|
* @param bool|false $keepRatio
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getThumb($width, $height, $keepRatio = false)
|
||||||
|
{
|
||||||
|
return Images::getThumbnail($this, $width, $height, $keepRatio);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,4 @@
|
|||||||
<?php namespace BookStack\Auth\Permissions;
|
<?php namespace BookStack;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
|
||||||
use BookStack\Entities\Entity;
|
|
||||||
use BookStack\Model;
|
|
||||||
|
|
||||||
class JointPermission extends Model
|
class JointPermission extends Model
|
||||||
{
|
{
|
||||||
@@ -15,4 +15,5 @@ class Model extends EloquentModel
|
|||||||
{
|
{
|
||||||
return parent::getAttributeFromArray($key);
|
return parent::getAttributeFromArray($key);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user