mirror of
https://github.com/BookStackApp/BookStack.git
synced 2025-06-18 02:12:30 +08:00
Merge branch 'development' into default-templates
This commit is contained in:
@ -37,8 +37,10 @@ MAIL_FROM=bookstack@example.com
|
|||||||
# SMTP mail options
|
# SMTP mail options
|
||||||
# These settings can be checked using the "Send a Test Email"
|
# These settings can be checked using the "Send a Test Email"
|
||||||
# feature found in the "Settings > Maintenance" area of the system.
|
# feature found in the "Settings > Maintenance" area of the system.
|
||||||
|
# For more detailed documentation on mail options, refer to:
|
||||||
|
# https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
|
||||||
MAIL_HOST=localhost
|
MAIL_HOST=localhost
|
||||||
MAIL_PORT=1025
|
MAIL_PORT=587
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
@ -3,6 +3,10 @@
|
|||||||
# Each option is shown with it's default value.
|
# Each option is shown with it's default value.
|
||||||
# Do not copy this whole file to use as your '.env' file.
|
# Do not copy this whole file to use as your '.env' file.
|
||||||
|
|
||||||
|
# The details here only serve as a quick reference.
|
||||||
|
# Please refer to the BookStack documentation for full details:
|
||||||
|
# https://www.bookstackapp.com/docs/
|
||||||
|
|
||||||
# Application environment
|
# Application environment
|
||||||
# Can be 'production', 'development', 'testing' or 'demo'
|
# Can be 'production', 'development', 'testing' or 'demo'
|
||||||
APP_ENV=production
|
APP_ENV=production
|
||||||
@ -65,20 +69,20 @@ DB_PASSWORD=database_user_password
|
|||||||
# certificate itself (Common Name or Subject Alternative Name).
|
# certificate itself (Common Name or Subject Alternative Name).
|
||||||
MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
|
MYSQL_ATTR_SSL_CA="/path/to/ca.pem"
|
||||||
|
|
||||||
# Mail system to use
|
# Mail configuration
|
||||||
# Can be 'smtp' or 'sendmail'
|
# Refer to https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration
|
||||||
MAIL_DRIVER=smtp
|
MAIL_DRIVER=smtp
|
||||||
|
MAIL_FROM=bookstack@example.com
|
||||||
# Mail sending options
|
|
||||||
MAIL_FROM=mail@bookstackapp.com
|
|
||||||
MAIL_FROM_NAME=BookStack
|
MAIL_FROM_NAME=BookStack
|
||||||
|
|
||||||
# SMTP mail options
|
|
||||||
MAIL_HOST=localhost
|
MAIL_HOST=localhost
|
||||||
MAIL_PORT=1025
|
MAIL_PORT=587
|
||||||
MAIL_USERNAME=null
|
MAIL_USERNAME=null
|
||||||
MAIL_PASSWORD=null
|
MAIL_PASSWORD=null
|
||||||
MAIL_ENCRYPTION=null
|
MAIL_ENCRYPTION=null
|
||||||
|
MAIL_VERIFY_SSL=true
|
||||||
|
|
||||||
|
MAIL_SENDMAIL_COMMAND="/usr/sbin/sendmail -bs"
|
||||||
|
|
||||||
# Cache & Session driver to use
|
# Cache & Session driver to use
|
||||||
# Can be 'file', 'database', 'memcached' or 'redis'
|
# Can be 'file', 'database', 'memcached' or 'redis'
|
||||||
@ -268,6 +272,8 @@ OIDC_DUMP_USER_DETAILS=false
|
|||||||
OIDC_USER_TO_GROUPS=false
|
OIDC_USER_TO_GROUPS=false
|
||||||
OIDC_GROUPS_CLAIM=groups
|
OIDC_GROUPS_CLAIM=groups
|
||||||
OIDC_REMOVE_FROM_GROUPS=false
|
OIDC_REMOVE_FROM_GROUPS=false
|
||||||
|
OIDC_EXTERNAL_ID_CLAIM=sub
|
||||||
|
OIDC_END_SESSION_ENDPOINT=false
|
||||||
|
|
||||||
# Disable default third-party services such as Gravatar and Draw.IO
|
# Disable default third-party services such as Gravatar and Draw.IO
|
||||||
# Service-specific options will override this option
|
# Service-specific options will override this option
|
||||||
@ -318,6 +324,13 @@ FILE_UPLOAD_SIZE_LIMIT=50
|
|||||||
# Can be 'a4' or 'letter'.
|
# Can be 'a4' or 'letter'.
|
||||||
EXPORT_PAGE_SIZE=a4
|
EXPORT_PAGE_SIZE=a4
|
||||||
|
|
||||||
|
# Set path to wkhtmltopdf binary for PDF generation.
|
||||||
|
# Can be 'false' or a path path like: '/home/bins/wkhtmltopdf'
|
||||||
|
# When false, BookStack will attempt to find a wkhtmltopdf in the application
|
||||||
|
# root folder then fall back to the default dompdf renderer if no binary exists.
|
||||||
|
# Only used if 'ALLOW_UNTRUSTED_SERVER_FETCHING=true' which disables security protections.
|
||||||
|
WKHTMLTOPDF=false
|
||||||
|
|
||||||
# Allow <script> tags in page content
|
# Allow <script> tags in page content
|
||||||
# Note, if set to 'true' the page editor may still escape scripts.
|
# Note, if set to 'true' the page editor may still escape scripts.
|
||||||
ALLOW_CONTENT_SCRIPTS=false
|
ALLOW_CONTENT_SCRIPTS=false
|
||||||
@ -347,6 +360,15 @@ ALLOWED_IFRAME_HOSTS=null
|
|||||||
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
|
# Current host and source for the "DRAWIO" setting will be auto-appended to the sources configured.
|
||||||
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
|
ALLOWED_IFRAME_SOURCES="https://*.draw.io https://*.youtube.com https://*.youtube-nocookie.com https://*.vimeo.com"
|
||||||
|
|
||||||
|
# A list of the sources/hostnames that can be reached by application SSR calls.
|
||||||
|
# This is used wherever users can provide URLs/hosts in-platform, like for webhooks.
|
||||||
|
# Host-specific functionality (usually controlled via other options) like auth
|
||||||
|
# or user avatars for example, won't use this list.
|
||||||
|
# Space seperated if multiple. Can use '*' as a wildcard.
|
||||||
|
# Values will be compared prefix-matched, case-insensitive, against called SSR urls.
|
||||||
|
# Defaults to allow all hosts.
|
||||||
|
ALLOWED_SSR_HOSTS="*"
|
||||||
|
|
||||||
# The default and maximum item-counts for listing API requests.
|
# The default and maximum item-counts for listing API requests.
|
||||||
API_DEFAULT_ITEM_COUNT=100
|
API_DEFAULT_ITEM_COUNT=100
|
||||||
API_MAX_ITEM_COUNT=500
|
API_MAX_ITEM_COUNT=500
|
||||||
|
33
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
33
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@ -1,7 +1,14 @@
|
|||||||
name: Bug Report
|
name: Bug Report
|
||||||
description: Create a report to help us improve or fix things
|
description: Create a report to help us fix bugs & issues in existing supported functionality
|
||||||
labels: [":bug: Bug"]
|
labels: [":bug: Bug"]
|
||||||
body:
|
body:
|
||||||
|
- type: markdown
|
||||||
|
attributes:
|
||||||
|
value: |
|
||||||
|
Thanks for taking the time to fill out a bug report!
|
||||||
|
Please note that this form is for reporting bugs in existing supported functionality.
|
||||||
|
|
||||||
|
If you are reporting something that's not an issue in functionality we've previously supported and/or is simply something different to your expectations, then it may be more appropriate to raise via a feature or support request instead.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: description
|
id: description
|
||||||
attributes:
|
attributes:
|
||||||
@ -13,7 +20,7 @@ body:
|
|||||||
id: reproduction
|
id: reproduction
|
||||||
attributes:
|
attributes:
|
||||||
label: Steps to Reproduce
|
label: Steps to Reproduce
|
||||||
description: Detail the steps that would replicate this issue
|
description: Detail the steps that would replicate this issue.
|
||||||
placeholder: |
|
placeholder: |
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
@ -32,7 +39,7 @@ body:
|
|||||||
id: context
|
id: context
|
||||||
attributes:
|
attributes:
|
||||||
label: Screenshots or Additional Context
|
label: Screenshots or Additional Context
|
||||||
description: Provide any additional context and screenshots here to help us solve this issue
|
description: Provide any additional context and screenshots here to help us solve this issue.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
- type: input
|
||||||
@ -48,23 +55,7 @@ body:
|
|||||||
id: bsversion
|
id: bsversion
|
||||||
attributes:
|
attributes:
|
||||||
label: Exact BookStack Version
|
label: Exact BookStack Version
|
||||||
description: This can be found in the settings view of BookStack. Please provide an exact version.
|
description: This can be found in the settings view of BookStack. Please provide an exact version(s) you've tested on.
|
||||||
placeholder: (eg. v21.08.5)
|
placeholder: (eg. v23.06.7)
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: phpversion
|
|
||||||
attributes:
|
|
||||||
label: PHP Version
|
|
||||||
description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that relevant to the issue.
|
|
||||||
placeholder: (eg. 7.4)
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
|
||||||
id: hosting
|
|
||||||
attributes:
|
|
||||||
label: Hosting Environment
|
|
||||||
description: Describe your hosting environment as much as possible including any proxies used (If applicable).
|
|
||||||
placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
8
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@ -33,9 +33,9 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Have you searched for an existing open/closed issue?
|
label: Have you searched for an existing open/closed issue?
|
||||||
description: |
|
description: |
|
||||||
To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundemental benefit/goal of your request.
|
To help us keep these issues under control, please ensure you have first [searched our issue list](https://github.com/BookStackApp/BookStack/issues?q=is%3Aissue) for any existing issues that cover the fundamental benefit/goal of your request.
|
||||||
options:
|
options:
|
||||||
- label: I have searched for existing issues and none cover my fundemental request
|
- label: I have searched for existing issues and none cover my fundamental request
|
||||||
required: true
|
required: true
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: existing_usage
|
id: existing_usage
|
||||||
@ -43,8 +43,8 @@ body:
|
|||||||
label: How long have you been using BookStack?
|
label: How long have you been using BookStack?
|
||||||
options:
|
options:
|
||||||
- Not using yet, just scoping
|
- Not using yet, just scoping
|
||||||
- 0 to 6 months
|
- Under 3 months
|
||||||
- 6 months to 1 year
|
- 3 months to 1 year
|
||||||
- 1 to 5 years
|
- 1 to 5 years
|
||||||
- Over 5 years
|
- Over 5 years
|
||||||
validations:
|
validations:
|
||||||
|
12
.github/ISSUE_TEMPLATE/support_request.yml
vendored
12
.github/ISSUE_TEMPLATE/support_request.yml
vendored
@ -33,7 +33,7 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Exact BookStack Version
|
label: Exact BookStack Version
|
||||||
description: This can be found in the settings view of BookStack. Please provide an exact version.
|
description: This can be found in the settings view of BookStack. Please provide an exact version.
|
||||||
placeholder: (eg. v21.08.5)
|
placeholder: (eg. v23.06.7)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
- type: textarea
|
- type: textarea
|
||||||
@ -44,19 +44,11 @@ body:
|
|||||||
placeholder: Be sure to remove any confidential details in your logs
|
placeholder: Be sure to remove any confidential details in your logs
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: false
|
||||||
- type: input
|
|
||||||
id: phpversion
|
|
||||||
attributes:
|
|
||||||
label: PHP Version
|
|
||||||
description: Keep in mind your command-line PHP version may differ to that of your webserver. Provide that most relevant to the issue.
|
|
||||||
placeholder: (eg. 7.4)
|
|
||||||
validations:
|
|
||||||
required: false
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: hosting
|
id: hosting
|
||||||
attributes:
|
attributes:
|
||||||
label: Hosting Environment
|
label: Hosting Environment
|
||||||
description: Describe your hosting environment as much as possible including any proxies used (If applicable).
|
description: Describe your hosting environment as much as possible including any proxies used (If applicable).
|
||||||
placeholder: (eg. Ubuntu 20.04 VPS, installed using official installation script)
|
placeholder: (eg. PHP8.1 on Ubuntu 22.04 VPS, installed using official installation script)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
13
.github/SECURITY.md
vendored
13
.github/SECURITY.md
vendored
@ -15,15 +15,10 @@ If you'd like to be notified of new potential security concerns you can [sign-up
|
|||||||
If you've found an issue that likely has no impact to existing users (For example, in a development-only branch)
|
If you've found an issue that likely has no impact to existing users (For example, in a development-only branch)
|
||||||
feel free to raise it via a standard GitHub bug report issue.
|
feel free to raise it via a standard GitHub bug report issue.
|
||||||
|
|
||||||
If the issue could have a security impact to BookStack instances, please use one of the below
|
If the issue could have a security impact to BookStack instances,
|
||||||
methods to report the vulnerability:
|
please directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown).
|
||||||
|
You will need to log in to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown).
|
||||||
- Directly contact the lead maintainer [@ssddanbrown](https://github.com/ssddanbrown).
|
Alternatively you can send a DM via Mastodon to [@danb@fosstodon.org](https://fosstodon.org/@danb).
|
||||||
- You will need to login to be able to see the email address on the [GitHub profile page](https://github.com/ssddanbrown).
|
|
||||||
- Alternatively you can send a DM via Twitter to [@ssddanbrown](https://twitter.com/ssddanbrown).
|
|
||||||
- [Disclose via huntr.dev](https://huntr.dev/bounties/disclose)
|
|
||||||
- Bounties may be available to you through this platform.
|
|
||||||
- Be sure to use `https://github.com/BookStackApp/BookStack` as the repository URL.
|
|
||||||
|
|
||||||
Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability
|
Please be patient while the vulnerability is being reviewed. Deploying the fix to address the vulnerability
|
||||||
can often take a little time due to the amount of preparation required, to ensure the vulnerability has
|
can often take a little time due to the amount of preparation required, to ensure the vulnerability has
|
||||||
|
81
.github/translators.txt
vendored
81
.github/translators.txt
vendored
@ -57,6 +57,7 @@ Name :: Languages
|
|||||||
@Jokuna :: Korean
|
@Jokuna :: Korean
|
||||||
@smartshogu :: German; German Informal
|
@smartshogu :: German; German Informal
|
||||||
@samadha56 :: Persian
|
@samadha56 :: Persian
|
||||||
|
@mrmuminov :: Uzbek
|
||||||
cipi1965 :: Italian
|
cipi1965 :: Italian
|
||||||
Mykola Ronik (Mantikor) :: Ukrainian
|
Mykola Ronik (Mantikor) :: Ukrainian
|
||||||
furkanoyk :: Turkish
|
furkanoyk :: Turkish
|
||||||
@ -176,7 +177,7 @@ Alexander Predl (Harveyhase68) :: German
|
|||||||
Rem (Rem9000) :: Dutch
|
Rem (Rem9000) :: Dutch
|
||||||
Michał Stelmach (stelmach-web) :: Polish
|
Michał Stelmach (stelmach-web) :: Polish
|
||||||
arniom :: French
|
arniom :: French
|
||||||
REMOVED_USER :: ; Dutch; Turkish
|
REMOVED_USER :: French; Dutch; Turkish;
|
||||||
林祖年 (contagion) :: Chinese Traditional
|
林祖年 (contagion) :: Chinese Traditional
|
||||||
Siamak Guodarzi (siamakgoudarzi88) :: Persian
|
Siamak Guodarzi (siamakgoudarzi88) :: Persian
|
||||||
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
|
Lis Maestrelo (lismtrl) :: Portuguese, Brazilian
|
||||||
@ -269,7 +270,7 @@ mcgong (GongMingCai) :: Chinese Simplified; Chinese Traditional
|
|||||||
Nanang Setia Budi (sefidananang) :: Indonesian
|
Nanang Setia Budi (sefidananang) :: Indonesian
|
||||||
Андрей Павлов (andrei.pavlov) :: Russian
|
Андрей Павлов (andrei.pavlov) :: Russian
|
||||||
Alex Navarro (alex.n.navarro) :: Portuguese, Brazilian
|
Alex Navarro (alex.n.navarro) :: Portuguese, Brazilian
|
||||||
Ji-Hyeon Gim (PotatoGim) :: Korean
|
Jihyeon Gim (PotatoGim) :: Korean
|
||||||
Mihai Ochian (soulstorm19) :: Romanian
|
Mihai Ochian (soulstorm19) :: Romanian
|
||||||
HeartCore :: German Informal; German
|
HeartCore :: German Informal; German
|
||||||
simon.pct :: French
|
simon.pct :: French
|
||||||
@ -283,13 +284,13 @@ Kuchinashi Hoshikawa (kuchinashi) :: Chinese Simplified
|
|||||||
digilady :: Greek
|
digilady :: Greek
|
||||||
Linus (LinusOP) :: Swedish
|
Linus (LinusOP) :: Swedish
|
||||||
Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian
|
Felipe Cardoso (felipecardosoruff) :: Portuguese, Brazilian
|
||||||
RandomUser0815 :: German
|
RandomUser0815 :: German Informal; German
|
||||||
Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
|
Ismael Mesquita (mesquitoliveira) :: Portuguese, Brazilian
|
||||||
구인회 (laskdjlaskdj12) :: Korean
|
구인회 (laskdjlaskdj12) :: Korean
|
||||||
LiZerui (CNLiZerui) :: Chinese Traditional
|
LiZerui (CNLiZerui) :: Chinese Traditional
|
||||||
Fabrice Boyer (FabriceBoyer) :: French
|
Fabrice Boyer (FabriceBoyer) :: French
|
||||||
mikael (bitcanon) :: Swedish
|
mikael (bitcanon) :: Swedish
|
||||||
Matthias Mai (schnapsidee) :: German
|
Matthias Mai (schnapsidee) :: German Informal; German
|
||||||
Ufuk Ayyıldız (ufukayyildiz) :: Turkish
|
Ufuk Ayyıldız (ufukayyildiz) :: Turkish
|
||||||
Jan Mitrof (jan.kachlik) :: Czech
|
Jan Mitrof (jan.kachlik) :: Czech
|
||||||
edwardsmirnov :: Russian
|
edwardsmirnov :: Russian
|
||||||
@ -298,3 +299,75 @@ shotu :: French
|
|||||||
Cesar_Lopez_Aguillon :: Spanish
|
Cesar_Lopez_Aguillon :: Spanish
|
||||||
bdewoop :: German
|
bdewoop :: German
|
||||||
dina davoudi (dina.davoudi) :: Persian
|
dina davoudi (dina.davoudi) :: Persian
|
||||||
|
Angelos Chouvardas (achouvardas) :: Greek
|
||||||
|
rndrss :: Portuguese, Brazilian
|
||||||
|
rirac294 :: Russian
|
||||||
|
David Furman (thefourCraft) :: Hebrew
|
||||||
|
Pafzedog :: French
|
||||||
|
Yllelder :: Spanish
|
||||||
|
Adrian Ocneanu (aocneanu) :: Romanian
|
||||||
|
Eduardo Castanho (EduardoCastanho) :: Portuguese
|
||||||
|
VIET NAM VPS (vietnamvps) :: Vietnamese
|
||||||
|
m4tthi4s :: French
|
||||||
|
toras9000 :: Japanese
|
||||||
|
pathab :: German
|
||||||
|
MichelSchoon85 :: Dutch
|
||||||
|
Jøran Haugli (haugli92) :: Norwegian Bokmal
|
||||||
|
Vasileios Kouvelis (VasilisKouvelis) :: Greek
|
||||||
|
Dremski :: Bulgarian
|
||||||
|
Frédéric SENE (nothingfr) :: French
|
||||||
|
bendem :: French
|
||||||
|
kostasdizas :: Greek
|
||||||
|
Ricardo Schroeder (brownstone666) :: Portuguese, Brazilian
|
||||||
|
Eitan MG (EitanMG) :: Hebrew
|
||||||
|
Robin Flikkema (RobinFlikkema) :: Dutch
|
||||||
|
Michal Gurcik (mgurcik) :: Slovak
|
||||||
|
Pooyan Arab (pooyanarab) :: Persian
|
||||||
|
Ochi Darma Putra (troke12) :: Indonesian
|
||||||
|
H.-H. Peng (Hsins) :: Chinese Traditional
|
||||||
|
Mosi Wang (mosiwang) :: Chinese Traditional
|
||||||
|
骆言 (LawssssCat) :: Chinese Simplified
|
||||||
|
Stickers Gaming Shøw (StickerSGSHOW) :: French
|
||||||
|
Le Van Chinh (Chino) (lvanchinh86) :: Vietnamese
|
||||||
|
Rubens nagios (rubenix) :: Catalan
|
||||||
|
Patrick Dantas (pa-tiq) :: Portuguese, Brazilian
|
||||||
|
Michal (michalgurcik) :: Slovak
|
||||||
|
Nepomacs :: German
|
||||||
|
Rubens (rubenix) :: Catalan
|
||||||
|
m4z :: German; German Informal
|
||||||
|
TheRazvy :: Romanian
|
||||||
|
Yossi Zilber (lortens) :: Hebrew; Uzbek
|
||||||
|
desdinova :: French
|
||||||
|
Ingus Rūķis (ingus.rukis) :: Latvian
|
||||||
|
Eugene Pershin (SilentEugene) :: Russian
|
||||||
|
周盛道 (zhoushengdao) :: Chinese Simplified
|
||||||
|
hamidreza amini (hamidrezaamini2022) :: Persian
|
||||||
|
Tomislav Kraljević (tomislav.kraljevic) :: Croatian
|
||||||
|
Taygun Yıldırım (yildirimtaygun) :: Turkish
|
||||||
|
robing29 :: German
|
||||||
|
Bruno Eduardo de Jesus Barroso (brunoejb) :: Portuguese, Brazilian
|
||||||
|
Igor V Belousov (biv) :: Russian
|
||||||
|
David Bauer (davbauer) :: German
|
||||||
|
Guttorm Hveem (guttormhveem) :: Norwegian Nynorsk; Norwegian Bokmal
|
||||||
|
Minh Giang Truong (minhgiang1204) :: Vietnamese
|
||||||
|
Ioannis Ioannides (i.ioannides) :: Greek
|
||||||
|
Vadim (vadrozh) :: Russian
|
||||||
|
Flip333 :: German Informal; German
|
||||||
|
Paulo Henrique (paulohsantos114) :: Portuguese, Brazilian
|
||||||
|
Dženan (Dzenan) :: Swedish
|
||||||
|
Péter Péli (peter.peli) :: Hungarian
|
||||||
|
TWME :: Chinese Traditional
|
||||||
|
Sascha (Man-in-Black) :: German; German Informal
|
||||||
|
Mohammadreza Madadi (madadi.efl) :: Persian
|
||||||
|
Konstantin (kkovacheli) :: Ukrainian; Russian
|
||||||
|
link1183 :: French
|
||||||
|
Renan (rfpe) :: Portuguese, Brazilian
|
||||||
|
Lowkey (bbsweb) :: Chinese Simplified
|
||||||
|
ZZnOB (zznobzz) :: Russian
|
||||||
|
rupus :: Swedish
|
||||||
|
developernecsys :: Norwegian Nynorsk
|
||||||
|
xuan LI (xuanli233) :: Chinese Simplified
|
||||||
|
LameeQS :: Latvian
|
||||||
|
Sorin T. (trimbitassorin) :: Romanian
|
||||||
|
poesty :: Chinese Simplified
|
||||||
|
balmag :: Hungarian
|
||||||
|
8
.github/workflows/analyse-php.yml
vendored
8
.github/workflows/analyse-php.yml
vendored
@ -1,6 +1,12 @@
|
|||||||
name: analyse-php
|
name: analyse-php
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
24
.github/workflows/lint-js.yml
vendored
Normal file
24
.github/workflows/lint-js.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
name: lint-js
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.js'
|
||||||
|
- '**.json'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.js'
|
||||||
|
- '**.json'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
if: ${{ github.ref != 'refs/heads/l10n_development' }}
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
- name: Install NPM deps
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Run formatting check
|
||||||
|
run: npm run lint
|
8
.github/workflows/lint-php.yml
vendored
8
.github/workflows/lint-php.yml
vendored
@ -1,6 +1,12 @@
|
|||||||
name: lint-php
|
name: lint-php
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
|
12
.github/workflows/test-migrations.yml
vendored
12
.github/workflows/test-migrations.yml
vendored
@ -1,6 +1,14 @@
|
|||||||
name: test-migrations
|
name: test-migrations
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
- 'composer.*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
- 'composer.*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -8,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: ['7.4', '8.0', '8.1', '8.2']
|
php: ['8.0', '8.1', '8.2', '8.3']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
|
14
.github/workflows/test-php.yml
vendored
14
.github/workflows/test-php.yml
vendored
@ -1,6 +1,14 @@
|
|||||||
name: test-php
|
name: test-php
|
||||||
|
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
- 'composer.*'
|
||||||
|
pull_request:
|
||||||
|
paths:
|
||||||
|
- '**.php'
|
||||||
|
- 'composer.*'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@ -8,7 +16,7 @@ jobs:
|
|||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
php: ['7.4', '8.0', '8.1', '8.2']
|
php: ['8.0', '8.1', '8.2', '8.3']
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v1
|
- uses: actions/checkout@v1
|
||||||
|
|
||||||
@ -16,7 +24,7 @@ jobs:
|
|||||||
uses: shivammathur/setup-php@v2
|
uses: shivammathur/setup-php@v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: ${{ matrix.php }}
|
||||||
extensions: gd, mbstring, json, curl, xml, mysql, ldap
|
extensions: gd, mbstring, json, curl, xml, mysql, ldap, gmp
|
||||||
|
|
||||||
- name: Get Composer Cache Directory
|
- name: Get Composer Cache Directory
|
||||||
id: composer-cache
|
id: composer-cache
|
||||||
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,5 +1,7 @@
|
|||||||
/vendor
|
/vendor
|
||||||
/node_modules
|
/node_modules
|
||||||
|
/.vscode
|
||||||
|
/composer
|
||||||
Homestead.yaml
|
Homestead.yaml
|
||||||
.env
|
.env
|
||||||
.idea
|
.idea
|
||||||
@ -11,6 +13,7 @@ yarn-error.log
|
|||||||
/public/js
|
/public/js
|
||||||
/public/bower
|
/public/bower
|
||||||
/public/build/
|
/public/build/
|
||||||
|
/public/favicon.ico
|
||||||
/storage/images
|
/storage/images
|
||||||
_ide_helper.php
|
_ide_helper.php
|
||||||
/storage/debugbar
|
/storage/debugbar
|
||||||
@ -20,8 +23,11 @@ yarn.lock
|
|||||||
nbproject
|
nbproject
|
||||||
.buildpath
|
.buildpath
|
||||||
.project
|
.project
|
||||||
|
.nvmrc
|
||||||
.settings/
|
.settings/
|
||||||
webpack-stats.json
|
webpack-stats.json
|
||||||
.phpunit.result.cache
|
.phpunit.result.cache
|
||||||
.DS_Store
|
.DS_Store
|
||||||
phpstan.neon
|
phpstan.neon
|
||||||
|
esbuild-meta.json
|
||||||
|
.phpactor.json
|
||||||
|
2
LICENSE
2
LICENSE
@ -1,6 +1,6 @@
|
|||||||
The MIT License (MIT)
|
The MIT License (MIT)
|
||||||
|
|
||||||
Copyright (c) 2015-2022, Dan Brown and the BookStack Project contributors.
|
Copyright (c) 2015-2023, Dan Brown and the BookStack Project 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,34 +1,24 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\EmailConfirmationService;
|
use BookStack\Access\EmailConfirmationService;
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
|
use BookStack\Users\UserRepo;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class ConfirmEmailController extends Controller
|
class ConfirmEmailController extends Controller
|
||||||
{
|
{
|
||||||
protected EmailConfirmationService $emailConfirmationService;
|
|
||||||
protected LoginService $loginService;
|
|
||||||
protected UserRepo $userRepo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new controller instance.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
EmailConfirmationService $emailConfirmationService,
|
protected EmailConfirmationService $emailConfirmationService,
|
||||||
LoginService $loginService,
|
protected LoginService $loginService,
|
||||||
UserRepo $userRepo
|
protected UserRepo $userRepo
|
||||||
) {
|
) {
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
$this->userRepo = $userRepo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,19 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Password;
|
use Illuminate\Support\Facades\Password;
|
||||||
|
|
||||||
class ForgotPasswordController extends Controller
|
class ForgotPasswordController extends Controller
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Create a new controller instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->middleware('guest');
|
$this->middleware('guest');
|
||||||
@ -30,10 +25,6 @@ class ForgotPasswordController extends Controller
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Send a reset link to the given user.
|
* Send a reset link to the given user.
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
*
|
|
||||||
* @return \Illuminate\Http\RedirectResponse
|
|
||||||
*/
|
*/
|
||||||
public function sendResetLinkEmail(Request $request)
|
public function sendResetLinkEmail(Request $request)
|
||||||
{
|
{
|
||||||
@ -56,13 +47,13 @@ class ForgotPasswordController extends Controller
|
|||||||
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
$message = trans('auth.reset_password_sent', ['email' => $request->get('email')]);
|
||||||
$this->showSuccessNotification($message);
|
$this->showSuccessNotification($message);
|
||||||
|
|
||||||
return back()->with('status', trans($response));
|
return redirect('/password/email')->with('status', trans($response));
|
||||||
}
|
}
|
||||||
|
|
||||||
// If an error was returned by the password broker, we will get this message
|
// If an error was returned by the password broker, we will get this message
|
||||||
// translated so we can notify a user of the problem. We'll redirect back
|
// translated so we can notify a user of the problem. We'll redirect back
|
||||||
// to where the users came from so they can attempt this process again.
|
// to where the users came from so they can attempt this process again.
|
||||||
return back()->withErrors(
|
return redirect('/password/email')->withErrors(
|
||||||
['email' => trans($response)]
|
['email' => trans($response)]
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
trait HandlesPartialLogins
|
trait HandlesPartialLogins
|
||||||
{
|
{
|
@ -1,36 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Auth;
|
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class LoginController extends Controller
|
class LoginController extends Controller
|
||||||
{
|
{
|
||||||
use ThrottlesLogins;
|
use ThrottlesLogins;
|
||||||
|
|
||||||
protected SocialAuthService $socialAuthService;
|
public function __construct(
|
||||||
protected LoginService $loginService;
|
protected SocialDriverManager $socialDriverManager,
|
||||||
|
protected LoginService $loginService,
|
||||||
/**
|
) {
|
||||||
* Create a new controller instance.
|
|
||||||
*/
|
|
||||||
public function __construct(SocialAuthService $socialAuthService, LoginService $loginService)
|
|
||||||
{
|
|
||||||
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
|
$this->middleware('guest', ['only' => ['getLogin', 'login']]);
|
||||||
$this->middleware('guard:standard,ldap', ['only' => ['login']]);
|
$this->middleware('guard:standard,ldap', ['only' => ['login']]);
|
||||||
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
|
$this->middleware('guard:standard,ldap,oidc', ['only' => ['logout']]);
|
||||||
|
|
||||||
$this->socialAuthService = $socialAuthService;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -38,7 +30,7 @@ class LoginController extends Controller
|
|||||||
*/
|
*/
|
||||||
public function getLogin(Request $request)
|
public function getLogin(Request $request)
|
||||||
{
|
{
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
$authMethod = config('auth.method');
|
$authMethod = config('auth.method');
|
||||||
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
$preventInitiation = $request->get('prevent_auto_init') === 'true';
|
||||||
|
|
||||||
@ -52,7 +44,7 @@ class LoginController extends Controller
|
|||||||
// Store the previous location for redirect after login
|
// Store the previous location for redirect after login
|
||||||
$this->updateIntendedFromPrevious();
|
$this->updateIntendedFromPrevious();
|
||||||
|
|
||||||
if (!$preventInitiation && $this->shouldAutoInitiate()) {
|
if (!$preventInitiation && $this->loginService->shouldAutoInitiate()) {
|
||||||
return view('auth.login-initiate', [
|
return view('auth.login-initiate', [
|
||||||
'authMethod' => $authMethod,
|
'authMethod' => $authMethod,
|
||||||
]);
|
]);
|
||||||
@ -101,15 +93,9 @@ class LoginController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Logout user and perform subsequent redirect.
|
* Logout user and perform subsequent redirect.
|
||||||
*/
|
*/
|
||||||
public function logout(Request $request)
|
public function logout()
|
||||||
{
|
{
|
||||||
Auth::guard()->logout();
|
return redirect($this->loginService->logout());
|
||||||
$request->session()->invalidate();
|
|
||||||
$request->session()->regenerateToken();
|
|
||||||
|
|
||||||
$redirectUri = $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
|
||||||
|
|
||||||
return redirect($redirectUri);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -200,7 +186,7 @@ class LoginController extends Controller
|
|||||||
{
|
{
|
||||||
// Store the previous location for redirect after login
|
// Store the previous location for redirect after login
|
||||||
$previous = url()->previous('');
|
$previous = url()->previous('');
|
||||||
$isPreviousFromInstance = (strpos($previous, url('/')) === 0);
|
$isPreviousFromInstance = str_starts_with($previous, url('/'));
|
||||||
if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
|
if (!$previous || !setting('app-public') || !$isPreviousFromInstance) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -211,23 +197,11 @@ class LoginController extends Controller
|
|||||||
];
|
];
|
||||||
|
|
||||||
foreach ($ignorePrefixList as $ignorePrefix) {
|
foreach ($ignorePrefixList as $ignorePrefix) {
|
||||||
if (strpos($previous, url($ignorePrefix)) === 0) {
|
if (str_starts_with($previous, url($ignorePrefix))) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
redirect()->setIntendedUrl($previous);
|
redirect()->setIntendedUrl($previous);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if login auto-initiate should be valid based upon authentication config.
|
|
||||||
*/
|
|
||||||
protected function shouldAutoInitiate(): bool
|
|
||||||
{
|
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
|
||||||
$authMethod = config('auth.method');
|
|
||||||
$autoRedirect = config('auth.auto_initiate');
|
|
||||||
|
|
||||||
return $autoRedirect && count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,14 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\Mfa\BackupCodeService;
|
||||||
use BookStack\Auth\Access\Mfa\BackupCodeService;
|
use BookStack\Access\Mfa\MfaSession;
|
||||||
use BookStack\Auth\Access\Mfa\MfaSession;
|
use BookStack\Access\Mfa\MfaValue;
|
||||||
use BookStack\Auth\Access\Mfa\MfaValue;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Access\Mfa\MfaValue;
|
||||||
use BookStack\Auth\Access\Mfa\MfaValue;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class MfaController extends Controller
|
class MfaController extends Controller
|
@ -1,15 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\Mfa\MfaSession;
|
||||||
use BookStack\Auth\Access\Mfa\MfaSession;
|
use BookStack\Access\Mfa\MfaValue;
|
||||||
use BookStack\Auth\Access\Mfa\MfaValue;
|
use BookStack\Access\Mfa\TotpService;
|
||||||
use BookStack\Auth\Access\Mfa\TotpService;
|
use BookStack\Access\Mfa\TotpValidationRule;
|
||||||
use BookStack\Auth\Access\Mfa\TotpValidationRule;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Exceptions\NotFoundException;
|
use BookStack\Exceptions\NotFoundException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
@ -1,19 +1,16 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\Oidc\OidcException;
|
use BookStack\Access\Oidc\OidcException;
|
||||||
use BookStack\Auth\Access\Oidc\OidcService;
|
use BookStack\Access\Oidc\OidcService;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class OidcController extends Controller
|
class OidcController extends Controller
|
||||||
{
|
{
|
||||||
protected OidcService $oidcService;
|
protected OidcService $oidcService;
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenIdController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(OidcService $oidcService)
|
public function __construct(OidcService $oidcService)
|
||||||
{
|
{
|
||||||
$this->oidcService = $oidcService;
|
$this->oidcService = $oidcService;
|
||||||
@ -63,4 +60,12 @@ class OidcController extends Controller
|
|||||||
|
|
||||||
return redirect()->intended();
|
return redirect()->intended();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log the user out then start the OIDC RP-initiated logout process.
|
||||||
|
*/
|
||||||
|
public function logout()
|
||||||
|
{
|
||||||
|
return redirect($this->oidcService->logout());
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,13 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Access\SocialDriverManager;
|
||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
|
use Illuminate\Contracts\Validation\Validator as ValidatorContract;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Validator;
|
use Illuminate\Support\Facades\Validator;
|
||||||
@ -15,7 +15,7 @@ use Illuminate\Validation\Rules\Password;
|
|||||||
|
|
||||||
class RegisterController extends Controller
|
class RegisterController extends Controller
|
||||||
{
|
{
|
||||||
protected SocialAuthService $socialAuthService;
|
protected SocialDriverManager $socialDriverManager;
|
||||||
protected RegistrationService $registrationService;
|
protected RegistrationService $registrationService;
|
||||||
protected LoginService $loginService;
|
protected LoginService $loginService;
|
||||||
|
|
||||||
@ -23,14 +23,14 @@ class RegisterController extends Controller
|
|||||||
* Create a new controller instance.
|
* Create a new controller instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
SocialAuthService $socialAuthService,
|
SocialDriverManager $socialDriverManager,
|
||||||
RegistrationService $registrationService,
|
RegistrationService $registrationService,
|
||||||
LoginService $loginService
|
LoginService $loginService
|
||||||
) {
|
) {
|
||||||
$this->middleware('guest');
|
$this->middleware('guest');
|
||||||
$this->middleware('guard:standard');
|
$this->middleware('guard:standard');
|
||||||
|
|
||||||
$this->socialAuthService = $socialAuthService;
|
$this->socialDriverManager = $socialDriverManager;
|
||||||
$this->registrationService = $registrationService;
|
$this->registrationService = $registrationService;
|
||||||
$this->loginService = $loginService;
|
$this->loginService = $loginService;
|
||||||
}
|
}
|
||||||
@ -43,7 +43,7 @@ class RegisterController extends Controller
|
|||||||
public function getRegister()
|
public function getRegister()
|
||||||
{
|
{
|
||||||
$this->registrationService->ensureRegistrationAllowed();
|
$this->registrationService->ensureRegistrationAllowed();
|
||||||
$socialDrivers = $this->socialAuthService->getActiveDrivers();
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
|
|
||||||
return view('auth.register', [
|
return view('auth.register', [
|
||||||
'socialDrivers' => $socialDrivers,
|
'socialDrivers' => $socialDrivers,
|
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
@ -66,7 +66,7 @@ class ResetPasswordController extends Controller
|
|||||||
// redirect them back to where they came from with their error message.
|
// redirect them back to where they came from with their error message.
|
||||||
return $response === Password::PASSWORD_RESET
|
return $response === Password::PASSWORD_RESET
|
||||||
? $this->sendResetResponse()
|
? $this->sendResetResponse()
|
||||||
: $this->sendResetFailedResponse($request, $response);
|
: $this->sendResetFailedResponse($request, $response, $request->get('token'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -83,7 +83,7 @@ class ResetPasswordController extends Controller
|
|||||||
/**
|
/**
|
||||||
* Get the response for a failed password reset.
|
* Get the response for a failed password reset.
|
||||||
*/
|
*/
|
||||||
protected function sendResetFailedResponse(Request $request, string $response): RedirectResponse
|
protected function sendResetFailedResponse(Request $request, string $response, string $token): RedirectResponse
|
||||||
{
|
{
|
||||||
// We show invalid users as invalid tokens as to not leak what
|
// We show invalid users as invalid tokens as to not leak what
|
||||||
// users may exist in the system.
|
// users may exist in the system.
|
||||||
@ -91,7 +91,7 @@ class ResetPasswordController extends Controller
|
|||||||
$response = Password::INVALID_TOKEN;
|
$response = Password::INVALID_TOKEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back()
|
return redirect("/password/reset/{$token}")
|
||||||
->withInput($request->only('email'))
|
->withInput($request->only('email'))
|
||||||
->withErrors(['email' => trans($response)]);
|
->withErrors(['email' => trans($response)]);
|
||||||
}
|
}
|
@ -1,22 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\Saml2Service;
|
use BookStack\Access\Saml2Service;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
class Saml2Controller extends Controller
|
class Saml2Controller extends Controller
|
||||||
{
|
{
|
||||||
protected Saml2Service $samlService;
|
public function __construct(
|
||||||
|
protected Saml2Service $samlService
|
||||||
/**
|
) {
|
||||||
* Saml2Controller constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(Saml2Service $samlService)
|
|
||||||
{
|
|
||||||
$this->samlService = $samlService;
|
|
||||||
$this->middleware('guard:saml2');
|
$this->middleware('guard:saml2');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,7 +31,12 @@ class Saml2Controller extends Controller
|
|||||||
*/
|
*/
|
||||||
public function logout()
|
public function logout()
|
||||||
{
|
{
|
||||||
$logoutDetails = $this->samlService->logout(auth()->user());
|
$user = user();
|
||||||
|
if ($user->isGuest()) {
|
||||||
|
return redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
$logoutDetails = $this->samlService->logout($user);
|
||||||
|
|
||||||
if ($logoutDetails['id']) {
|
if ($logoutDetails['id']) {
|
||||||
session()->flash('saml2_logout_request_id', $logoutDetails['id']);
|
session()->flash('saml2_logout_request_id', $logoutDetails['id']);
|
||||||
@ -64,7 +64,7 @@ class Saml2Controller extends Controller
|
|||||||
public function sls()
|
public function sls()
|
||||||
{
|
{
|
||||||
$requestId = session()->pull('saml2_logout_request_id', null);
|
$requestId = session()->pull('saml2_logout_request_id', null);
|
||||||
$redirect = $this->samlService->processSlsResponse($requestId) ?? '/';
|
$redirect = $this->samlService->processSlsResponse($requestId);
|
||||||
|
|
||||||
return redirect($redirect);
|
return redirect($redirect);
|
||||||
}
|
}
|
@ -1,37 +1,27 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use BookStack\Auth\Access\SocialAuthService;
|
use BookStack\Access\SocialAuthService;
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
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\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||||
|
|
||||||
class SocialController extends Controller
|
class SocialController extends Controller
|
||||||
{
|
{
|
||||||
protected SocialAuthService $socialAuthService;
|
|
||||||
protected RegistrationService $registrationService;
|
|
||||||
protected LoginService $loginService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SocialController constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
SocialAuthService $socialAuthService,
|
protected SocialAuthService $socialAuthService,
|
||||||
RegistrationService $registrationService,
|
protected RegistrationService $registrationService,
|
||||||
LoginService $loginService
|
protected LoginService $loginService,
|
||||||
) {
|
) {
|
||||||
$this->middleware('guest')->only(['register']);
|
$this->middleware('guest')->only(['register']);
|
||||||
$this->socialAuthService = $socialAuthService;
|
|
||||||
$this->registrationService = $registrationService;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -89,7 +79,7 @@ class SocialController extends Controller
|
|||||||
try {
|
try {
|
||||||
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
return $this->socialAuthService->handleLoginCallback($socialDriver, $socialUser);
|
||||||
} catch (SocialSignInAccountNotUsed $exception) {
|
} catch (SocialSignInAccountNotUsed $exception) {
|
||||||
if ($this->socialAuthService->driverAutoRegisterEnabled($socialDriver)) {
|
if ($this->socialAuthService->drivers()->isAutoRegisterEnabled($socialDriver)) {
|
||||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +91,7 @@ class SocialController extends Controller
|
|||||||
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
return $this->socialRegisterCallback($socialDriver, $socialUser);
|
||||||
}
|
}
|
||||||
|
|
||||||
return redirect()->back();
|
return redirect('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -112,7 +102,7 @@ class SocialController extends Controller
|
|||||||
$this->socialAuthService->detachSocialAccount($socialDriver);
|
$this->socialAuthService->detachSocialAccount($socialDriver);
|
||||||
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
|
session()->flash('success', trans('settings.users_social_disconnected', ['socialAccount' => Str::title($socialDriver)]));
|
||||||
|
|
||||||
return redirect(user()->getEditUrl());
|
return redirect('/my-account/auth#social-accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,7 +114,7 @@ class SocialController extends Controller
|
|||||||
{
|
{
|
||||||
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
$socialUser = $this->socialAuthService->handleRegistrationCallback($socialDriver, $socialUser);
|
||||||
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
$socialAccount = $this->socialAuthService->newSocialAccount($socialDriver, $socialUser);
|
||||||
$emailVerified = $this->socialAuthService->driverAutoConfirmEmailEnabled($socialDriver);
|
$emailVerified = $this->socialAuthService->drivers()->isAutoConfirmEmailEnabled($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 = [
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use Illuminate\Cache\RateLimiter;
|
use Illuminate\Cache\RateLimiter;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@ -71,7 +71,7 @@ trait ThrottlesLogins
|
|||||||
*/
|
*/
|
||||||
protected function limiter(): RateLimiter
|
protected function limiter(): RateLimiter
|
||||||
{
|
{
|
||||||
return app(RateLimiter::class);
|
return app()->make(RateLimiter::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers\Auth;
|
namespace BookStack\Access\Controllers;
|
||||||
|
|
||||||
use BookStack\Auth\Access\UserInviteService;
|
use BookStack\Access\UserInviteService;
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
use BookStack\Http\Controllers\Controller;
|
use BookStack\Http\Controller;
|
||||||
|
use BookStack\Users\UserRepo;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
@ -1,15 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Access\Notifications\ConfirmEmailNotification;
|
||||||
use BookStack\Exceptions\ConfirmationEmailException;
|
use BookStack\Exceptions\ConfirmationEmailException;
|
||||||
use BookStack\Notifications\ConfirmEmail;
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
class EmailConfirmationService extends UserTokenService
|
class EmailConfirmationService extends UserTokenService
|
||||||
{
|
{
|
||||||
protected $tokenTable = 'email_confirmations';
|
protected string $tokenTable = 'email_confirmations';
|
||||||
protected $expiryTime = 24;
|
protected int $expiryTime = 24;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new confirmation for a user,
|
* Create new confirmation for a user,
|
||||||
@ -26,7 +26,7 @@ class EmailConfirmationService extends UserTokenService
|
|||||||
$this->deleteByUser($user);
|
$this->deleteByUser($user);
|
||||||
$token = $this->createTokenForUser($user);
|
$token = $this->createTokenForUser($user);
|
||||||
|
|
||||||
$user->notify(new ConfirmEmail($token));
|
$user->notify(new ConfirmEmailNotification($token));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use Illuminate\Contracts\Auth\Authenticatable;
|
use Illuminate\Contracts\Auth\Authenticatable;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
@ -1,9 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\Role;
|
use BookStack\Users\Models\Role;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
class GroupSyncService
|
class GroupSyncService
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Access\Guards;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Saml2 Session Guard.
|
* Saml2 Session Guard.
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Access\Guards;
|
||||||
|
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use Illuminate\Auth\GuardHelpers;
|
use Illuminate\Auth\GuardHelpers;
|
||||||
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
|
||||||
use Illuminate\Contracts\Auth\StatefulGuard;
|
use Illuminate\Contracts\Auth\StatefulGuard;
|
@ -1,15 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Guards;
|
namespace BookStack\Access\Guards;
|
||||||
|
|
||||||
use BookStack\Auth\Access\LdapService;
|
use BookStack\Access\LdapService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
use BookStack\Exceptions\LoginAttemptEmailNeededException;
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Contracts\Auth\UserProvider;
|
use Illuminate\Contracts\Auth\UserProvider;
|
||||||
use Illuminate\Contracts\Session\Session;
|
use Illuminate\Contracts\Session\Session;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
110
app/Access/Ldap.php
Normal file
110
app/Access/Ldap.php
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Ldap
|
||||||
|
* An object-orientated thin abstraction wrapper for common PHP LDAP functions.
|
||||||
|
* Allows the standard LDAP functions to be mocked for testing.
|
||||||
|
*/
|
||||||
|
class Ldap
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Connect to an LDAP server.
|
||||||
|
*
|
||||||
|
* @return resource|\LDAP\Connection|false
|
||||||
|
*/
|
||||||
|
public function connect(string $hostName)
|
||||||
|
{
|
||||||
|
return ldap_connect($hostName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the value of an LDAP option for the given connection.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection|null $ldapConnection
|
||||||
|
*/
|
||||||
|
public function setOption($ldapConnection, int $option, mixed $value): bool
|
||||||
|
{
|
||||||
|
return ldap_set_option($ldapConnection, $option, $value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start TLS on the given LDAP connection.
|
||||||
|
*/
|
||||||
|
public function startTls($ldapConnection): bool
|
||||||
|
{
|
||||||
|
return ldap_start_tls($ldapConnection);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the version number for the given LDAP connection.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection $ldapConnection
|
||||||
|
*/
|
||||||
|
public function setVersion($ldapConnection, int $version): bool
|
||||||
|
{
|
||||||
|
return $this->setOption($ldapConnection, LDAP_OPT_PROTOCOL_VERSION, $version);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search LDAP tree using the provided filter.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection $ldapConnection
|
||||||
|
*
|
||||||
|
* @return resource|\LDAP\Result
|
||||||
|
*/
|
||||||
|
public function search($ldapConnection, string $baseDn, string $filter, array $attributes = null)
|
||||||
|
{
|
||||||
|
return ldap_search($ldapConnection, $baseDn, $filter, $attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entries from an LDAP search result.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection $ldapConnection
|
||||||
|
* @param resource|\LDAP\Result $ldapSearchResult
|
||||||
|
*/
|
||||||
|
public function getEntries($ldapConnection, $ldapSearchResult): array|false
|
||||||
|
{
|
||||||
|
return ldap_get_entries($ldapConnection, $ldapSearchResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search and get entries immediately.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection $ldapConnection
|
||||||
|
*/
|
||||||
|
public function searchAndGetEntries($ldapConnection, string $baseDn, string $filter, array $attributes = null): array|false
|
||||||
|
{
|
||||||
|
$search = $this->search($ldapConnection, $baseDn, $filter, $attributes);
|
||||||
|
|
||||||
|
return $this->getEntries($ldapConnection, $search);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bind to LDAP directory.
|
||||||
|
*
|
||||||
|
* @param resource|\LDAP\Connection $ldapConnection
|
||||||
|
*/
|
||||||
|
public function bind($ldapConnection, string $bindRdn = null, string $bindPassword = null): bool
|
||||||
|
{
|
||||||
|
return ldap_bind($ldapConnection, $bindRdn, $bindPassword);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Explode an LDAP dn string into an array of components.
|
||||||
|
*/
|
||||||
|
public function explodeDn(string $dn, int $withAttrib): array|false
|
||||||
|
{
|
||||||
|
return ldap_explode_dn($dn, $withAttrib);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escape a string for use in an LDAP filter.
|
||||||
|
*/
|
||||||
|
public function escape(string $value, string $ignore = '', int $flags = 0): string
|
||||||
|
{
|
||||||
|
return ldap_escape($value, $ignore, $flags);
|
||||||
|
}
|
||||||
|
}
|
@ -1,11 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\LdapException;
|
use BookStack\Exceptions\LdapException;
|
||||||
use BookStack\Uploads\UserAvatars;
|
use BookStack\Uploads\UserAvatars;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use ErrorException;
|
use ErrorException;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
@ -15,26 +15,19 @@ use Illuminate\Support\Facades\Log;
|
|||||||
*/
|
*/
|
||||||
class LdapService
|
class LdapService
|
||||||
{
|
{
|
||||||
protected Ldap $ldap;
|
|
||||||
protected GroupSyncService $groupSyncService;
|
|
||||||
protected UserAvatars $userAvatars;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var resource
|
* @var resource|\LDAP\Connection
|
||||||
*/
|
*/
|
||||||
protected $ldapConnection;
|
protected $ldapConnection;
|
||||||
|
|
||||||
protected array $config;
|
protected array $config;
|
||||||
protected bool $enabled;
|
protected bool $enabled;
|
||||||
|
|
||||||
/**
|
public function __construct(
|
||||||
* LdapService constructor.
|
protected Ldap $ldap,
|
||||||
*/
|
protected UserAvatars $userAvatars,
|
||||||
public function __construct(Ldap $ldap, UserAvatars $userAvatars, GroupSyncService $groupSyncService)
|
protected GroupSyncService $groupSyncService
|
||||||
{
|
) {
|
||||||
$this->ldap = $ldap;
|
|
||||||
$this->userAvatars = $userAvatars;
|
|
||||||
$this->groupSyncService = $groupSyncService;
|
|
||||||
$this->config = config('services.ldap');
|
$this->config = config('services.ldap');
|
||||||
$this->enabled = config('auth.method') === 'ldap';
|
$this->enabled = config('auth.method') === 'ldap';
|
||||||
}
|
}
|
||||||
@ -59,7 +52,7 @@ class LdapService
|
|||||||
|
|
||||||
// Clean attributes
|
// Clean attributes
|
||||||
foreach ($attributes as $index => $attribute) {
|
foreach ($attributes as $index => $attribute) {
|
||||||
if (strpos($attribute, 'BIN;') === 0) {
|
if (str_starts_with($attribute, 'BIN;')) {
|
||||||
$attributes[$index] = substr($attribute, strlen('BIN;'));
|
$attributes[$index] = substr($attribute, strlen('BIN;'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -82,7 +75,7 @@ class LdapService
|
|||||||
* Get the details of a user from LDAP using the given username.
|
* Get the details of a user from LDAP using the given username.
|
||||||
* User found via configurable user filter.
|
* User found via configurable user filter.
|
||||||
*
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException|JsonDebugException
|
||||||
*/
|
*/
|
||||||
public function getUserDetails(string $userName): ?array
|
public function getUserDetails(string $userName): ?array
|
||||||
{
|
{
|
||||||
@ -126,7 +119,7 @@ class LdapService
|
|||||||
*/
|
*/
|
||||||
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
|
protected function getUserResponseProperty(array $userDetails, string $propertyKey, $defaultValue)
|
||||||
{
|
{
|
||||||
$isBinary = strpos($propertyKey, 'BIN;') === 0;
|
$isBinary = str_starts_with($propertyKey, 'BIN;');
|
||||||
$propertyKey = strtolower($propertyKey);
|
$propertyKey = strtolower($propertyKey);
|
||||||
$value = $defaultValue;
|
$value = $defaultValue;
|
||||||
|
|
||||||
@ -170,11 +163,11 @@ class LdapService
|
|||||||
* Bind the system user to the LDAP connection using the given credentials
|
* Bind the system user to the LDAP connection using the given credentials
|
||||||
* otherwise anonymous access is attempted.
|
* otherwise anonymous access is attempted.
|
||||||
*
|
*
|
||||||
* @param resource $connection
|
* @param resource|\LDAP\Connection $connection
|
||||||
*
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*/
|
*/
|
||||||
protected function bindSystemUser($connection)
|
protected function bindSystemUser($connection): void
|
||||||
{
|
{
|
||||||
$ldapDn = $this->config['dn'];
|
$ldapDn = $this->config['dn'];
|
||||||
$ldapPass = $this->config['pass'];
|
$ldapPass = $this->config['pass'];
|
||||||
@ -197,7 +190,7 @@ class LdapService
|
|||||||
*
|
*
|
||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
*
|
*
|
||||||
* @return resource
|
* @return resource|\LDAP\Connection
|
||||||
*/
|
*/
|
||||||
protected function getConnection()
|
protected function getConnection()
|
||||||
{
|
{
|
||||||
@ -216,8 +209,8 @@ class LdapService
|
|||||||
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
$this->ldap->setOption(null, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);
|
||||||
}
|
}
|
||||||
|
|
||||||
$serverDetails = $this->parseServerString($this->config['server']);
|
$ldapHost = $this->parseServerString($this->config['server']);
|
||||||
$ldapConnection = $this->ldap->connect($serverDetails['host'], $serverDetails['port']);
|
$ldapConnection = $this->ldap->connect($ldapHost);
|
||||||
|
|
||||||
if ($ldapConnection === false) {
|
if ($ldapConnection === false) {
|
||||||
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
throw new LdapException(trans('errors.ldap_cannot_connect'));
|
||||||
@ -242,23 +235,16 @@ class LdapService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse a LDAP server string and return the host and port for a connection.
|
* Parse an LDAP server string and return the host suitable for a connection.
|
||||||
* Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'.
|
* Is flexible to formats such as 'ldap.example.com:8069' or 'ldaps://ldap.example.com'.
|
||||||
*/
|
*/
|
||||||
protected function parseServerString(string $serverString): array
|
protected function parseServerString(string $serverString): string
|
||||||
{
|
{
|
||||||
$serverNameParts = explode(':', $serverString);
|
if (str_starts_with($serverString, 'ldaps://') || str_starts_with($serverString, 'ldap://')) {
|
||||||
|
return $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
|
return "ldap://{$serverString}";
|
||||||
$hostName = $serverNameParts[0];
|
|
||||||
$ldapPort = (count($serverNameParts) > 1) ? intval($serverNameParts[1]) : 389;
|
|
||||||
|
|
||||||
return ['host' => $hostName, 'port' => $ldapPort];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -386,7 +372,7 @@ class LdapService
|
|||||||
* @throws LdapException
|
* @throws LdapException
|
||||||
* @throws JsonDebugException
|
* @throws JsonDebugException
|
||||||
*/
|
*/
|
||||||
public function syncGroups(User $user, string $username)
|
public function syncGroups(User $user, string $username): void
|
||||||
{
|
{
|
||||||
$userLdapGroups = $this->getUserGroups($username);
|
$userLdapGroups = $this->getUserGroups($username);
|
||||||
$this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);
|
$this->groupSyncService->syncUserWithFoundGroups($user, $userLdapGroups, $this->config['remove_from_groups']);
|
@ -1,28 +1,26 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Access\Mfa\MfaSession;
|
||||||
use BookStack\Auth\Access\Mfa\MfaSession;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\LoginAttemptException;
|
use BookStack\Exceptions\LoginAttemptException;
|
||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
||||||
class LoginService
|
class LoginService
|
||||||
{
|
{
|
||||||
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
protected const LAST_LOGIN_ATTEMPTED_SESSION_KEY = 'auth-login-last-attempted';
|
||||||
|
|
||||||
protected $mfaSession;
|
public function __construct(
|
||||||
protected $emailConfirmationService;
|
protected MfaSession $mfaSession,
|
||||||
|
protected EmailConfirmationService $emailConfirmationService,
|
||||||
public function __construct(MfaSession $mfaSession, EmailConfirmationService $emailConfirmationService)
|
protected SocialDriverManager $socialDriverManager,
|
||||||
{
|
) {
|
||||||
$this->mfaSession = $mfaSession;
|
|
||||||
$this->emailConfirmationService = $emailConfirmationService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -163,4 +161,33 @@ class LoginService
|
|||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the current user out of the application.
|
||||||
|
* Returns an app post-redirect path.
|
||||||
|
*/
|
||||||
|
public function logout(): string
|
||||||
|
{
|
||||||
|
auth()->logout();
|
||||||
|
session()->invalidate();
|
||||||
|
session()->regenerateToken();
|
||||||
|
|
||||||
|
return $this->shouldAutoInitiate() ? '/login?prevent_auto_init=true' : '/';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if login auto-initiate should be active based upon authentication config.
|
||||||
|
*/
|
||||||
|
public function shouldAutoInitiate(): bool
|
||||||
|
{
|
||||||
|
$autoRedirect = config('auth.auto_initiate');
|
||||||
|
if (!$autoRedirect) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$socialDrivers = $this->socialDriverManager->getActive();
|
||||||
|
$authMethod = config('auth.method');
|
||||||
|
|
||||||
|
return count($socialDrivers) === 0 && in_array($authMethod, ['oidc', 'saml2']);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Mfa;
|
namespace BookStack\Access\Mfa;
|
||||||
|
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Mfa;
|
namespace BookStack\Access\Mfa;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
class MfaSession
|
class MfaSession
|
||||||
{
|
{
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Mfa;
|
namespace BookStack\Access\Mfa;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Users\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Mfa;
|
namespace BookStack\Access\Mfa;
|
||||||
|
|
||||||
use BaconQrCode\Renderer\Color\Rgb;
|
use BaconQrCode\Renderer\Color\Rgb;
|
||||||
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
use BaconQrCode\Renderer\Image\SvgImageBackEnd;
|
||||||
@ -8,7 +8,7 @@ use BaconQrCode\Renderer\ImageRenderer;
|
|||||||
use BaconQrCode\Renderer\RendererStyle\Fill;
|
use BaconQrCode\Renderer\RendererStyle\Fill;
|
||||||
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
use BaconQrCode\Renderer\RendererStyle\RendererStyle;
|
||||||
use BaconQrCode\Writer;
|
use BaconQrCode\Writer;
|
||||||
use BookStack\Auth\User;
|
use BookStack\Users\Models\User;
|
||||||
use PragmaRX\Google2FA\Google2FA;
|
use PragmaRX\Google2FA\Google2FA;
|
||||||
use PragmaRX\Google2FA\Support\Constants;
|
use PragmaRX\Google2FA\Support\Constants;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Mfa;
|
namespace BookStack\Access\Mfa;
|
||||||
|
|
||||||
use Illuminate\Contracts\Validation\Rule;
|
use Illuminate\Contracts\Validation\Rule;
|
||||||
|
|
26
app/Access/Notifications/ConfirmEmailNotification.php
Normal file
26
app/Access/Notifications/ConfirmEmailNotification.php
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access\Notifications;
|
||||||
|
|
||||||
|
use BookStack\App\MailNotification;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class ConfirmEmailNotification extends MailNotification
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $token
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$appName = ['appName' => setting('app-name')];
|
||||||
|
|
||||||
|
return $this->newMailMessage()
|
||||||
|
->subject(trans('auth.email_confirm_subject', $appName))
|
||||||
|
->greeting(trans('auth.email_confirm_greeting', $appName))
|
||||||
|
->line(trans('auth.email_confirm_text'))
|
||||||
|
->action(trans('auth.email_confirm_action'), url('/register/confirm/' . $this->token));
|
||||||
|
}
|
||||||
|
}
|
24
app/Access/Notifications/ResetPasswordNotification.php
Normal file
24
app/Access/Notifications/ResetPasswordNotification.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access\Notifications;
|
||||||
|
|
||||||
|
use BookStack\App\MailNotification;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class ResetPasswordNotification extends MailNotification
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $token
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
return $this->newMailMessage()
|
||||||
|
->subject(trans('auth.email_reset_subject', ['appName' => setting('app-name')]))
|
||||||
|
->line(trans('auth.email_reset_text'))
|
||||||
|
->action(trans('auth.reset_password'), url('password/reset/' . $this->token))
|
||||||
|
->line(trans('auth.email_reset_not_requested'));
|
||||||
|
}
|
||||||
|
}
|
27
app/Access/Notifications/UserInviteNotification.php
Normal file
27
app/Access/Notifications/UserInviteNotification.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access\Notifications;
|
||||||
|
|
||||||
|
use BookStack\App\MailNotification;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class UserInviteNotification extends MailNotification
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
public string $token
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
$appName = ['appName' => setting('app-name')];
|
||||||
|
$locale = $notifiable->getLocale();
|
||||||
|
|
||||||
|
return $this->newMailMessage($locale)
|
||||||
|
->subject($locale->trans('auth.user_invite_email_subject', $appName))
|
||||||
|
->greeting($locale->trans('auth.user_invite_email_greeting', $appName))
|
||||||
|
->line($locale->trans('auth.user_invite_email_text'))
|
||||||
|
->action($locale->trans('auth.user_invite_email_action'), url('/register/invite/' . $this->token));
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use InvalidArgumentException;
|
use InvalidArgumentException;
|
||||||
use League\OAuth2\Client\Token\AccessToken;
|
use League\OAuth2\Client\Token\AccessToken;
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -1,38 +1,19 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
class OidcIdToken
|
class OidcIdToken
|
||||||
{
|
{
|
||||||
/**
|
protected array $header;
|
||||||
* @var array
|
protected array $payload;
|
||||||
*/
|
protected string $signature;
|
||||||
protected $header;
|
protected string $issuer;
|
||||||
|
protected array $tokenParts = [];
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $payload;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $signature;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var array[]|string[]
|
* @var array[]|string[]
|
||||||
*/
|
*/
|
||||||
protected $keys;
|
protected array $keys;
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $issuer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var array
|
|
||||||
*/
|
|
||||||
protected $tokenParts = [];
|
|
||||||
|
|
||||||
public function __construct(string $token, string $issuer, array $keys)
|
public function __construct(string $token, string $issuer, array $keys)
|
||||||
{
|
{
|
||||||
@ -106,6 +87,14 @@ class OidcIdToken
|
|||||||
return $this->payload;
|
return $this->payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replace the existing claim data of this token with that provided.
|
||||||
|
*/
|
||||||
|
public function replaceClaims(array $claims): void
|
||||||
|
{
|
||||||
|
$this->payload = $claims;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate the structure of the given token and ensure we have the required pieces.
|
* Validate the structure of the given token and ensure we have the required pieces.
|
||||||
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
|
* As per https://datatracker.ietf.org/doc/html/rfc7519#section-7.2.
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
class OidcInvalidKeyException extends \Exception
|
class OidcInvalidKeyException extends \Exception
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use Exception;
|
use Exception;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use phpseclib3\Crypt\Common\PublicKey;
|
use phpseclib3\Crypt\Common\PublicKey;
|
||||||
use phpseclib3\Crypt\PublicKeyLoader;
|
use phpseclib3\Crypt\PublicKeyLoader;
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use League\OAuth2\Client\Grant\AbstractGrant;
|
use League\OAuth2\Client\Grant\AbstractGrant;
|
||||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||||
@ -20,15 +20,8 @@ class OidcOAuthProvider extends AbstractProvider
|
|||||||
{
|
{
|
||||||
use BearerAuthorizationTrait;
|
use BearerAuthorizationTrait;
|
||||||
|
|
||||||
/**
|
protected string $authorizationEndpoint;
|
||||||
* @var string
|
protected string $tokenEndpoint;
|
||||||
*/
|
|
||||||
protected $authorizationEndpoint;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string
|
|
||||||
*/
|
|
||||||
protected $tokenEndpoint;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scopes to use for the OIDC authorization call.
|
* Scopes to use for the OIDC authorization call.
|
||||||
@ -60,7 +53,7 @@ class OidcOAuthProvider extends AbstractProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add an additional scope to this provider upon the default.
|
* Add another scope to this provider upon the default.
|
||||||
*/
|
*/
|
||||||
public function addScope(string $scope): void
|
public function addScope(string $scope): void
|
||||||
{
|
{
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use GuzzleHttp\Psr7\Request;
|
use GuzzleHttp\Psr7\Request;
|
||||||
use Illuminate\Contracts\Cache\Repository;
|
use Illuminate\Contracts\Cache\Repository;
|
||||||
@ -21,6 +21,7 @@ class OidcProviderSettings
|
|||||||
public ?string $redirectUri;
|
public ?string $redirectUri;
|
||||||
public ?string $authorizationEndpoint;
|
public ?string $authorizationEndpoint;
|
||||||
public ?string $tokenEndpoint;
|
public ?string $tokenEndpoint;
|
||||||
|
public ?string $endSessionEndpoint;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var string[]|array[]
|
* @var string[]|array[]
|
||||||
@ -59,7 +60,7 @@ class OidcProviderSettings
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (strpos($this->issuer, 'https://') !== 0) {
|
if (!str_starts_with($this->issuer, 'https://')) {
|
||||||
throw new InvalidArgumentException('Issuer value must start with https://');
|
throw new InvalidArgumentException('Issuer value must start with https://');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -132,6 +133,10 @@ class OidcProviderSettings
|
|||||||
$discoveredSettings['keys'] = $this->filterKeys($keys);
|
$discoveredSettings['keys'] = $this->filterKeys($keys);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!empty($result['end_session_endpoint'])) {
|
||||||
|
$discoveredSettings['endSessionEndpoint'] = $result['end_session_endpoint'];
|
||||||
|
}
|
||||||
|
|
||||||
return $discoveredSettings;
|
return $discoveredSettings;
|
||||||
}
|
}
|
||||||
|
|
@ -1,19 +1,21 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access\Oidc;
|
namespace BookStack\Access\Oidc;
|
||||||
|
|
||||||
use BookStack\Auth\Access\GroupSyncService;
|
use BookStack\Access\GroupSyncService;
|
||||||
use BookStack\Auth\Access\LoginService;
|
use BookStack\Access\LoginService;
|
||||||
use BookStack\Auth\Access\RegistrationService;
|
use BookStack\Access\RegistrationService;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Facades\Theme;
|
||||||
|
use BookStack\Http\HttpRequestService;
|
||||||
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
use Illuminate\Support\Facades\Cache;
|
use Illuminate\Support\Facades\Cache;
|
||||||
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
use League\OAuth2\Client\OptionProvider\HttpBasicAuthOptionProvider;
|
||||||
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
use League\OAuth2\Client\Provider\Exception\IdentityProviderException;
|
||||||
use Psr\Http\Client\ClientInterface as HttpClient;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class OpenIdConnectService
|
* Class OpenIdConnectService
|
||||||
@ -21,24 +23,12 @@ use Psr\Http\Client\ClientInterface as HttpClient;
|
|||||||
*/
|
*/
|
||||||
class OidcService
|
class OidcService
|
||||||
{
|
{
|
||||||
protected RegistrationService $registrationService;
|
|
||||||
protected LoginService $loginService;
|
|
||||||
protected HttpClient $httpClient;
|
|
||||||
protected GroupSyncService $groupService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* OpenIdService constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
RegistrationService $registrationService,
|
protected RegistrationService $registrationService,
|
||||||
LoginService $loginService,
|
protected LoginService $loginService,
|
||||||
HttpClient $httpClient,
|
protected HttpRequestService $http,
|
||||||
GroupSyncService $groupService
|
protected GroupSyncService $groupService
|
||||||
) {
|
) {
|
||||||
$this->registrationService = $registrationService;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
$this->httpClient = $httpClient;
|
|
||||||
$this->groupService = $groupService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -94,6 +84,7 @@ class OidcService
|
|||||||
'redirectUri' => url('/oidc/callback'),
|
'redirectUri' => url('/oidc/callback'),
|
||||||
'authorizationEndpoint' => $config['authorization_endpoint'],
|
'authorizationEndpoint' => $config['authorization_endpoint'],
|
||||||
'tokenEndpoint' => $config['token_endpoint'],
|
'tokenEndpoint' => $config['token_endpoint'],
|
||||||
|
'endSessionEndpoint' => is_string($config['end_session_endpoint']) ? $config['end_session_endpoint'] : null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Use keys if configured
|
// Use keys if configured
|
||||||
@ -104,12 +95,20 @@ class OidcService
|
|||||||
// Run discovery
|
// Run discovery
|
||||||
if ($config['discover'] ?? false) {
|
if ($config['discover'] ?? false) {
|
||||||
try {
|
try {
|
||||||
$settings->discoverFromIssuer($this->httpClient, Cache::store(null), 15);
|
$settings->discoverFromIssuer($this->http->buildClient(5), Cache::store(null), 15);
|
||||||
} catch (OidcIssuerDiscoveryException $exception) {
|
} catch (OidcIssuerDiscoveryException $exception) {
|
||||||
throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
|
throw new OidcException('OIDC Discovery Error: ' . $exception->getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prevent use of RP-initiated logout if specifically disabled
|
||||||
|
// Or force use of a URL if specifically set.
|
||||||
|
if ($config['end_session_endpoint'] === false) {
|
||||||
|
$settings->endSessionEndpoint = null;
|
||||||
|
} else if (is_string($config['end_session_endpoint'])) {
|
||||||
|
$settings->endSessionEndpoint = $config['end_session_endpoint'];
|
||||||
|
}
|
||||||
|
|
||||||
$settings->validate();
|
$settings->validate();
|
||||||
|
|
||||||
return $settings;
|
return $settings;
|
||||||
@ -121,7 +120,7 @@ class OidcService
|
|||||||
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
|
protected function getProvider(OidcProviderSettings $settings): OidcOAuthProvider
|
||||||
{
|
{
|
||||||
$provider = new OidcOAuthProvider($settings->arrayForProvider(), [
|
$provider = new OidcOAuthProvider($settings->arrayForProvider(), [
|
||||||
'httpClient' => $this->httpClient,
|
'httpClient' => $this->http->buildClient(5),
|
||||||
'optionProvider' => new HttpBasicAuthOptionProvider(),
|
'optionProvider' => new HttpBasicAuthOptionProvider(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -152,10 +151,11 @@ class OidcService
|
|||||||
*/
|
*/
|
||||||
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
|
protected function getUserDisplayName(OidcIdToken $token, string $defaultValue): string
|
||||||
{
|
{
|
||||||
$displayNameAttr = $this->config()['display_name_claims'];
|
$displayNameAttrString = $this->config()['display_name_claims'] ?? '';
|
||||||
|
$displayNameAttrs = explode('|', $displayNameAttrString);
|
||||||
|
|
||||||
$displayName = [];
|
$displayName = [];
|
||||||
foreach ($displayNameAttr as $dnAttr) {
|
foreach ($displayNameAttrs as $dnAttr) {
|
||||||
$dnComponent = $token->getClaim($dnAttr) ?? '';
|
$dnComponent = $token->getClaim($dnAttr) ?? '';
|
||||||
if ($dnComponent !== '') {
|
if ($dnComponent !== '') {
|
||||||
$displayName[] = $dnComponent;
|
$displayName[] = $dnComponent;
|
||||||
@ -198,7 +198,8 @@ class OidcService
|
|||||||
*/
|
*/
|
||||||
protected function getUserDetails(OidcIdToken $token): array
|
protected function getUserDetails(OidcIdToken $token): array
|
||||||
{
|
{
|
||||||
$id = $token->getClaim('sub');
|
$idClaim = $this->config()['external_id_claim'];
|
||||||
|
$id = $token->getClaim($idClaim);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'external_id' => $id,
|
'external_id' => $id,
|
||||||
@ -225,6 +226,18 @@ class OidcService
|
|||||||
$settings->keys,
|
$settings->keys,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
session()->put("oidc_id_token", $idTokenText);
|
||||||
|
|
||||||
|
$returnClaims = Theme::dispatch(ThemeEvents::OIDC_ID_TOKEN_PRE_VALIDATE, $idToken->getAllClaims(), [
|
||||||
|
'access_token' => $accessToken->getToken(),
|
||||||
|
'expires_in' => $accessToken->getExpires(),
|
||||||
|
'refresh_token' => $accessToken->getRefreshToken(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!is_null($returnClaims)) {
|
||||||
|
$idToken->replaceClaims($returnClaims);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->config()['dump_user_details']) {
|
if ($this->config()['dump_user_details']) {
|
||||||
throw new JsonDebugException($idToken->getAllClaims());
|
throw new JsonDebugException($idToken->getAllClaims());
|
||||||
}
|
}
|
||||||
@ -282,4 +295,30 @@ class OidcService
|
|||||||
{
|
{
|
||||||
return $this->config()['user_to_groups'] !== false;
|
return $this->config()['user_to_groups'] !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the RP-initiated logout flow if active, otherwise start a standard logout flow.
|
||||||
|
* Returns a post-app-logout redirect URL.
|
||||||
|
* Reference: https://openid.net/specs/openid-connect-rpinitiated-1_0.html
|
||||||
|
* @throws OidcException
|
||||||
|
*/
|
||||||
|
public function logout(): string
|
||||||
|
{
|
||||||
|
$oidcToken = session()->pull("oidc_id_token");
|
||||||
|
$defaultLogoutUrl = url($this->loginService->logout());
|
||||||
|
$oidcSettings = $this->getProviderSettings();
|
||||||
|
|
||||||
|
if (!$oidcSettings->endSessionEndpoint) {
|
||||||
|
return $defaultLogoutUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
$endpointParams = [
|
||||||
|
'id_token_hint' => $oidcToken,
|
||||||
|
'post_logout_redirect_uri' => $defaultLogoutUrl,
|
||||||
|
];
|
||||||
|
|
||||||
|
$joiner = str_contains($oidcSettings->endSessionEndpoint, '?') ? '&' : '?';
|
||||||
|
|
||||||
|
return $oidcSettings->endSessionEndpoint . $joiner . http_build_query($endpointParams);
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,15 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Auth\SocialAccount;
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Auth\UserRepo;
|
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use BookStack\Facades\Activity;
|
use BookStack\Facades\Activity;
|
||||||
use BookStack\Facades\Theme;
|
use BookStack\Facades\Theme;
|
||||||
use BookStack\Theming\ThemeEvents;
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use BookStack\Users\UserRepo;
|
||||||
use Exception;
|
use Exception;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\JsonDebugException;
|
use BookStack\Exceptions\JsonDebugException;
|
||||||
use BookStack\Exceptions\SamlException;
|
use BookStack\Exceptions\SamlException;
|
||||||
use BookStack\Exceptions\StoppedAuthenticationException;
|
use BookStack\Exceptions\StoppedAuthenticationException;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Exception;
|
use Exception;
|
||||||
use OneLogin\Saml2\Auth;
|
use OneLogin\Saml2\Auth;
|
||||||
use OneLogin\Saml2\Constants;
|
use OneLogin\Saml2\Constants;
|
||||||
@ -21,19 +21,13 @@ use OneLogin\Saml2\ValidationError;
|
|||||||
class Saml2Service
|
class Saml2Service
|
||||||
{
|
{
|
||||||
protected array $config;
|
protected array $config;
|
||||||
protected RegistrationService $registrationService;
|
|
||||||
protected LoginService $loginService;
|
|
||||||
protected GroupSyncService $groupSyncService;
|
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
RegistrationService $registrationService,
|
protected RegistrationService $registrationService,
|
||||||
LoginService $loginService,
|
protected LoginService $loginService,
|
||||||
GroupSyncService $groupSyncService
|
protected GroupSyncService $groupSyncService
|
||||||
) {
|
) {
|
||||||
$this->config = config('saml2');
|
$this->config = config('saml2');
|
||||||
$this->registrationService = $registrationService;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
$this->groupSyncService = $groupSyncService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -54,20 +48,23 @@ class Saml2Service
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a logout flow.
|
* Initiate a logout flow.
|
||||||
|
* Returns the SAML2 request ID, and the URL to redirect the user to.
|
||||||
*
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
|
* @returns array{url: string, id: ?string}
|
||||||
*/
|
*/
|
||||||
public function logout(User $user): array
|
public function logout(User $user): array
|
||||||
{
|
{
|
||||||
$toolKit = $this->getToolkit();
|
$toolKit = $this->getToolkit();
|
||||||
$returnRoute = url('/');
|
$sessionIndex = session()->get('saml2_session_index');
|
||||||
|
$returnUrl = url($this->loginService->logout());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$url = $toolKit->logout(
|
$url = $toolKit->logout(
|
||||||
$returnRoute,
|
$returnUrl,
|
||||||
[],
|
[],
|
||||||
$user->email,
|
$user->email,
|
||||||
null,
|
$sessionIndex,
|
||||||
true,
|
true,
|
||||||
Constants::NAMEID_EMAIL_ADDRESS
|
Constants::NAMEID_EMAIL_ADDRESS
|
||||||
);
|
);
|
||||||
@ -77,8 +74,7 @@ class Saml2Service
|
|||||||
throw $error;
|
throw $error;
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actionLogout();
|
$url = $returnUrl;
|
||||||
$url = '/';
|
|
||||||
$id = null;
|
$id = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +114,7 @@ class Saml2Service
|
|||||||
|
|
||||||
$attrs = $toolkit->getAttributes();
|
$attrs = $toolkit->getAttributes();
|
||||||
$id = $toolkit->getNameId();
|
$id = $toolkit->getNameId();
|
||||||
|
session()->put('saml2_session_index', $toolkit->getSessionIndex());
|
||||||
|
|
||||||
return $this->processLoginCallback($id, $attrs);
|
return $this->processLoginCallback($id, $attrs);
|
||||||
}
|
}
|
||||||
@ -127,7 +124,7 @@ class Saml2Service
|
|||||||
*
|
*
|
||||||
* @throws Error
|
* @throws Error
|
||||||
*/
|
*/
|
||||||
public function processSlsResponse(?string $requestId): ?string
|
public function processSlsResponse(?string $requestId): string
|
||||||
{
|
{
|
||||||
$toolkit = $this->getToolkit();
|
$toolkit = $this->getToolkit();
|
||||||
|
|
||||||
@ -136,7 +133,7 @@ class Saml2Service
|
|||||||
// value so that the exact encoding format is matched when checking the signature.
|
// value so that the exact encoding format is matched when checking the signature.
|
||||||
// This is primarily due to ADFS encoding query params with lowercase percent encoding while
|
// This is primarily due to ADFS encoding query params with lowercase percent encoding while
|
||||||
// PHP (And most other sensible providers) standardise on uppercase.
|
// PHP (And most other sensible providers) standardise on uppercase.
|
||||||
$redirect = $toolkit->processSLO(true, $requestId, true, null, true);
|
$samlRedirect = $toolkit->processSLO(true, $requestId, true, null, true);
|
||||||
$errors = $toolkit->getErrors();
|
$errors = $toolkit->getErrors();
|
||||||
|
|
||||||
if (!empty($errors)) {
|
if (!empty($errors)) {
|
||||||
@ -145,18 +142,9 @@ class Saml2Service
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->actionLogout();
|
$defaultBookStackRedirect = $this->loginService->logout();
|
||||||
|
|
||||||
return $redirect;
|
return $samlRedirect ?? $defaultBookStackRedirect;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Do the required actions to log a user out.
|
|
||||||
*/
|
|
||||||
protected function actionLogout()
|
|
||||||
{
|
|
||||||
auth()->logout();
|
|
||||||
session()->invalidate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -356,6 +344,10 @@ class Saml2Service
|
|||||||
$userDetails = $this->getUserDetails($samlID, $samlAttributes);
|
$userDetails = $this->getUserDetails($samlID, $samlAttributes);
|
||||||
$isLoggedIn = auth()->check();
|
$isLoggedIn = auth()->check();
|
||||||
|
|
||||||
|
if ($this->shouldSyncGroups()) {
|
||||||
|
$userDetails['groups'] = $this->getUserGroups($samlAttributes);
|
||||||
|
}
|
||||||
|
|
||||||
if ($this->config['dump_user_details']) {
|
if ($this->config['dump_user_details']) {
|
||||||
throw new JsonDebugException([
|
throw new JsonDebugException([
|
||||||
'id_from_idp' => $samlID,
|
'id_from_idp' => $samlID,
|
||||||
@ -378,13 +370,8 @@ class Saml2Service
|
|||||||
$userDetails['external_id']
|
$userDetails['external_id']
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($user === null) {
|
|
||||||
throw new SamlException(trans('errors.saml_user_not_registered', ['name' => $userDetails['external_id']]), '/login');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->shouldSyncGroups()) {
|
if ($this->shouldSyncGroups()) {
|
||||||
$groups = $this->getUserGroups($samlAttributes);
|
$this->groupSyncService->syncUserWithFoundGroups($user, $userDetails['groups'], $this->config['remove_from_groups']);
|
||||||
$this->groupSyncService->syncUserWithFoundGroups($user, $groups, $this->config['remove_from_groups']);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->loginService->login($user, 'saml2');
|
$this->loginService->login($user, 'saml2');
|
@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Activity\Models\Loggable;
|
||||||
use BookStack\Model;
|
use BookStack\App\Model;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class SocialAccount.
|
* Class SocialAccount.
|
@ -1,70 +1,25 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\SocialAccount;
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\SocialDriverNotConfigured;
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
use BookStack\Exceptions\SocialSignInAccountNotUsed;
|
||||||
use BookStack\Exceptions\UserRegistrationException;
|
use BookStack\Exceptions\UserRegistrationException;
|
||||||
use Illuminate\Support\Facades\Event;
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
use Laravel\Socialite\Contracts\Factory as Socialite;
|
use Laravel\Socialite\Contracts\Factory as Socialite;
|
||||||
use Laravel\Socialite\Contracts\Provider;
|
use Laravel\Socialite\Contracts\Provider;
|
||||||
use Laravel\Socialite\Contracts\User as SocialUser;
|
use Laravel\Socialite\Contracts\User as SocialUser;
|
||||||
use Laravel\Socialite\Two\GoogleProvider;
|
use Laravel\Socialite\Two\GoogleProvider;
|
||||||
use SocialiteProviders\Manager\SocialiteWasCalled;
|
|
||||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||||
|
|
||||||
class SocialAuthService
|
class SocialAuthService
|
||||||
{
|
{
|
||||||
/**
|
public function __construct(
|
||||||
* The core socialite library used.
|
protected Socialite $socialite,
|
||||||
*
|
protected LoginService $loginService,
|
||||||
* @var Socialite
|
protected SocialDriverManager $driverManager,
|
||||||
*/
|
) {
|
||||||
protected $socialite;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var LoginService
|
|
||||||
*/
|
|
||||||
protected $loginService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default built-in social drivers we support.
|
|
||||||
*
|
|
||||||
* @var string[]
|
|
||||||
*/
|
|
||||||
protected $validSocialDrivers = [
|
|
||||||
'google',
|
|
||||||
'github',
|
|
||||||
'facebook',
|
|
||||||
'slack',
|
|
||||||
'twitter',
|
|
||||||
'azure',
|
|
||||||
'okta',
|
|
||||||
'gitlab',
|
|
||||||
'twitch',
|
|
||||||
'discord',
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callbacks to run when configuring a social driver
|
|
||||||
* for an initial redirect action.
|
|
||||||
* Array is keyed by social driver name.
|
|
||||||
* Callbacks are passed an instance of the driver.
|
|
||||||
*
|
|
||||||
* @var array<string, callable>
|
|
||||||
*/
|
|
||||||
protected $configureForRedirectCallbacks = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* SocialAuthService constructor.
|
|
||||||
*/
|
|
||||||
public function __construct(Socialite $socialite, LoginService $loginService)
|
|
||||||
{
|
|
||||||
$this->socialite = $socialite;
|
|
||||||
$this->loginService = $loginService;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,9 +29,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function startLogIn(string $socialDriver): RedirectResponse
|
public function startLogIn(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($socialDriver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,9 +42,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function startRegister(string $socialDriver): RedirectResponse
|
public function startRegister(string $socialDriver): RedirectResponse
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->getDriverForRedirect($driver)->redirect();
|
return $this->getDriverForRedirect($socialDriver)->redirect();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -119,9 +76,10 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function getSocialUser(string $socialDriver): SocialUser
|
public function getSocialUser(string $socialDriver): SocialUser
|
||||||
{
|
{
|
||||||
$driver = $this->validateDriver($socialDriver);
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
|
$this->driverManager->ensureDriverActive($socialDriver);
|
||||||
|
|
||||||
return $this->socialite->driver($driver)->user();
|
return $this->socialite->driver($socialDriver)->user();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -131,6 +89,7 @@ class SocialAuthService
|
|||||||
*/
|
*/
|
||||||
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
public function handleLoginCallback(string $socialDriver, SocialUser $socialUser)
|
||||||
{
|
{
|
||||||
|
$socialDriver = trim(strtolower($socialDriver));
|
||||||
$socialId = $socialUser->getId();
|
$socialId = $socialUser->getId();
|
||||||
|
|
||||||
// Get any attached social accounts or users
|
// Get any attached social accounts or users
|
||||||
@ -154,21 +113,21 @@ class SocialAuthService
|
|||||||
$currentUser->socialAccounts()->save($account);
|
$currentUser->socialAccounts()->save($account);
|
||||||
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('success', trans('settings.users_social_connected', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect('/my-account/auth#social_accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in and the social account exists and is already linked to the current user.
|
// When a user is logged in and the social account exists and is already linked to the current user.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id === $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect('/my-account/auth#social_accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
// When a user is logged in, A social account exists but the users do not match.
|
// When a user is logged in, A social account exists but the users do not match.
|
||||||
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
if ($isLoggedIn && $socialAccount !== null && $socialAccount->user->id != $currentUser->id) {
|
||||||
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
session()->flash('error', trans('errors.social_account_already_used_existing', ['socialAccount' => $titleCaseDriver]));
|
||||||
|
|
||||||
return redirect($currentUser->getEditUrl());
|
return redirect('/my-account/auth#social_accounts');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise let the user know this social account is not used by anyone.
|
// Otherwise let the user know this social account is not used by anyone.
|
||||||
@ -181,75 +140,11 @@ class SocialAuthService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure the social driver is correct and supported.
|
* Get the social driver manager used by this service.
|
||||||
*
|
|
||||||
* @throws SocialDriverNotConfigured
|
|
||||||
*/
|
*/
|
||||||
protected function validateDriver(string $socialDriver): string
|
public function drivers(): SocialDriverManager
|
||||||
{
|
{
|
||||||
$driver = trim(strtolower($socialDriver));
|
return $this->driverManager;
|
||||||
|
|
||||||
if (!in_array($driver, $this->validSocialDrivers)) {
|
|
||||||
abort(404, trans('errors.social_driver_not_found'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!$this->checkDriverConfigured($driver)) {
|
|
||||||
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($socialDriver)]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $driver;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check a social driver has been configured correctly.
|
|
||||||
*/
|
|
||||||
protected function checkDriverConfigured(string $driver): bool
|
|
||||||
{
|
|
||||||
$lowerName = strtolower($driver);
|
|
||||||
$configPrefix = 'services.' . $lowerName . '.';
|
|
||||||
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
|
||||||
|
|
||||||
return !in_array(false, $config) && !in_array(null, $config);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the names of the active social drivers.
|
|
||||||
*/
|
|
||||||
public function getActiveDrivers(): array
|
|
||||||
{
|
|
||||||
$activeDrivers = [];
|
|
||||||
|
|
||||||
foreach ($this->validSocialDrivers as $driverKey) {
|
|
||||||
if ($this->checkDriverConfigured($driverKey)) {
|
|
||||||
$activeDrivers[$driverKey] = $this->getDriverName($driverKey);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $activeDrivers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the presentational name for a driver.
|
|
||||||
*/
|
|
||||||
public function getDriverName(string $driver): string
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.name');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current config for the given driver allows auto-registration.
|
|
||||||
*/
|
|
||||||
public function driverAutoRegisterEnabled(string $driver): bool
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.auto_register') === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current config for the given driver allow email address auto-confirmation.
|
|
||||||
*/
|
|
||||||
public function driverAutoConfirmEmailEnabled(string $driver): bool
|
|
||||||
{
|
|
||||||
return config('services.' . strtolower($driver) . '.auto_confirm') === true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -283,33 +178,8 @@ class SocialAuthService
|
|||||||
$driver->with(['prompt' => 'select_account']);
|
$driver->with(['prompt' => 'select_account']);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->configureForRedirectCallbacks[$driverName])) {
|
$this->driverManager->getConfigureForRedirectCallback($driverName)($driver);
|
||||||
$this->configureForRedirectCallbacks[$driverName]($driver);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $driver;
|
return $driver;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a custom socialite driver to be used.
|
|
||||||
* Driver name should be lower_snake_case.
|
|
||||||
* Config array should mirror the structure of a service
|
|
||||||
* within the `Config/services.php` file.
|
|
||||||
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
|
||||||
*/
|
|
||||||
public function addSocialDriver(
|
|
||||||
string $driverName,
|
|
||||||
array $config,
|
|
||||||
string $socialiteHandler,
|
|
||||||
callable $configureForRedirect = null
|
|
||||||
) {
|
|
||||||
$this->validSocialDrivers[] = $driverName;
|
|
||||||
config()->set('services.' . $driverName, $config);
|
|
||||||
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
|
||||||
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
|
||||||
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
|
||||||
if (!is_null($configureForRedirect)) {
|
|
||||||
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
147
app/Access/SocialDriverManager.php
Normal file
147
app/Access/SocialDriverManager.php
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Access;
|
||||||
|
|
||||||
|
use BookStack\Exceptions\SocialDriverNotConfigured;
|
||||||
|
use Illuminate\Support\Facades\Event;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
|
use SocialiteProviders\Manager\SocialiteWasCalled;
|
||||||
|
|
||||||
|
class SocialDriverManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The default built-in social drivers we support.
|
||||||
|
*
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
protected array $validDrivers = [
|
||||||
|
'google',
|
||||||
|
'github',
|
||||||
|
'facebook',
|
||||||
|
'slack',
|
||||||
|
'twitter',
|
||||||
|
'azure',
|
||||||
|
'okta',
|
||||||
|
'gitlab',
|
||||||
|
'twitch',
|
||||||
|
'discord',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callbacks to run when configuring a social driver
|
||||||
|
* for an initial redirect action.
|
||||||
|
* Array is keyed by social driver name.
|
||||||
|
* Callbacks are passed an instance of the driver.
|
||||||
|
*
|
||||||
|
* @var array<string, callable>
|
||||||
|
*/
|
||||||
|
protected array $configureForRedirectCallbacks = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current config for the given driver allows auto-registration.
|
||||||
|
*/
|
||||||
|
public function isAutoRegisterEnabled(string $driver): bool
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'auto_register') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current config for the given driver allow email address auto-confirmation.
|
||||||
|
*/
|
||||||
|
public function isAutoConfirmEmailEnabled(string $driver): bool
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'auto_confirm') === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the names of the active social drivers, keyed by driver id.
|
||||||
|
* @returns array<string, string>
|
||||||
|
*/
|
||||||
|
public function getActive(): array
|
||||||
|
{
|
||||||
|
$activeDrivers = [];
|
||||||
|
|
||||||
|
foreach ($this->validDrivers as $driverKey) {
|
||||||
|
if ($this->checkDriverConfigured($driverKey)) {
|
||||||
|
$activeDrivers[$driverKey] = $this->getName($driverKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $activeDrivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the configure-for-redirect callback for the given driver.
|
||||||
|
* This is a callable that allows modification of the driver at redirect time.
|
||||||
|
* Commonly used to perform custom dynamic configuration where required.
|
||||||
|
* The callback is passed a \Laravel\Socialite\Contracts\Provider instance.
|
||||||
|
*/
|
||||||
|
public function getConfigureForRedirectCallback(string $driver): callable
|
||||||
|
{
|
||||||
|
return $this->configureForRedirectCallbacks[$driver] ?? (fn() => true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a custom socialite driver to be used.
|
||||||
|
* Driver name should be lower_snake_case.
|
||||||
|
* Config array should mirror the structure of a service
|
||||||
|
* within the `Config/services.php` file.
|
||||||
|
* Handler should be a Class@method handler to the SocialiteWasCalled event.
|
||||||
|
*/
|
||||||
|
public function addSocialDriver(
|
||||||
|
string $driverName,
|
||||||
|
array $config,
|
||||||
|
string $socialiteHandler,
|
||||||
|
callable $configureForRedirect = null
|
||||||
|
) {
|
||||||
|
$this->validDrivers[] = $driverName;
|
||||||
|
config()->set('services.' . $driverName, $config);
|
||||||
|
config()->set('services.' . $driverName . '.redirect', url('/login/service/' . $driverName . '/callback'));
|
||||||
|
config()->set('services.' . $driverName . '.name', $config['name'] ?? $driverName);
|
||||||
|
Event::listen(SocialiteWasCalled::class, $socialiteHandler);
|
||||||
|
if (!is_null($configureForRedirect)) {
|
||||||
|
$this->configureForRedirectCallbacks[$driverName] = $configureForRedirect;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the presentational name for a driver.
|
||||||
|
*/
|
||||||
|
protected function getName(string $driver): string
|
||||||
|
{
|
||||||
|
return $this->getDriverConfigProperty($driver, 'name') ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getDriverConfigProperty(string $driver, string $property): mixed
|
||||||
|
{
|
||||||
|
return config("services.{$driver}.{$property}");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the social driver is correct and supported.
|
||||||
|
*
|
||||||
|
* @throws SocialDriverNotConfigured
|
||||||
|
*/
|
||||||
|
public function ensureDriverActive(string $driverName): void
|
||||||
|
{
|
||||||
|
if (!in_array($driverName, $this->validDrivers)) {
|
||||||
|
abort(404, trans('errors.social_driver_not_found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->checkDriverConfigured($driverName)) {
|
||||||
|
throw new SocialDriverNotConfigured(trans('errors.social_driver_not_configured', ['socialAccount' => Str::title($driverName)]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check a social driver has been configured correctly.
|
||||||
|
*/
|
||||||
|
protected function checkDriverConfigured(string $driver): bool
|
||||||
|
{
|
||||||
|
$lowerName = strtolower($driver);
|
||||||
|
$configPrefix = 'services.' . $lowerName . '.';
|
||||||
|
$config = [config($configPrefix . 'client_id'), config($configPrefix . 'client_secret'), config('services.callback_url')];
|
||||||
|
|
||||||
|
return !in_array(false, $config) && !in_array(null, $config);
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,23 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\Access\Notifications\UserInviteNotification;
|
||||||
use BookStack\Notifications\UserInvite;
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
class UserInviteService extends UserTokenService
|
class UserInviteService extends UserTokenService
|
||||||
{
|
{
|
||||||
protected $tokenTable = 'user_invites';
|
protected string $tokenTable = 'user_invites';
|
||||||
protected $expiryTime = 336; // Two weeks
|
protected int $expiryTime = 336; // Two weeks
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an invitation to a user to sign into BookStack
|
* Send an invitation to a user to sign into BookStack
|
||||||
* Removes existing invitation tokens.
|
* Removes existing invitation tokens.
|
||||||
*
|
|
||||||
* @param User $user
|
|
||||||
*/
|
*/
|
||||||
public function sendInvitation(User $user)
|
public function sendInvitation(User $user)
|
||||||
{
|
{
|
||||||
$this->deleteByUser($user);
|
$this->deleteByUser($user);
|
||||||
$token = $this->createTokenForUser($user);
|
$token = $this->createTokenForUser($user);
|
||||||
$user->notify(new UserInvite($token));
|
$user->notify(new UserInviteNotification($token));
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,10 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Auth\Access;
|
namespace BookStack\Access;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Exceptions\UserTokenExpiredException;
|
use BookStack\Exceptions\UserTokenExpiredException;
|
||||||
use BookStack\Exceptions\UserTokenNotFoundException;
|
use BookStack\Exceptions\UserTokenNotFoundException;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
@ -14,41 +14,29 @@ class UserTokenService
|
|||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Name of table where user tokens are stored.
|
* Name of table where user tokens are stored.
|
||||||
*
|
|
||||||
* @var string
|
|
||||||
*/
|
*/
|
||||||
protected $tokenTable = 'user_tokens';
|
protected string $tokenTable = 'user_tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Token expiry time in hours.
|
* Token expiry time in hours.
|
||||||
*
|
|
||||||
* @var int
|
|
||||||
*/
|
*/
|
||||||
protected $expiryTime = 24;
|
protected int $expiryTime = 24;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all email confirmations that belong to a user.
|
* Delete all tokens that belong to a user.
|
||||||
*
|
|
||||||
* @param User $user
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
*/
|
||||||
public function deleteByUser(User $user)
|
public function deleteByUser(User $user): void
|
||||||
{
|
{
|
||||||
return DB::table($this->tokenTable)
|
DB::table($this->tokenTable)
|
||||||
->where('user_id', '=', $user->id)
|
->where('user_id', '=', $user->id)
|
||||||
->delete();
|
->delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the user id from a token, while check the token exists and has not expired.
|
* Get the user id from a token, while checking the token exists and has not expired.
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
*
|
*
|
||||||
* @throws UserTokenNotFoundException
|
* @throws UserTokenNotFoundException
|
||||||
* @throws UserTokenExpiredException
|
* @throws UserTokenExpiredException
|
||||||
*
|
|
||||||
* @return int
|
|
||||||
*/
|
*/
|
||||||
public function checkTokenAndGetUserId(string $token): int
|
public function checkTokenAndGetUserId(string $token): int
|
||||||
{
|
{
|
||||||
@ -67,8 +55,6 @@ class UserTokenService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a unique token within the email confirmation database.
|
* Creates a unique token within the email confirmation database.
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
protected function generateToken(): string
|
protected function generateToken(): string
|
||||||
{
|
{
|
||||||
@ -82,10 +68,6 @@ class UserTokenService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate and store a token for the given user.
|
* Generate and store a token for the given user.
|
||||||
*
|
|
||||||
* @param User $user
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
*/
|
||||||
protected function createTokenForUser(User $user): string
|
protected function createTokenForUser(User $user): string
|
||||||
{
|
{
|
||||||
@ -102,10 +84,6 @@ class UserTokenService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given token exists.
|
* Check if the given token exists.
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
protected function tokenExists(string $token): bool
|
protected function tokenExists(string $token): bool
|
||||||
{
|
{
|
||||||
@ -115,12 +93,8 @@ class UserTokenService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a token entry for the given token.
|
* Get a token entry for the given token.
|
||||||
*
|
|
||||||
* @param string $token
|
|
||||||
*
|
|
||||||
* @return object|null
|
|
||||||
*/
|
*/
|
||||||
protected function getEntryByToken(string $token)
|
protected function getEntryByToken(string $token): ?stdClass
|
||||||
{
|
{
|
||||||
return DB::table($this->tokenTable)
|
return DB::table($this->tokenTable)
|
||||||
->where('token', '=', $token)
|
->where('token', '=', $token)
|
||||||
@ -129,10 +103,6 @@ class UserTokenService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if the given token entry has expired.
|
* Check if the given token entry has expired.
|
||||||
*
|
|
||||||
* @param stdClass $tokenEntry
|
|
||||||
*
|
|
||||||
* @return bool
|
|
||||||
*/
|
*/
|
||||||
protected function entryExpired(stdClass $tokenEntry): bool
|
protected function entryExpired(stdClass $tokenEntry): bool
|
||||||
{
|
{
|
@ -1,60 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
|
||||||
|
|
||||||
use BookStack\Model;
|
|
||||||
use BookStack\Traits\HasCreatorAndUpdater;
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @property int $id
|
|
||||||
* @property string $text
|
|
||||||
* @property string $html
|
|
||||||
* @property int|null $parent_id
|
|
||||||
* @property int $local_id
|
|
||||||
*/
|
|
||||||
class Comment extends Model
|
|
||||||
{
|
|
||||||
use HasFactory;
|
|
||||||
use HasCreatorAndUpdater;
|
|
||||||
|
|
||||||
protected $fillable = ['text', 'parent_id'];
|
|
||||||
protected $appends = ['created', 'updated'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the entity that this comment belongs to.
|
|
||||||
*/
|
|
||||||
public function entity(): MorphTo
|
|
||||||
{
|
|
||||||
return $this->morphTo('entity');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if a comment has been updated since creation.
|
|
||||||
*/
|
|
||||||
public function isUpdated(): bool
|
|
||||||
{
|
|
||||||
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get created date as a relative diff.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getCreatedAttribute()
|
|
||||||
{
|
|
||||||
return $this->created_at->diffForHumans();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get updated date as a relative diff.
|
|
||||||
*
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function getUpdatedAttribute()
|
|
||||||
{
|
|
||||||
return $this->updated_at->diffForHumans();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,82 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Facades\Theme;
|
|
||||||
use BookStack\Interfaces\Loggable;
|
|
||||||
use BookStack\Theming\ThemeEvents;
|
|
||||||
use Illuminate\Bus\Queueable;
|
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
|
||||||
use Illuminate\Queue\InteractsWithQueue;
|
|
||||||
use Illuminate\Queue\SerializesModels;
|
|
||||||
use Illuminate\Support\Facades\Http;
|
|
||||||
use Illuminate\Support\Facades\Log;
|
|
||||||
|
|
||||||
class DispatchWebhookJob implements ShouldQueue
|
|
||||||
{
|
|
||||||
use Dispatchable;
|
|
||||||
use InteractsWithQueue;
|
|
||||||
use Queueable;
|
|
||||||
use SerializesModels;
|
|
||||||
|
|
||||||
protected Webhook $webhook;
|
|
||||||
protected string $event;
|
|
||||||
protected User $initiator;
|
|
||||||
protected int $initiatedTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @var string|Loggable
|
|
||||||
*/
|
|
||||||
protected $detail;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new job instance.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function __construct(Webhook $webhook, string $event, $detail)
|
|
||||||
{
|
|
||||||
$this->webhook = $webhook;
|
|
||||||
$this->event = $event;
|
|
||||||
$this->detail = $detail;
|
|
||||||
$this->initiator = user();
|
|
||||||
$this->initiatedTime = time();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute the job.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function handle()
|
|
||||||
{
|
|
||||||
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime);
|
|
||||||
$webhookData = $themeResponse ?? WebhookFormatter::getDefault($this->event, $this->webhook, $this->detail, $this->initiator, $this->initiatedTime)->format();
|
|
||||||
$lastError = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
$response = Http::asJson()
|
|
||||||
->withOptions(['allow_redirects' => ['strict' => true]])
|
|
||||||
->timeout($this->webhook->timeout)
|
|
||||||
->post($this->webhook->endpoint, $webhookData);
|
|
||||||
} catch (\Exception $exception) {
|
|
||||||
$lastError = $exception->getMessage();
|
|
||||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isset($response) && $response->failed()) {
|
|
||||||
$lastError = "Response status from endpoint was {$response->status()}";
|
|
||||||
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}");
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->webhook->last_called_at = now();
|
|
||||||
if ($lastError) {
|
|
||||||
$this->webhook->last_errored_at = now();
|
|
||||||
$this->webhook->last_error = $lastError;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->webhook->save();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,19 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
|
||||||
|
|
||||||
use BookStack\Model;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
|
||||||
|
|
||||||
class Favourite extends Model
|
|
||||||
{
|
|
||||||
protected $fillable = ['user_id'];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the related model that can be favourited.
|
|
||||||
*/
|
|
||||||
public function favouritable(): MorphTo
|
|
||||||
{
|
|
||||||
return $this->morphTo();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,13 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity;
|
||||||
|
|
||||||
use BookStack\Auth\Permissions\PermissionApplicator;
|
use BookStack\Activity\Models\Activity;
|
||||||
use BookStack\Auth\User;
|
|
||||||
use BookStack\Entities\Models\Book;
|
use BookStack\Entities\Models\Book;
|
||||||
use BookStack\Entities\Models\Chapter;
|
use BookStack\Entities\Models\Chapter;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Builder;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||||
|
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity;
|
||||||
|
|
||||||
class ActivityType
|
class ActivityType
|
||||||
{
|
{
|
||||||
@ -27,6 +27,10 @@ class ActivityType
|
|||||||
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
const BOOKSHELF_DELETE = 'bookshelf_delete';
|
||||||
|
|
||||||
const COMMENTED_ON = 'commented_on';
|
const COMMENTED_ON = 'commented_on';
|
||||||
|
const COMMENT_CREATE = 'comment_create';
|
||||||
|
const COMMENT_UPDATE = 'comment_update';
|
||||||
|
const COMMENT_DELETE = 'comment_delete';
|
||||||
|
|
||||||
const PERMISSIONS_UPDATE = 'permissions_update';
|
const PERMISSIONS_UPDATE = 'permissions_update';
|
||||||
|
|
||||||
const REVISION_RESTORE = 'revision_restore';
|
const REVISION_RESTORE = 'revision_restore';
|
@ -1,32 +1,20 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Comment;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Facades\Activity as ActivityService;
|
use BookStack\Facades\Activity as ActivityService;
|
||||||
use League\CommonMark\CommonMarkConverter;
|
use League\CommonMark\CommonMarkConverter;
|
||||||
|
|
||||||
/**
|
|
||||||
* Class CommentRepo.
|
|
||||||
*/
|
|
||||||
class CommentRepo
|
class CommentRepo
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* @var Comment
|
|
||||||
*/
|
|
||||||
protected $comment;
|
|
||||||
|
|
||||||
public function __construct(Comment $comment)
|
|
||||||
{
|
|
||||||
$this->comment = $comment;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a comment by ID.
|
* Get a comment by ID.
|
||||||
*/
|
*/
|
||||||
public function getById(int $id): Comment
|
public function getById(int $id): Comment
|
||||||
{
|
{
|
||||||
return $this->comment->newQuery()->findOrFail($id);
|
return Comment::query()->findOrFail($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -35,7 +23,7 @@ class CommentRepo
|
|||||||
public function create(Entity $entity, string $text, ?int $parent_id): Comment
|
public function create(Entity $entity, string $text, ?int $parent_id): Comment
|
||||||
{
|
{
|
||||||
$userId = user()->id;
|
$userId = user()->id;
|
||||||
$comment = $this->comment->newInstance();
|
$comment = new Comment();
|
||||||
|
|
||||||
$comment->text = $text;
|
$comment->text = $text;
|
||||||
$comment->html = $this->commentToHtml($text);
|
$comment->html = $this->commentToHtml($text);
|
||||||
@ -45,6 +33,7 @@ class CommentRepo
|
|||||||
$comment->parent_id = $parent_id;
|
$comment->parent_id = $parent_id;
|
||||||
|
|
||||||
$entity->comments()->save($comment);
|
$entity->comments()->save($comment);
|
||||||
|
ActivityService::add(ActivityType::COMMENT_CREATE, $comment);
|
||||||
ActivityService::add(ActivityType::COMMENTED_ON, $entity);
|
ActivityService::add(ActivityType::COMMENTED_ON, $entity);
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
@ -60,6 +49,8 @@ class CommentRepo
|
|||||||
$comment->html = $this->commentToHtml($text);
|
$comment->html = $this->commentToHtml($text);
|
||||||
$comment->save();
|
$comment->save();
|
||||||
|
|
||||||
|
ActivityService::add(ActivityType::COMMENT_UPDATE, $comment);
|
||||||
|
|
||||||
return $comment;
|
return $comment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,6 +60,8 @@ class CommentRepo
|
|||||||
public function delete(Comment $comment): void
|
public function delete(Comment $comment): void
|
||||||
{
|
{
|
||||||
$comment->delete();
|
$comment->delete();
|
||||||
|
|
||||||
|
ActivityService::add(ActivityType::COMMENT_DELETE, $comment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -82,7 +75,7 @@ class CommentRepo
|
|||||||
'allow_unsafe_links' => false,
|
'allow_unsafe_links' => false,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return $converter->convertToHtml($commentText);
|
return $converter->convert($commentText);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -90,9 +83,8 @@ class CommentRepo
|
|||||||
*/
|
*/
|
||||||
protected function getNextLocalId(Entity $entity): int
|
protected function getNextLocalId(Entity $entity): int
|
||||||
{
|
{
|
||||||
/** @var Comment $comment */
|
$currentMaxId = $entity->comments()->max('local_id');
|
||||||
$comment = $entity->comments(false)->orderBy('local_id', 'desc')->first();
|
|
||||||
|
|
||||||
return ($comment->local_id ?? 0) + 1;
|
return $currentMaxId + 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,12 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\Activity;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\DB;
|
|
||||||
|
|
||||||
class AuditLogController extends Controller
|
class AuditLogController extends Controller
|
||||||
{
|
{
|
@ -1,19 +1,18 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\CommentRepo;
|
use BookStack\Activity\CommentRepo;
|
||||||
use BookStack\Entities\Models\Page;
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
class CommentController extends Controller
|
class CommentController extends Controller
|
||||||
{
|
{
|
||||||
protected $commentRepo;
|
public function __construct(
|
||||||
|
protected CommentRepo $commentRepo
|
||||||
public function __construct(CommentRepo $commentRepo)
|
) {
|
||||||
{
|
|
||||||
$this->commentRepo = $commentRepo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -42,7 +41,13 @@ class CommentController extends Controller
|
|||||||
$this->checkPermission('comment-create-all');
|
$this->checkPermission('comment-create-all');
|
||||||
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
|
$comment = $this->commentRepo->create($page, $request->get('text'), $request->get('parent_id'));
|
||||||
|
|
||||||
return view('comments.comment', ['comment' => $comment]);
|
return view('comments.comment-branch', [
|
||||||
|
'readOnly' => false,
|
||||||
|
'branch' => [
|
||||||
|
'comment' => $comment,
|
||||||
|
'children' => [],
|
||||||
|
]
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -62,7 +67,7 @@ class CommentController extends Controller
|
|||||||
|
|
||||||
$comment = $this->commentRepo->update($comment, $request->get('text'));
|
$comment = $this->commentRepo->update($comment, $request->get('text'));
|
||||||
|
|
||||||
return view('comments.comment', ['comment' => $comment]);
|
return view('comments.comment', ['comment' => $comment, 'readOnly' => false]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
72
app/Activity/Controllers/FavouriteController.php
Normal file
72
app/Activity/Controllers/FavouriteController.php
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Entities\Queries\TopFavourites;
|
||||||
|
use BookStack\Entities\Tools\MixedEntityRequestHelper;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class FavouriteController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected MixedEntityRequestHelper $entityHelper,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show a listing of all favourite items for the current user.
|
||||||
|
*/
|
||||||
|
public function index(Request $request)
|
||||||
|
{
|
||||||
|
$viewCount = 20;
|
||||||
|
$page = intval($request->get('page', 1));
|
||||||
|
$favourites = (new TopFavourites())->run($viewCount + 1, (($page - 1) * $viewCount));
|
||||||
|
|
||||||
|
$hasMoreLink = ($favourites->count() > $viewCount) ? url('/favourites?page=' . ($page + 1)) : null;
|
||||||
|
|
||||||
|
$this->setPageTitle(trans('entities.my_favourites'));
|
||||||
|
|
||||||
|
return view('common.detailed-listing-with-more', [
|
||||||
|
'title' => trans('entities.my_favourites'),
|
||||||
|
'entities' => $favourites->slice(0, $viewCount),
|
||||||
|
'hasMoreLink' => $hasMoreLink,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a new item as a favourite.
|
||||||
|
*/
|
||||||
|
public function add(Request $request)
|
||||||
|
{
|
||||||
|
$modelInfo = $this->validate($request, $this->entityHelper->validationRules());
|
||||||
|
$entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
|
||||||
|
$entity->favourites()->firstOrCreate([
|
||||||
|
'user_id' => user()->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('activities.favourite_add_notification', [
|
||||||
|
'name' => $entity->name,
|
||||||
|
]));
|
||||||
|
|
||||||
|
return redirect($entity->getUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove an item as a favourite.
|
||||||
|
*/
|
||||||
|
public function remove(Request $request)
|
||||||
|
{
|
||||||
|
$modelInfo = $this->validate($request, $this->entityHelper->validationRules());
|
||||||
|
$entity = $this->entityHelper->getVisibleEntityFromRequestData($modelInfo);
|
||||||
|
$entity->favourites()->where([
|
||||||
|
'user_id' => user()->id,
|
||||||
|
])->delete();
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('activities.favourite_remove_notification', [
|
||||||
|
'name' => $entity->name,
|
||||||
|
]));
|
||||||
|
|
||||||
|
return redirect($entity->getUrl());
|
||||||
|
}
|
||||||
|
}
|
@ -1,18 +1,17 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\TagRepo;
|
use BookStack\Activity\TagRepo;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
class TagController extends Controller
|
class TagController extends Controller
|
||||||
{
|
{
|
||||||
protected TagRepo $tagRepo;
|
public function __construct(
|
||||||
|
protected TagRepo $tagRepo
|
||||||
public function __construct(TagRepo $tagRepo)
|
) {
|
||||||
{
|
|
||||||
$this->tagRepo = $tagRepo;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
29
app/Activity/Controllers/WatchController.php
Normal file
29
app/Activity/Controllers/WatchController.php
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Tools\UserEntityWatchOptions;
|
||||||
|
use BookStack\Entities\Tools\MixedEntityRequestHelper;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class WatchController extends Controller
|
||||||
|
{
|
||||||
|
public function update(Request $request, MixedEntityRequestHelper $entityHelper)
|
||||||
|
{
|
||||||
|
$this->checkPermission('receive-notifications');
|
||||||
|
$this->preventGuestAccess();
|
||||||
|
|
||||||
|
$requestData = $this->validate($request, array_merge([
|
||||||
|
'level' => ['required', 'string'],
|
||||||
|
], $entityHelper->validationRules()));
|
||||||
|
|
||||||
|
$watchable = $entityHelper->getVisibleEntityFromRequestData($requestData);
|
||||||
|
$watchOptions = new UserEntityWatchOptions(user(), $watchable);
|
||||||
|
$watchOptions->updateLevelByName($requestData['level']);
|
||||||
|
|
||||||
|
$this->showSuccessNotification(trans('activities.watch_update_level_notification'));
|
||||||
|
|
||||||
|
return redirect($watchable->getUrl());
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Http\Controllers;
|
namespace BookStack\Activity\Controllers;
|
||||||
|
|
||||||
use BookStack\Actions\ActivityType;
|
use BookStack\Activity\ActivityType;
|
||||||
use BookStack\Actions\Queries\WebhooksAllPaginatedAndSorted;
|
use BookStack\Activity\Models\Webhook;
|
||||||
use BookStack\Actions\Webhook;
|
use BookStack\Activity\Queries\WebhooksAllPaginatedAndSorted;
|
||||||
|
use BookStack\Http\Controller;
|
||||||
use BookStack\Util\SimpleListOptions;
|
use BookStack\Util\SimpleListOptions;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
|
84
app/Activity/DispatchWebhookJob.php
Normal file
84
app/Activity/DispatchWebhookJob.php
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Models\Webhook;
|
||||||
|
use BookStack\Activity\Tools\WebhookFormatter;
|
||||||
|
use BookStack\Facades\Theme;
|
||||||
|
use BookStack\Http\HttpRequestService;
|
||||||
|
use BookStack\Theming\ThemeEvents;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use BookStack\Util\SsrUrlValidator;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class DispatchWebhookJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable;
|
||||||
|
use InteractsWithQueue;
|
||||||
|
use Queueable;
|
||||||
|
use SerializesModels;
|
||||||
|
|
||||||
|
protected Webhook $webhook;
|
||||||
|
protected User $initiator;
|
||||||
|
protected int $initiatedTime;
|
||||||
|
protected array $webhookData;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new job instance.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function __construct(Webhook $webhook, string $event, Loggable|string $detail)
|
||||||
|
{
|
||||||
|
$this->webhook = $webhook;
|
||||||
|
$this->initiator = user();
|
||||||
|
$this->initiatedTime = time();
|
||||||
|
|
||||||
|
$themeResponse = Theme::dispatch(ThemeEvents::WEBHOOK_CALL_BEFORE, $event, $this->webhook, $detail, $this->initiator, $this->initiatedTime);
|
||||||
|
$this->webhookData = $themeResponse ?? WebhookFormatter::getDefault($event, $this->webhook, $detail, $this->initiator, $this->initiatedTime)->format();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle(HttpRequestService $http)
|
||||||
|
{
|
||||||
|
$lastError = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
(new SsrUrlValidator())->ensureAllowed($this->webhook->endpoint);
|
||||||
|
|
||||||
|
$client = $http->buildClient($this->webhook->timeout, [
|
||||||
|
'connect_timeout' => 10,
|
||||||
|
'allow_redirects' => ['strict' => true],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $client->sendRequest($http->jsonRequest('POST', $this->webhook->endpoint, $this->webhookData));
|
||||||
|
$statusCode = $response->getStatusCode();
|
||||||
|
|
||||||
|
if ($statusCode >= 400) {
|
||||||
|
$lastError = "Response status from endpoint was {$statusCode}";
|
||||||
|
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$statusCode}");
|
||||||
|
}
|
||||||
|
} catch (\Exception $error) {
|
||||||
|
$lastError = $error->getMessage();
|
||||||
|
Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with error \"{$lastError}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webhook->last_called_at = now();
|
||||||
|
if ($lastError) {
|
||||||
|
$this->webhook->last_errored_at = now();
|
||||||
|
$this->webhook->last_error = $lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->webhook->save();
|
||||||
|
}
|
||||||
|
}
|
@ -1,12 +1,15 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use BookStack\Auth\User;
|
use BookStack\App\Model;
|
||||||
use BookStack\Entities\Models\Entity;
|
use BookStack\Entities\Models\Entity;
|
||||||
use BookStack\Model;
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Support\Str;
|
use Illuminate\Support\Str;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -17,6 +20,8 @@ use Illuminate\Support\Str;
|
|||||||
* @property string $entity_type
|
* @property string $entity_type
|
||||||
* @property int $entity_id
|
* @property int $entity_id
|
||||||
* @property int $user_id
|
* @property int $user_id
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
*/
|
*/
|
||||||
class Activity extends Model
|
class Activity extends Model
|
||||||
{
|
{
|
||||||
@ -40,6 +45,12 @@ class Activity extends Model
|
|||||||
return $this->belongsTo(User::class);
|
return $this->belongsTo(User::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function jointPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
|
||||||
|
->whereColumn('activities.entity_type', '=', 'joint_permissions.entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns text from the language files, Looks up by using the activity key.
|
* Returns text from the language files, Looks up by using the activity key.
|
||||||
*/
|
*/
|
76
app/Activity/Models/Comment.php
Normal file
76
app/Activity/Models/Comment.php
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
|
use BookStack\App\Model;
|
||||||
|
use BookStack\Users\Models\HasCreatorAndUpdater;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property string $text
|
||||||
|
* @property string $html
|
||||||
|
* @property int|null $parent_id - Relates to local_id, not id
|
||||||
|
* @property int $local_id
|
||||||
|
* @property string $entity_type
|
||||||
|
* @property int $entity_id
|
||||||
|
* @property int $created_by
|
||||||
|
* @property int $updated_by
|
||||||
|
*/
|
||||||
|
class Comment extends Model implements Loggable
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
use HasCreatorAndUpdater;
|
||||||
|
|
||||||
|
protected $fillable = ['text', 'parent_id'];
|
||||||
|
protected $appends = ['created', 'updated'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the entity that this comment belongs to.
|
||||||
|
*/
|
||||||
|
public function entity(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo('entity');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the parent comment this is in reply to (if existing).
|
||||||
|
*/
|
||||||
|
public function parent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Comment::class, 'parent_id', 'local_id', 'parent')
|
||||||
|
->where('entity_type', '=', $this->entity_type)
|
||||||
|
->where('entity_id', '=', $this->entity_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a comment has been updated since creation.
|
||||||
|
*/
|
||||||
|
public function isUpdated(): bool
|
||||||
|
{
|
||||||
|
return $this->updated_at->timestamp > $this->created_at->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get created date as a relative diff.
|
||||||
|
*/
|
||||||
|
public function getCreatedAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->created_at->diffForHumans();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get updated date as a relative diff.
|
||||||
|
*/
|
||||||
|
public function getUpdatedAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->updated_at->diffForHumans();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function logDescriptor(): string
|
||||||
|
{
|
||||||
|
return "Comment #{$this->local_id} (ID: {$this->id}) for {$this->entity_type} (ID: {$this->entity_id})";
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Interfaces;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
27
app/Activity/Models/Favourite.php
Normal file
27
app/Activity/Models/Favourite.php
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
|
use BookStack\App\Model;
|
||||||
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
class Favourite extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = ['user_id'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the related model that can be favourited.
|
||||||
|
*/
|
||||||
|
public function favouritable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jointPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(JointPermission::class, 'entity_id', 'favouritable_id')
|
||||||
|
->whereColumn('favourites.favouritable_type', '=', 'joint_permissions.entity_type');
|
||||||
|
}
|
||||||
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Interfaces;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
interface Loggable
|
interface Loggable
|
||||||
{
|
{
|
@ -1,9 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use BookStack\Model;
|
use BookStack\App\Model;
|
||||||
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -27,6 +29,12 @@ class Tag extends Model
|
|||||||
return $this->morphTo('entity');
|
return $this->morphTo('entity');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function jointPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(JointPermission::class, 'entity_id', 'entity_id')
|
||||||
|
->whereColumn('tags.entity_type', '=', 'joint_permissions.entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a full URL to start a tag name search for this tag name.
|
* Get a full URL to start a tag name search for this tag name.
|
||||||
*/
|
*/
|
@ -1,9 +1,10 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use BookStack\Interfaces\Viewable;
|
use BookStack\App\Model;
|
||||||
use BookStack\Model;
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,13 +29,19 @@ class View extends Model
|
|||||||
return $this->morphTo();
|
return $this->morphTo();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function jointPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(JointPermission::class, 'entity_id', 'viewable_id')
|
||||||
|
->whereColumn('views.viewable_type', '=', 'joint_permissions.entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Increment the current user's view count for the given viewable model.
|
* Increment the current user's view count for the given viewable model.
|
||||||
*/
|
*/
|
||||||
public static function incrementFor(Viewable $viewable): int
|
public static function incrementFor(Viewable $viewable): int
|
||||||
{
|
{
|
||||||
$user = user();
|
$user = user();
|
||||||
if (is_null($user) || $user->isDefault()) {
|
if ($user->isGuest()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -47,12 +54,4 @@ class View extends Model
|
|||||||
|
|
||||||
return $view->views;
|
return $view->views;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear all views from the system.
|
|
||||||
*/
|
|
||||||
public static function clearAll()
|
|
||||||
{
|
|
||||||
static::query()->truncate();
|
|
||||||
}
|
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Interfaces;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||||
|
|
45
app/Activity/Models/Watch.php
Normal file
45
app/Activity/Models/Watch.php
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
|
use BookStack\Activity\WatchLevels;
|
||||||
|
use BookStack\Permissions\Models\JointPermission;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @property int $id
|
||||||
|
* @property int $user_id
|
||||||
|
* @property int $watchable_id
|
||||||
|
* @property string $watchable_type
|
||||||
|
* @property int $level
|
||||||
|
* @property Carbon $created_at
|
||||||
|
* @property Carbon $updated_at
|
||||||
|
*/
|
||||||
|
class Watch extends Model
|
||||||
|
{
|
||||||
|
protected $guarded = [];
|
||||||
|
|
||||||
|
public function watchable(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function jointPermissions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(JointPermission::class, 'entity_id', 'watchable_id')
|
||||||
|
->whereColumn('watches.watchable_type', '=', 'joint_permissions.entity_type');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLevelName(): string
|
||||||
|
{
|
||||||
|
return WatchLevels::levelValueToName($this->level);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ignoring(): bool
|
||||||
|
{
|
||||||
|
return $this->level === WatchLevels::IGNORE;
|
||||||
|
}
|
||||||
|
}
|
@ -1,8 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use BookStack\Interfaces\Loggable;
|
use BookStack\Activity\ActivityType;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
use Illuminate\Database\Eloquent\Collection;
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
@ -1,6 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
namespace BookStack\Actions;
|
namespace BookStack\Activity\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Handlers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\Messages\BaseActivityNotification;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
abstract class BaseNotificationHandler implements NotificationHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param class-string<BaseActivityNotification> $notification
|
||||||
|
* @param int[] $userIds
|
||||||
|
*/
|
||||||
|
protected function sendNotificationToUserIds(string $notification, array $userIds, User $initiator, string|Loggable $detail, Entity $relatedModel): void
|
||||||
|
{
|
||||||
|
$users = User::query()->whereIn('id', array_unique($userIds))->get();
|
||||||
|
|
||||||
|
foreach ($users as $user) {
|
||||||
|
// Prevent sending to the user that initiated the activity
|
||||||
|
if ($user->id === $initiator->id) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent sending of the user does not have notification permissions
|
||||||
|
if (!$user->can('receive-notifications')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent sending if the user does not have access to the related content
|
||||||
|
$permissions = new PermissionApplicator($user);
|
||||||
|
if (!$permissions->checkOwnableUserAccess($relatedModel, 'view')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the notification
|
||||||
|
$user->notify(new $notification($detail, $initiator));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Handlers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Activity\Models\Comment;
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\Messages\CommentCreationNotification;
|
||||||
|
use BookStack\Activity\Tools\EntityWatchers;
|
||||||
|
use BookStack\Activity\WatchLevels;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Settings\UserNotificationPreferences;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
class CommentCreationNotificationHandler extends BaseNotificationHandler
|
||||||
|
{
|
||||||
|
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||||
|
{
|
||||||
|
if (!($detail instanceof Comment)) {
|
||||||
|
throw new \InvalidArgumentException("Detail for comment creation notifications must be a comment");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main watchers
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = $detail->entity;
|
||||||
|
$watchers = new EntityWatchers($page, WatchLevels::COMMENTS);
|
||||||
|
$watcherIds = $watchers->getWatcherUserIds();
|
||||||
|
|
||||||
|
// Page owner if user preferences allow
|
||||||
|
if (!$watchers->isUserIgnoring($page->owned_by) && $page->ownedBy) {
|
||||||
|
$userNotificationPrefs = new UserNotificationPreferences($page->ownedBy);
|
||||||
|
if ($userNotificationPrefs->notifyOnOwnPageComments()) {
|
||||||
|
$watcherIds[] = $page->owned_by;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent comment creator if preferences allow
|
||||||
|
$parentComment = $detail->parent()->first();
|
||||||
|
if ($parentComment && !$watchers->isUserIgnoring($parentComment->created_by) && $parentComment->createdBy) {
|
||||||
|
$parentCommenterNotificationsPrefs = new UserNotificationPreferences($parentComment->createdBy);
|
||||||
|
if ($parentCommenterNotificationsPrefs->notifyOnCommentReplies()) {
|
||||||
|
$watcherIds[] = $parentComment->created_by;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sendNotificationToUserIds(CommentCreationNotification::class, $watcherIds, $user, $detail, $page);
|
||||||
|
}
|
||||||
|
}
|
17
app/Activity/Notifications/Handlers/NotificationHandler.php
Normal file
17
app/Activity/Notifications/Handlers/NotificationHandler.php
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Handlers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
interface NotificationHandler
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run this handler.
|
||||||
|
* Provides the activity, related activity detail/model
|
||||||
|
* along with the user that triggered the activity.
|
||||||
|
*/
|
||||||
|
public function handle(Activity $activity, string|Loggable $detail, User $user): void;
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Handlers;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\Messages\PageCreationNotification;
|
||||||
|
use BookStack\Activity\Tools\EntityWatchers;
|
||||||
|
use BookStack\Activity\WatchLevels;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
class PageCreationNotificationHandler extends BaseNotificationHandler
|
||||||
|
{
|
||||||
|
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||||
|
{
|
||||||
|
if (!($detail instanceof Page)) {
|
||||||
|
throw new \InvalidArgumentException("Detail for page create notifications must be a page");
|
||||||
|
}
|
||||||
|
|
||||||
|
$watchers = new EntityWatchers($detail, WatchLevels::NEW);
|
||||||
|
$this->sendNotificationToUserIds(PageCreationNotification::class, $watchers->getWatcherUserIds(), $user, $detail, $detail);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Handlers;
|
||||||
|
|
||||||
|
use BookStack\Activity\ActivityType;
|
||||||
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\Messages\PageUpdateNotification;
|
||||||
|
use BookStack\Activity\Tools\EntityWatchers;
|
||||||
|
use BookStack\Activity\WatchLevels;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Settings\UserNotificationPreferences;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
class PageUpdateNotificationHandler extends BaseNotificationHandler
|
||||||
|
{
|
||||||
|
public function handle(Activity $activity, Loggable|string $detail, User $user): void
|
||||||
|
{
|
||||||
|
if (!($detail instanceof Page)) {
|
||||||
|
throw new \InvalidArgumentException("Detail for page update notifications must be a page");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get last update from activity
|
||||||
|
$lastUpdate = $detail->activity()
|
||||||
|
->where('type', '=', ActivityType::PAGE_UPDATE)
|
||||||
|
->where('id', '!=', $activity->id)
|
||||||
|
->latest('created_at')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
// Return if the same user has already updated the page in the last 15 mins
|
||||||
|
if ($lastUpdate && $lastUpdate->user_id === $user->id) {
|
||||||
|
if ($lastUpdate->created_at->gt(now()->subMinutes(15))) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get active watchers
|
||||||
|
$watchers = new EntityWatchers($detail, WatchLevels::UPDATES);
|
||||||
|
$watcherIds = $watchers->getWatcherUserIds();
|
||||||
|
|
||||||
|
// Add page owner if preferences allow
|
||||||
|
if (!$watchers->isUserIgnoring($detail->owned_by) && $detail->ownedBy) {
|
||||||
|
$userNotificationPrefs = new UserNotificationPreferences($detail->ownedBy);
|
||||||
|
if ($userNotificationPrefs->notifyOnOwnPageChanges()) {
|
||||||
|
$watcherIds[] = $detail->owned_by;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->sendNotificationToUserIds(PageUpdateNotification::class, $watcherIds, $user, $detail, $detail);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\MessageParts;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link to a specific entity in the system, with the text showing its name.
|
||||||
|
*/
|
||||||
|
class EntityLinkMessageLine implements Htmlable, Stringable
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected Entity $entity,
|
||||||
|
protected int $nameLength = 120,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toHtml(): string
|
||||||
|
{
|
||||||
|
return '<a href="' . e($this->entity->getUrl()) . '">' . e($this->entity->getShortName($this->nameLength)) . '</a>';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return "{$this->entity->getShortName($this->nameLength)} ({$this->entity->getUrl()})";
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\MessageParts;
|
||||||
|
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A link to a specific entity in the system, with the text showing its name.
|
||||||
|
*/
|
||||||
|
class EntityPathMessageLine implements Htmlable, Stringable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var EntityLinkMessageLine[]
|
||||||
|
*/
|
||||||
|
protected array $entityLinks;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected array $entities
|
||||||
|
) {
|
||||||
|
$this->entityLinks = array_map(fn (Entity $entity) => new EntityLinkMessageLine($entity, 24), $this->entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toHtml(): string
|
||||||
|
{
|
||||||
|
$entityHtmls = array_map(fn (EntityLinkMessageLine $line) => $line->toHtml(), $this->entityLinks);
|
||||||
|
return implode(' > ', $entityHtmls);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
return implode(' > ', $this->entityLinks);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\MessageParts;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A line of text with linked text included, intended for use
|
||||||
|
* in MailMessages. The line should have a ':link' placeholder for
|
||||||
|
* where the link should be inserted within the line.
|
||||||
|
*/
|
||||||
|
class LinkedMailMessageLine implements Htmlable, Stringable
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected string $url,
|
||||||
|
protected string $line,
|
||||||
|
protected string $linkText,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toHtml(): string
|
||||||
|
{
|
||||||
|
$link = '<a href="' . e($this->url) . '">' . e($this->linkText) . '</a>';
|
||||||
|
return str_replace(':link', $link, e($this->line));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$link = "{$this->linkText} ({$this->url})";
|
||||||
|
return str_replace(':link', $link, $this->line);
|
||||||
|
}
|
||||||
|
}
|
36
app/Activity/Notifications/MessageParts/ListMessageLine.php
Normal file
36
app/Activity/Notifications/MessageParts/ListMessageLine.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\MessageParts;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\Support\Htmlable;
|
||||||
|
use Stringable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A bullet point list of content, where the keys of the given list array
|
||||||
|
* are bolded header elements, and the values follow.
|
||||||
|
*/
|
||||||
|
class ListMessageLine implements Htmlable, Stringable
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
protected array $list
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function toHtml(): string
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
foreach ($this->list as $header => $content) {
|
||||||
|
$list[] = '<strong>' . e($header) . '</strong> ' . e($content);
|
||||||
|
}
|
||||||
|
return implode("<br>\n", $list);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function __toString(): string
|
||||||
|
{
|
||||||
|
$list = [];
|
||||||
|
foreach ($this->list as $header => $content) {
|
||||||
|
$list[] = $header . ' ' . $content;
|
||||||
|
}
|
||||||
|
return implode("\n", $list);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Messages;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\EntityPathMessageLine;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\LinkedMailMessageLine;
|
||||||
|
use BookStack\App\MailNotification;
|
||||||
|
use BookStack\Entities\Models\Entity;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Permissions\PermissionApplicator;
|
||||||
|
use BookStack\Translation\LocaleDefinition;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
|
||||||
|
abstract class BaseActivityNotification extends MailNotification
|
||||||
|
{
|
||||||
|
use Queueable;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
protected Loggable|string $detail,
|
||||||
|
protected User $user,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the array representation of the notification.
|
||||||
|
*
|
||||||
|
* @param mixed $notifiable
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toArray($notifiable)
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'activity_detail' => $this->detail,
|
||||||
|
'activity_creator' => $this->user,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build the common reason footer line used in mail messages.
|
||||||
|
*/
|
||||||
|
protected function buildReasonFooterLine(LocaleDefinition $locale): LinkedMailMessageLine
|
||||||
|
{
|
||||||
|
return new LinkedMailMessageLine(
|
||||||
|
url('/preferences/notifications'),
|
||||||
|
$locale->trans('notifications.footer_reason'),
|
||||||
|
$locale->trans('notifications.footer_reason_link'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a line which provides the book > chapter path to a page.
|
||||||
|
* Takes into account visibility of these parent items.
|
||||||
|
* Returns null if no path items can be used.
|
||||||
|
*/
|
||||||
|
protected function buildPagePathLine(Page $page, User $notifiable): ?EntityPathMessageLine
|
||||||
|
{
|
||||||
|
$permissions = new PermissionApplicator($notifiable);
|
||||||
|
|
||||||
|
$path = array_filter([$page->book, $page->chapter], function (?Entity $entity) use ($permissions) {
|
||||||
|
return !is_null($entity) && $permissions->checkOwnableUserAccess($entity, 'view');
|
||||||
|
});
|
||||||
|
|
||||||
|
return empty($path) ? null : new EntityPathMessageLine($path);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Messages;
|
||||||
|
|
||||||
|
use BookStack\Activity\Models\Comment;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\EntityLinkMessageLine;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class CommentCreationNotification extends BaseActivityNotification
|
||||||
|
{
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
/** @var Comment $comment */
|
||||||
|
$comment = $this->detail;
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = $comment->entity;
|
||||||
|
|
||||||
|
$locale = $notifiable->getLocale();
|
||||||
|
|
||||||
|
$listLines = array_filter([
|
||||||
|
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||||
|
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||||
|
$locale->trans('notifications.detail_commenter') => $this->user->name,
|
||||||
|
$locale->trans('notifications.detail_comment') => strip_tags($comment->html),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->newMailMessage($locale)
|
||||||
|
->subject($locale->trans('notifications.new_comment_subject', ['pageName' => $page->getShortName()]))
|
||||||
|
->line($locale->trans('notifications.new_comment_intro', ['appName' => setting('app-name')]))
|
||||||
|
->line(new ListMessageLine($listLines))
|
||||||
|
->action($locale->trans('notifications.action_view_comment'), $page->getUrl('#comment' . $comment->local_id))
|
||||||
|
->line($this->buildReasonFooterLine($locale));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Messages;
|
||||||
|
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\EntityLinkMessageLine;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class PageCreationNotification extends BaseActivityNotification
|
||||||
|
{
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = $this->detail;
|
||||||
|
|
||||||
|
$locale = $notifiable->getLocale();
|
||||||
|
|
||||||
|
$listLines = array_filter([
|
||||||
|
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||||
|
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||||
|
$locale->trans('notifications.detail_created_by') => $this->user->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->newMailMessage($locale)
|
||||||
|
->subject($locale->trans('notifications.new_page_subject', ['pageName' => $page->getShortName()]))
|
||||||
|
->line($locale->trans('notifications.new_page_intro', ['appName' => setting('app-name')]))
|
||||||
|
->line(new ListMessageLine($listLines))
|
||||||
|
->action($locale->trans('notifications.action_view_page'), $page->getUrl())
|
||||||
|
->line($this->buildReasonFooterLine($locale));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications\Messages;
|
||||||
|
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\EntityLinkMessageLine;
|
||||||
|
use BookStack\Activity\Notifications\MessageParts\ListMessageLine;
|
||||||
|
use BookStack\Entities\Models\Page;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
use Illuminate\Notifications\Messages\MailMessage;
|
||||||
|
|
||||||
|
class PageUpdateNotification extends BaseActivityNotification
|
||||||
|
{
|
||||||
|
public function toMail(User $notifiable): MailMessage
|
||||||
|
{
|
||||||
|
/** @var Page $page */
|
||||||
|
$page = $this->detail;
|
||||||
|
|
||||||
|
$locale = $notifiable->getLocale();
|
||||||
|
|
||||||
|
$listLines = array_filter([
|
||||||
|
$locale->trans('notifications.detail_page_name') => new EntityLinkMessageLine($page),
|
||||||
|
$locale->trans('notifications.detail_page_path') => $this->buildPagePathLine($page, $notifiable),
|
||||||
|
$locale->trans('notifications.detail_updated_by') => $this->user->name,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->newMailMessage($locale)
|
||||||
|
->subject($locale->trans('notifications.updated_page_subject', ['pageName' => $page->getShortName()]))
|
||||||
|
->line($locale->trans('notifications.updated_page_intro', ['appName' => setting('app-name')]))
|
||||||
|
->line(new ListMessageLine($listLines))
|
||||||
|
->line($locale->trans('notifications.updated_page_debounce'))
|
||||||
|
->action($locale->trans('notifications.action_view_page'), $page->getUrl())
|
||||||
|
->line($this->buildReasonFooterLine($locale));
|
||||||
|
}
|
||||||
|
}
|
52
app/Activity/Notifications/NotificationManager.php
Normal file
52
app/Activity/Notifications/NotificationManager.php
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace BookStack\Activity\Notifications;
|
||||||
|
|
||||||
|
use BookStack\Activity\ActivityType;
|
||||||
|
use BookStack\Activity\Models\Activity;
|
||||||
|
use BookStack\Activity\Models\Loggable;
|
||||||
|
use BookStack\Activity\Notifications\Handlers\CommentCreationNotificationHandler;
|
||||||
|
use BookStack\Activity\Notifications\Handlers\NotificationHandler;
|
||||||
|
use BookStack\Activity\Notifications\Handlers\PageCreationNotificationHandler;
|
||||||
|
use BookStack\Activity\Notifications\Handlers\PageUpdateNotificationHandler;
|
||||||
|
use BookStack\Users\Models\User;
|
||||||
|
|
||||||
|
class NotificationManager
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var class-string<NotificationHandler>[]
|
||||||
|
*/
|
||||||
|
protected array $handlers = [];
|
||||||
|
|
||||||
|
public function handle(Activity $activity, string|Loggable $detail, User $user): void
|
||||||
|
{
|
||||||
|
$activityType = $activity->type;
|
||||||
|
$handlersToRun = $this->handlers[$activityType] ?? [];
|
||||||
|
foreach ($handlersToRun as $handlerClass) {
|
||||||
|
/** @var NotificationHandler $handler */
|
||||||
|
$handler = new $handlerClass();
|
||||||
|
$handler->handle($activity, $detail, $user);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param class-string<NotificationHandler> $handlerClass
|
||||||
|
*/
|
||||||
|
public function registerHandler(string $activityType, string $handlerClass): void
|
||||||
|
{
|
||||||
|
if (!isset($this->handlers[$activityType])) {
|
||||||
|
$this->handlers[$activityType] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!in_array($handlerClass, $this->handlers[$activityType])) {
|
||||||
|
$this->handlers[$activityType][] = $handlerClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadDefaultHandlers(): void
|
||||||
|
{
|
||||||
|
$this->registerHandler(ActivityType::PAGE_CREATE, PageCreationNotificationHandler::class);
|
||||||
|
$this->registerHandler(ActivityType::PAGE_UPDATE, PageUpdateNotificationHandler::class);
|
||||||
|
$this->registerHandler(ActivityType::COMMENT_CREATE, CommentCreationNotificationHandler::class);
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user