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-12-11 18:33:34 +02:00
"github.com/pocketbase/pocketbase/tools/inflector"
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"
2024-12-11 18:33:34 +02:00
"github.com/pocketbase/pocketbase/tools/security"
2022-07-07 00:19:05 +03:00
)
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-12-11 18:33:34 +02:00
dummyRecord := e . Record . Clone ( )
2024-09-29 19:23:19 +03:00
2024-12-11 18:33:34 +02:00
// set an id if it doesn't have already
// (the value doesn't matter; it is used only to minimize the breaking changes with earlier versions)
if dummyRecord . Id == "" {
dummyRecord . Id = "__pb_create__temp_id_" + security . PseudorandomString ( 5 )
2024-09-29 19:23:19 +03:00
}
2024-12-11 18:33:34 +02:00
// unset the verified field to prevent manage API rule misuse in case the rule relies on it
dummyRecord . SetVerified ( false )
2024-07-01 21:43:20 +03:00
2024-12-11 18:33:34 +02:00
// export the dummy record data into db params
dummyExport , err := dummyRecord . DBExport ( e . App )
if err != nil {
return e . BadRequestError ( "Failed to create record" , fmt . Errorf ( "dummy DBExport error: %w" , err ) )
}
2024-09-29 19:23:19 +03:00
2024-12-11 18:33:34 +02:00
dummyParams := make ( dbx . Params , len ( dummyExport ) )
selects := make ( [ ] string , 0 , len ( dummyExport ) )
var param string
for k , v := range dummyExport {
k = inflector . Columnify ( k ) // columnify is just as extra measure in case of custom fields
param = "__pb_create__" + k
dummyParams [ param ] = v
selects = append ( selects , "{:" + param + "} AS [[" + k + "]]" )
2024-09-29 19:23:19 +03:00
}
2024-12-11 18:33:34 +02:00
// shallow clone the current collection
dummyRandomPart := "__pb_create__" + security . PseudorandomString ( 5 )
dummyCollection := * e . Collection
dummyCollection . Id += dummyRandomPart
dummyCollection . Name += inflector . Columnify ( dummyRandomPart )
withFrom := fmt . Sprintf ( "WITH {{%s}} as (SELECT %s)" , dummyCollection . Name , strings . Join ( selects , "," ) )
// check non-empty create rule
if * dummyCollection . CreateRule != "" {
ruleQuery := e . App . DB ( ) . Select ( "(1)" ) . PreFragment ( withFrom ) . From ( dummyCollection . Name ) . AndBind ( dummyParams )
resolver := core . NewRecordFieldResolver ( e . App , & dummyCollection , requestInfo , true )
expr , err := search . FilterData ( * dummyCollection . CreateRule ) . BuildExpr ( resolver )
2024-09-29 19:23:19 +03:00
if err != nil {
2024-12-11 18:33:34 +02:00
return e . BadRequestError ( "Failed to create record" , fmt . Errorf ( "create rule build expression failure: %w" , err ) )
2024-09-29 19:23:19 +03:00
}
2024-12-11 18:33:34 +02:00
ruleQuery . AndWhere ( expr )
2024-09-29 19:23:19 +03:00
2024-12-11 18:33:34 +02:00
resolver . UpdateQuery ( ruleQuery )
2024-09-29 19:23:19 +03:00
2024-12-11 18:33:34 +02:00
var exists bool
err = ruleQuery . Limit ( 1 ) . Row ( & exists )
if err != nil || ! exists {
return e . BadRequestError ( "Failed to create record" , fmt . Errorf ( "create rule failure: %w" , err ) )
}
2024-09-29 19:23:19 +03:00
}
2024-12-11 18:33:34 +02:00
// check for manage rule access
manageRuleQuery := e . App . DB ( ) . Select ( "(1)" ) . PreFragment ( withFrom ) . From ( dummyCollection . Name ) . AndBind ( dummyParams )
if ! form . HasManageAccess ( ) &&
hasAuthManageAccess ( e . App , requestInfo , & dummyCollection , manageRuleQuery ) {
form . GrantManagerAccess ( )
2024-09-29 19:23:19 +03:00
}
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-12-11 18:33:34 +02:00
return firstApiError ( err , e . BadRequestError ( "Failed to create record" , err ) )
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 ) )
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 )
2024-12-11 18:33:34 +02:00
manageRuleQuery := e . App . DB ( ) . Select ( "(1)" ) . From ( e . Collection . Name ) . AndWhere ( dbx . HashExp {
e . Collection . Name + ".id" : e . Record . Id ,
} )
if ! form . HasManageAccess ( ) &&
hasAuthManageAccess ( e . App , requestInfo , e . Collection , manageRuleQuery ) {
2024-09-29 19:23:19 +03:00
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
}
2024-12-11 18:33:34 +02:00
// hasAuthManageAccess checks whether the client is allowed to have
// [forms.RecordUpsert] auth management permissions
// (e.g. allowing to change system auth fields without oldPassword).
func hasAuthManageAccess ( app core . App , requestInfo * core . RequestInfo , collection * core . Collection , query * dbx . SelectQuery ) bool {
if ! collection . IsAuth ( ) {
return false
}
manageRule := collection . ManageRule
if manageRule == nil || * manageRule == "" {
return false // only for superusers (manageRule can't be empty)
}
if requestInfo == nil || requestInfo . Auth == nil {
return false // no auth record
}
resolver := core . NewRecordFieldResolver ( app , collection , requestInfo , true )
expr , err := search . FilterData ( * manageRule ) . BuildExpr ( resolver )
if err != nil {
app . Logger ( ) . Error ( "Manage rule build expression error" , "error" , err , "collectionId" , collection . Id )
return false
}
query . AndWhere ( expr )
resolver . UpdateQuery ( query )
var exists bool
err = query . Limit ( 1 ) . Row ( & exists )
return err == nil && exists
}