2022-07-07 00:19:05 +03:00
package apis
import (
2024-11-11 14:24:38 +02:00
cryptoRand "crypto/rand"
2024-09-29 19:23:19 +03:00
"errors"
2022-07-07 00:19:05 +03:00
"fmt"
2024-11-11 14:24:38 +02:00
"math/big"
2022-07-07 00:19:05 +03:00
"net/http"
2024-09-29 19:23:19 +03:00
"strings"
2024-11-11 14:24:38 +02:00
"time"
2022-07-07 00:19:05 +03:00
2022-07-09 22:17:41 +08:00
"github.com/pocketbase/dbx"
2022-07-07 00:19:05 +03:00
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/forms"
2024-09-29 19:23:19 +03:00
"github.com/pocketbase/pocketbase/tools/filesystem"
2024-11-19 17:42:41 +02:00
"github.com/pocketbase/pocketbase/tools/list"
2024-09-29 19:23:19 +03:00
"github.com/pocketbase/pocketbase/tools/router"
2022-07-07 00:19:05 +03:00
"github.com/pocketbase/pocketbase/tools/search"
)
2022-10-30 10:28:14 +02:00
// bindRecordCrudApi registers the record crud api endpoints and
// the corresponding handlers.
2024-09-29 19:23:19 +03:00
//
// note: the rate limiter is "inlined" because some of the crud actions are also used in the batch APIs
func bindRecordCrudApi ( app core . App , rg * router . RouterGroup [ * core . RequestEvent ] ) {
subGroup := rg . Group ( "/collections/{collection}/records" ) . Unbind ( DefaultRateLimitMiddlewareId )
subGroup . GET ( "" , recordsList )
subGroup . GET ( "/{id}" , recordView )
subGroup . POST ( "" , recordCreate ( nil ) ) . Bind ( dynamicCollectionBodyLimit ( "" ) )
subGroup . PATCH ( "/{id}" , recordUpdate ( nil ) ) . Bind ( dynamicCollectionBodyLimit ( "" ) )
subGroup . DELETE ( "/{id}" , recordDelete ( nil ) )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
func recordsList ( e * core . RequestEvent ) error {
collection , err := e . App . FindCachedCollectionByNameOrId ( e . Request . PathValue ( "collection" ) )
if err != nil || collection == nil {
return e . NotFoundError ( "Missing collection context." , err )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
err = checkCollectionRateLimit ( e , collection , "list" )
if err != nil {
return err
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
requestInfo , err := e . RequestInfo ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "" , err ) )
}
2023-12-10 21:06:02 +02:00
2024-09-29 19:23:19 +03:00
if collection . ListRule == nil && ! requestInfo . HasSuperuserAuth ( ) {
return e . ForbiddenError ( "Only superusers can perform this action." , nil )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
// forbid users and guests to query special filter/sort fields
err = checkForSuperuserOnlyRuleFields ( requestInfo )
if err != nil {
return err
2022-11-17 14:17:10 +02:00
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
fieldsResolver := core . NewRecordFieldResolver (
e . App ,
2022-10-30 10:28:14 +02:00
collection ,
2023-07-17 23:13:39 +03:00
requestInfo ,
2024-09-29 19:23:19 +03:00
// hidden fields are searchable only by superusers
requestInfo . HasSuperuserAuth ( ) ,
2022-10-30 10:28:14 +02:00
)
2022-07-07 00:19:05 +03:00
searchProvider := search . NewProvider ( fieldsResolver ) .
2024-09-29 19:23:19 +03:00
Query ( e . App . RecordQuery ( collection ) )
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
if ! requestInfo . HasSuperuserAuth ( ) && collection . ListRule != nil {
2022-07-07 00:19:05 +03:00
searchProvider . AddFilter ( search . FilterData ( * collection . ListRule ) )
}
2024-09-29 19:23:19 +03:00
records := [ ] * core . Record { }
2023-02-21 16:38:12 +02:00
2024-09-29 19:23:19 +03:00
result , err := searchProvider . ParseAndExec ( e . Request . URL . Query ( ) . Encode ( ) , & records )
2022-07-07 00:19:05 +03:00
if err != nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . BadRequestError ( "" , err ) )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
event := new ( core . RecordsListRequestEvent )
event . RequestEvent = e
2023-01-27 22:19:08 +02:00
event . Collection = collection
event . Records = records
event . Result = result
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
return e . App . OnRecordsListRequest ( ) . Trigger ( event , func ( e * core . RecordsListRequestEvent ) error {
if err := EnrichRecords ( e . RequestEvent , e . Records ) ; err != nil {
return firstApiError ( err , e . InternalServerError ( "Failed to enrich records" , err ) )
2022-11-17 14:17:10 +02:00
}
2024-11-11 14:24:38 +02:00
// Add a randomized throttle in case of too many empty search filter attempts.
//
// This is just for extra precaution since security researches raised concern regarding the possibity of eventual
// timing attacks because the List API rule acts also as filter and executes in a single run with the client-side filters.
// This is by design and it is an accepted tradeoff between performance, usability and correctness.
//
// While technically the below doesn't fully guarantee protection against filter timing attacks, in practice combined with the network latency it makes them even less feasible.
// A properly configured rate limiter or individual fields Hidden checks are better suited if you are really concerned about eventual information disclosure by side-channel attacks.
//
// In all cases it doesn't really matter that much because it doesn't affect the builtin PocketBase security sensitive fields (e.g. password and tokenKey) since they
// are not client-side filterable and in the few places where they need to be compared against an external value, a constant time check is used.
if ! e . HasSuperuserAuth ( ) &&
( collection . ListRule != nil && * collection . ListRule != "" ) &&
( requestInfo . Query [ "filter" ] != "" ) &&
len ( e . Records ) == 0 &&
checkRateLimit ( e . RequestEvent , "@pb_list_timing_check_" + collection . Id , listTimingRateLimitRule ) != nil {
e . App . Logger ( ) . Debug ( "Randomized throttle because of too many failed searches" , "collectionId" , collection . Id )
randomizedThrottle ( 100 )
}
2024-09-29 19:23:19 +03:00
return e . JSON ( http . StatusOK , e . Result )
2022-07-07 00:19:05 +03:00
} )
}
2024-11-11 14:24:38 +02:00
var listTimingRateLimitRule = core . RateLimitRule { MaxRequests : 3 , Duration : 3 }
func randomizedThrottle ( softMax int64 ) {
var timeout int64
randRange , err := cryptoRand . Int ( cryptoRand . Reader , big . NewInt ( softMax ) )
if err == nil {
timeout = randRange . Int64 ( )
} else {
timeout = softMax
}
time . Sleep ( time . Duration ( timeout ) * time . Millisecond )
}
2024-09-29 19:23:19 +03:00
func recordView ( e * core . RequestEvent ) error {
collection , err := e . App . FindCachedCollectionByNameOrId ( e . Request . PathValue ( "collection" ) )
if err != nil || collection == nil {
return e . NotFoundError ( "Missing collection context." , err )
}
err = checkCollectionRateLimit ( e , collection , "view" )
if err != nil {
return err
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
recordId := e . Request . PathValue ( "id" )
2022-07-07 00:19:05 +03:00
if recordId == "" {
2024-09-29 19:23:19 +03:00
return e . NotFoundError ( "" , nil )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
requestInfo , err := e . RequestInfo ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "" , err ) )
}
2022-11-17 14:17:10 +02:00
2024-09-29 19:23:19 +03:00
if collection . ViewRule == nil && ! requestInfo . HasSuperuserAuth ( ) {
return e . ForbiddenError ( "Only superusers can perform this action." , nil )
2022-11-17 14:17:10 +02:00
}
2022-07-07 00:19:05 +03:00
ruleFunc := func ( q * dbx . SelectQuery ) error {
2024-09-29 19:23:19 +03:00
if ! requestInfo . HasSuperuserAuth ( ) && collection . ViewRule != nil && * collection . ViewRule != "" {
resolver := core . NewRecordFieldResolver ( e . App , collection , requestInfo , true )
2022-07-07 00:19:05 +03:00
expr , err := search . FilterData ( * collection . ViewRule ) . BuildExpr ( resolver )
if err != nil {
return err
}
resolver . UpdateQuery ( q )
q . AndWhere ( expr )
}
return nil
}
2024-09-29 19:23:19 +03:00
record , fetchErr := e . App . FindRecordById ( collection , recordId , ruleFunc )
2022-07-07 00:19:05 +03:00
if fetchErr != nil || record == nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . NotFoundError ( "" , fetchErr ) )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
event := new ( core . RecordRequestEvent )
event . RequestEvent = e
2023-01-27 22:19:08 +02:00
event . Collection = collection
event . Record = record
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
return e . App . OnRecordViewRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEvent ) error {
if err := EnrichRecord ( e . RequestEvent , e . Record ) ; err != nil {
return firstApiError ( err , e . InternalServerError ( "Failed to enrich record" , err ) )
2023-07-20 10:40:03 +03:00
}
2024-09-29 19:23:19 +03:00
return e . JSON ( http . StatusOK , e . Record )
2022-07-07 00:19:05 +03:00
} )
}
2024-10-24 08:37:22 +03:00
func recordCreate ( optFinalizer func ( data any ) error ) func ( e * core . RequestEvent ) error {
2024-09-29 19:23:19 +03:00
return func ( e * core . RequestEvent ) error {
collection , err := e . App . FindCachedCollectionByNameOrId ( e . Request . PathValue ( "collection" ) )
if err != nil || collection == nil {
return e . NotFoundError ( "Missing collection context." , err )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
if collection . IsView ( ) {
return e . BadRequestError ( "Unsupported collection type." , nil )
}
2022-11-17 14:17:10 +02:00
2024-09-29 19:23:19 +03:00
err = checkCollectionRateLimit ( e , collection , "create" )
if err != nil {
return err
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
requestInfo , err := e . RequestInfo ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "" , err ) )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
hasSuperuserAuth := requestInfo . HasSuperuserAuth ( )
2024-11-05 21:12:17 +02:00
if ! hasSuperuserAuth && collection . CreateRule == nil {
2024-09-29 19:23:19 +03:00
return e . ForbiddenError ( "Only superusers can perform this action." , nil )
2023-01-07 22:25:56 +02:00
}
2024-09-29 19:23:19 +03:00
record := core . NewRecord ( collection )
data , err := recordDataFromRequest ( e , record )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "Failed to read the submitted data." , err ) )
2023-01-07 22:25:56 +02:00
}
2024-09-29 19:23:19 +03:00
// replace modifiers fields so that the resolved value is always
// available when accessing requestInfo.Body
requestInfo . Body = data
form := forms . NewRecordUpsert ( e . App , record )
if hasSuperuserAuth {
form . GrantSuperuserAccess ( )
2024-07-01 21:43:20 +03:00
}
2024-09-29 19:23:19 +03:00
form . Load ( data )
var isOptFinalizerCalled bool
event := new ( core . RecordRequestEvent )
event . RequestEvent = e
event . Collection = collection
event . Record = record
hookErr := e . App . OnRecordCreateRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEvent ) error {
form . SetApp ( e . App )
form . SetRecord ( e . Record )
// temporary save the record and check it against the create and manage rules
2024-11-05 21:12:17 +02:00
if ! hasSuperuserAuth && e . Collection . CreateRule != nil {
2024-09-29 19:23:19 +03:00
// temporary grant manager access level
form . GrantManagerAccess ( )
// manually unset the verified field to prevent manage API rule misuse in case the rule relies on it
initialVerified := e . Record . Verified ( )
if initialVerified {
e . Record . SetVerified ( false )
}
createRuleFunc := func ( q * dbx . SelectQuery ) error {
if * e . Collection . CreateRule == "" {
return nil // no create rule to resolve
}
2024-07-01 21:43:20 +03:00
2024-09-29 19:23:19 +03:00
resolver := core . NewRecordFieldResolver ( e . App , e . Collection , requestInfo , true )
expr , err := search . FilterData ( * e . Collection . CreateRule ) . BuildExpr ( resolver )
if err != nil {
return err
}
resolver . UpdateQuery ( q )
q . AndWhere ( expr )
return nil
}
testErr := form . DrySubmit ( func ( txApp core . App , drySavedRecord * core . Record ) error {
foundRecord , err := txApp . FindRecordById ( drySavedRecord . Collection ( ) , drySavedRecord . Id , createRuleFunc )
if err != nil {
return fmt . Errorf ( "DrySubmit create rule failure: %w" , err )
}
// reset the form access level in case it satisfies the Manage API rule
if ! hasAuthManageAccess ( txApp , requestInfo , foundRecord ) {
form . ResetAccess ( )
}
return nil
} )
if testErr != nil {
return e . BadRequestError ( "Failed to create record." , testErr )
}
// restore initial verified state (it will be further validated on submit)
if initialVerified != e . Record . Verified ( ) {
e . Record . SetVerified ( initialVerified )
}
2022-10-30 10:28:14 +02:00
}
2024-09-29 19:23:19 +03:00
err := form . Submit ( )
2022-07-07 00:19:05 +03:00
if err != nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . BadRequestError ( "Failed to create record." , err ) )
}
err = EnrichRecord ( e . RequestEvent , e . Record )
if err != nil {
return firstApiError ( err , e . InternalServerError ( "Failed to enrich record" , err ) )
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
err = e . JSON ( http . StatusOK , e . Record )
2022-10-30 10:28:14 +02:00
if err != nil {
2024-09-29 19:23:19 +03:00
return err
2022-10-30 10:28:14 +02:00
}
2024-09-29 19:23:19 +03:00
if optFinalizer != nil {
isOptFinalizerCalled = true
2024-10-24 08:37:22 +03:00
err = optFinalizer ( e . Record )
2024-09-29 19:23:19 +03:00
if err != nil {
return firstApiError ( err , e . InternalServerError ( "" , err ) )
}
}
2022-10-30 10:28:14 +02:00
return nil
2022-07-07 00:19:05 +03:00
} )
2024-09-29 19:23:19 +03:00
if hookErr != nil {
return hookErr
}
2022-10-30 10:28:14 +02:00
2024-09-29 19:23:19 +03:00
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if ! isOptFinalizerCalled && optFinalizer != nil {
2024-10-24 08:37:22 +03:00
if err := optFinalizer ( event . Record ) ; err != nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . InternalServerError ( "" , err ) )
}
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
return nil
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
}
2022-07-07 00:19:05 +03:00
2024-10-24 08:37:22 +03:00
func recordUpdate ( optFinalizer func ( data any ) error ) func ( e * core . RequestEvent ) error {
2024-09-29 19:23:19 +03:00
return func ( e * core . RequestEvent ) error {
collection , err := e . App . FindCachedCollectionByNameOrId ( e . Request . PathValue ( "collection" ) )
if err != nil || collection == nil {
return e . NotFoundError ( "Missing collection context." , err )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
if collection . IsView ( ) {
return e . BadRequestError ( "Unsupported collection type." , nil )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
err = checkCollectionRateLimit ( e , collection , "update" )
if err != nil {
return err
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
recordId := e . Request . PathValue ( "id" )
if recordId == "" {
return e . NotFoundError ( "" , nil )
}
2023-01-15 17:00:28 +02:00
2024-09-29 19:23:19 +03:00
requestInfo , err := e . RequestInfo ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "" , err ) )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
hasSuperuserAuth := requestInfo . HasSuperuserAuth ( )
if ! hasSuperuserAuth && collection . UpdateRule == nil {
return firstApiError ( err , e . ForbiddenError ( "Only superusers can perform this action." , nil ) )
}
// eager fetch the record so that the modifiers field values can be resolved
record , err := e . App . FindRecordById ( collection , recordId )
if err != nil {
return firstApiError ( err , e . NotFoundError ( "" , err ) )
}
data , err := recordDataFromRequest ( e , record )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "Failed to read the submitted data." , err ) )
}
// replace modifiers fields so that the resolved value is always
// available when accessing requestInfo.Body
requestInfo . Body = data
ruleFunc := func ( q * dbx . SelectQuery ) error {
if ! hasSuperuserAuth && collection . UpdateRule != nil && * collection . UpdateRule != "" {
resolver := core . NewRecordFieldResolver ( e . App , collection , requestInfo , true )
expr , err := search . FilterData ( * collection . UpdateRule ) . BuildExpr ( resolver )
if err != nil {
return err
2022-10-30 10:28:14 +02:00
}
2024-09-29 19:23:19 +03:00
resolver . UpdateQuery ( q )
q . AndWhere ( expr )
}
return nil
}
2022-10-30 10:28:14 +02:00
2024-09-29 19:23:19 +03:00
// refetch with access checks
record , err = e . App . FindRecordById ( collection , recordId , ruleFunc )
if err != nil {
return firstApiError ( err , e . NotFoundError ( "" , err ) )
}
2023-07-20 10:40:03 +03:00
2024-09-29 19:23:19 +03:00
form := forms . NewRecordUpsert ( e . App , record )
if hasSuperuserAuth {
form . GrantSuperuserAccess ( )
2022-07-12 13:42:06 +03:00
}
2024-09-29 19:23:19 +03:00
form . Load ( data )
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
var isOptFinalizerCalled bool
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
event := new ( core . RecordRequestEvent )
event . RequestEvent = e
event . Collection = collection
event . Record = record
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
hookErr := e . App . OnRecordUpdateRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEvent ) error {
form . SetApp ( e . App )
form . SetRecord ( e . Record )
if ! form . HasManageAccess ( ) && hasAuthManageAccess ( e . App , requestInfo , e . Record ) {
form . GrantManagerAccess ( )
}
2022-11-17 14:17:10 +02:00
2024-09-29 19:23:19 +03:00
err := form . Submit ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "Failed to update record." , err ) )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
err = EnrichRecord ( e . RequestEvent , e . Record )
if err != nil {
return firstApiError ( err , e . InternalServerError ( "Failed to enrich record" , err ) )
}
2023-01-07 22:25:56 +02:00
2024-09-29 19:23:19 +03:00
err = e . JSON ( http . StatusOK , e . Record )
2022-07-07 00:19:05 +03:00
if err != nil {
return err
}
2024-09-29 19:23:19 +03:00
if optFinalizer != nil {
isOptFinalizerCalled = true
2024-10-24 08:37:22 +03:00
err = optFinalizer ( e . Record )
2024-09-29 19:23:19 +03:00
if err != nil {
return firstApiError ( err , e . InternalServerError ( "" , fmt . Errorf ( "update optFinalizer error: %w" , err ) ) )
}
}
return nil
} )
if hookErr != nil {
return hookErr
}
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if ! isOptFinalizerCalled && optFinalizer != nil {
2024-10-24 08:37:22 +03:00
if err := optFinalizer ( event . Record ) ; err != nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . InternalServerError ( "" , fmt . Errorf ( "update optFinalizer error: %w" , err ) ) )
}
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
2022-07-07 00:19:05 +03:00
return nil
}
2024-09-29 19:23:19 +03:00
}
2022-07-07 00:19:05 +03:00
2024-10-24 08:37:22 +03:00
func recordDelete ( optFinalizer func ( data any ) error ) func ( e * core . RequestEvent ) error {
2024-09-29 19:23:19 +03:00
return func ( e * core . RequestEvent ) error {
collection , err := e . App . FindCachedCollectionByNameOrId ( e . Request . PathValue ( "collection" ) )
if err != nil || collection == nil {
return e . NotFoundError ( "Missing collection context." , err )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
if collection . IsView ( ) {
return e . BadRequestError ( "Unsupported collection type." , nil )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
err = checkCollectionRateLimit ( e , collection , "delete" )
if err != nil {
return err
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
recordId := e . Request . PathValue ( "id" )
if recordId == "" {
return e . NotFoundError ( "" , nil )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
requestInfo , err := e . RequestInfo ( )
if err != nil {
return firstApiError ( err , e . BadRequestError ( "" , err ) )
}
if ! requestInfo . HasSuperuserAuth ( ) && collection . DeleteRule == nil {
return e . ForbiddenError ( "Only superusers can perform this action." , nil )
}
2023-01-15 17:00:28 +02:00
2024-09-29 19:23:19 +03:00
ruleFunc := func ( q * dbx . SelectQuery ) error {
if ! requestInfo . HasSuperuserAuth ( ) && collection . DeleteRule != nil && * collection . DeleteRule != "" {
resolver := core . NewRecordFieldResolver ( e . App , collection , requestInfo , true )
expr , err := search . FilterData ( * collection . DeleteRule ) . BuildExpr ( resolver )
if err != nil {
return err
2022-07-12 13:42:06 +03:00
}
2024-09-29 19:23:19 +03:00
resolver . UpdateQuery ( q )
q . AndWhere ( expr )
}
return nil
}
record , err := e . App . FindRecordById ( collection , recordId , ruleFunc )
if err != nil || record == nil {
return e . NotFoundError ( "" , err )
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
var isOptFinalizerCalled bool
event := new ( core . RecordRequestEvent )
event . RequestEvent = e
event . Collection = collection
event . Record = record
hookErr := e . App . OnRecordDeleteRequest ( ) . Trigger ( event , func ( e * core . RecordRequestEvent ) error {
if err := e . App . Delete ( e . Record ) ; err != nil {
return firstApiError ( err , e . BadRequestError ( "Failed to delete record. Make sure that the record is not part of a required relation reference." , err ) )
}
err = e . NoContent ( http . StatusNoContent )
if err != nil {
return err
}
if optFinalizer != nil {
isOptFinalizerCalled = true
2024-10-24 08:37:22 +03:00
err = optFinalizer ( e . Record )
2024-09-29 19:23:19 +03:00
if err != nil {
return firstApiError ( err , e . InternalServerError ( "" , fmt . Errorf ( "delete optFinalizer error: %w" , err ) ) )
2022-10-30 10:28:14 +02:00
}
2024-09-29 19:23:19 +03:00
}
2022-10-30 10:28:14 +02:00
2024-09-29 19:23:19 +03:00
return nil
} )
if hookErr != nil {
return hookErr
}
2023-07-20 10:40:03 +03:00
2024-09-29 19:23:19 +03:00
// e.g. in case the regular hook chain was stopped and the finalizer cannot be executed as part of the last e.Next() task
if ! isOptFinalizerCalled && optFinalizer != nil {
2024-10-24 08:37:22 +03:00
if err := optFinalizer ( event . Record ) ; err != nil {
2024-09-29 19:23:19 +03:00
return firstApiError ( err , e . InternalServerError ( "" , fmt . Errorf ( "delete optFinalizer error: %w" , err ) ) )
}
2022-07-12 13:42:06 +03:00
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
return nil
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
// -------------------------------------------------------------------
func recordDataFromRequest ( e * core . RequestEvent , record * core . Record ) ( map [ string ] any , error ) {
info , err := e . RequestInfo ( )
if err != nil {
return nil , err
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
// resolve regular fields
result := record . ReplaceModifiers ( info . Body )
2022-11-17 14:17:10 +02:00
2024-09-29 19:23:19 +03:00
// resolve uploaded files
2024-09-30 16:27:59 +03:00
uploadedFiles , err := extractUploadedFiles ( e , record . Collection ( ) , "" )
2024-09-29 19:23:19 +03:00
if err != nil {
return nil , err
}
if len ( uploadedFiles ) > 0 {
2024-11-19 17:42:41 +02:00
for k , files := range uploadedFiles {
uploaded := make ( [ ] any , 0 , len ( files ) )
// if not remove/prepend/append -> merge with the submitted
// info.Body values to prevent accidental old files deletion
if info . Body [ k ] != nil &&
! strings . HasPrefix ( k , "+" ) &&
! strings . HasSuffix ( k , "+" ) &&
! strings . HasSuffix ( k , "-" ) {
existing := list . ToUniqueStringSlice ( info . Body [ k ] )
for _ , name := range existing {
uploaded = append ( uploaded , name )
}
}
for _ , file := range files {
uploaded = append ( uploaded , file )
}
result [ k ] = uploaded
2024-09-29 19:23:19 +03:00
}
2024-11-19 17:42:41 +02:00
2024-09-29 19:23:19 +03:00
result = record . ReplaceModifiers ( result )
2022-11-17 14:17:10 +02:00
}
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
isAuth := record . Collection ( ) . IsAuth ( )
// unset hidden fields for non-superusers
if ! info . HasSuperuserAuth ( ) {
for _ , f := range record . Collection ( ) . Fields {
if f . GetHidden ( ) {
// exception for the auth collection "password" field
if isAuth && f . GetName ( ) == core . FieldNamePassword {
continue
}
delete ( result , f . GetName ( ) )
2022-07-07 00:19:05 +03:00
}
}
}
2024-09-29 19:23:19 +03:00
return result , nil
}
2024-09-30 16:27:59 +03:00
func extractUploadedFiles ( re * core . RequestEvent , collection * core . Collection , prefix string ) ( map [ string ] [ ] * filesystem . File , error ) {
contentType := re . Request . Header . Get ( "content-type" )
2024-09-29 19:23:19 +03:00
if ! strings . HasPrefix ( contentType , "multipart/form-data" ) {
return nil , nil // not multipart/form-data request
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
result := map [ string ] [ ] * filesystem . File { }
for _ , field := range collection . Fields {
if field . Type ( ) != core . FieldTypeFile {
continue
}
baseKey := field . GetName ( )
2022-07-07 00:19:05 +03:00
2024-09-29 19:23:19 +03:00
keys := [ ] string {
baseKey ,
// prepend and append modifiers
"+" + baseKey ,
baseKey + "+" ,
2022-07-07 00:19:05 +03:00
}
2024-09-29 19:23:19 +03:00
for _ , k := range keys {
if prefix != "" {
k = prefix + "." + k
2023-07-20 10:40:03 +03:00
}
2024-09-30 16:27:59 +03:00
files , err := re . FindUploadedFiles ( k )
2024-09-29 19:23:19 +03:00
if err != nil && ! errors . Is ( err , http . ErrMissingFile ) {
return nil , err
}
if len ( files ) > 0 {
result [ k ] = files
}
}
}
2023-07-20 10:40:03 +03:00
2024-09-29 19:23:19 +03:00
return result , nil
2022-07-07 00:19:05 +03:00
}