2023-11-29 14:47:27 +01:00
< ? php
/**
* EGroupware Timesheet : REST API
*
* @ link https :// www . egroupware . org
* @ package mail
* @ author Ralf Becker < rb @ egroupware . org >
* @ license http :// opensource . org / licenses / gpl - license . php GPL - GNU General Public License
*/
namespace EGroupware\Timesheet ;
use EGroupware\Api ;
/**
* REST API for Timesheet
*/
class ApiHandler extends Api\CalDAV\Handler
{
/**
* @ var \timesheet_bo
*/
protected \timesheet_bo $bo ;
/**
* Extension to append to url / path
*
* @ var string
*/
static $path_extension = '' ;
/**
* Constructor
*
* @ param string $app 'calendar' , 'addressbook' or 'infolog'
* @ param Api\CalDAV $caldav calling class
*/
function __construct ( $app , Api\CalDAV $caldav )
{
2024-02-01 21:16:36 +01:00
parent :: __construct ( 'timesheet' , $caldav );
2023-11-29 14:47:27 +01:00
self :: $path_extension = '' ;
$this -> bo = new \timesheet_bo ();
}
/**
* Options for json_encode of responses
*/
const JSON_RESPONSE_OPTIONS = JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR ;
/**
* Handle post request for mail ( send or compose mail and upload attachments )
*
* @ param array & $options
* @ param int $id
* @ param int $user = null account_id of owner , default null
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function post ( & $options , $id , $user = null )
{
if ( $this -> debug ) error_log ( __METHOD__ . " ( $id , $user ) " . print_r ( $options , true ));
$path = $options [ 'path' ];
if ( empty ( $user ))
{
$user = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
header ( 'Content-Type: application/json' );
try {
2024-02-01 21:16:36 +01:00
throw new \Exception ( 'Not Implemented' , 501 );
2023-11-29 14:47:27 +01:00
}
catch ( \Throwable $e ) {
return self :: handleException ( $e );
}
}
/**
* Handle propfind in the timesheet folder / get request on the collection itself
*
* @ param string $path
* @ param array & $options
* @ param array & $files
* @ param int $user account_id
* @ param string $id = ''
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function propfind ( $path , & $options , & $files , $user , $id = '' )
{
$filter = [
'ts_owner' => $user ? : null ,
];
// process REPORT filters or multiget href's
$nresults = null ;
if (( $id || $options [ 'root' ][ 'name' ] != 'propfind' ) && ! $this -> _report_filters ( $options , $filter , $id , $nresults ))
{
return false ;
}
if ( $id ) $path = dirname ( $path ) . '/' ; // carddav_name get's added anyway in the callback
if ( $this -> debug ) error_log ( __METHOD__ . " ( $path , " . array2string ( $options ) . " ,, $user , $id ) filter= " . array2string ( $filter ));
// rfc 6578 sync-collection report: filter for sync-token is already set in _report_filters
if ( $options [ 'root' ][ 'name' ] == 'sync-collection' )
{
// callback to query sync-token, after propfind_callbacks / iterator is run and
// stored max. modification-time in $this->sync_collection_token
$files [ 'sync-token' ] = array ( $this , 'get_sync_collection_token' );
$files [ 'sync-token-params' ] = array ( $path , $user );
$this -> sync_collection_token = null ;
$filter [ 'order' ] = 'ts_modified ASC' ; // return oldest modifications first
$filter [ 'sync-collection' ] = true ;
}
if ( isset ( $nresults ))
{
$files [ 'files' ] = $this -> propfind_generator ( $path , $filter , $files [ 'files' ], ( int ) $nresults );
// hack to support limit with sync-collection report: contacts are returned in modified ASC order (oldest first)
// if limit is smaller than full result, return modified-1 as sync-token, so client requests next chunk incl. modified
// (which might contain further entries with identical modification time)
if ( $options [ 'root' ][ 'name' ] == 'sync-collection' && $this -> bo -> total > $nresults )
{
-- $this -> sync_collection_token ;
$files [ 'sync-token-params' ][] = true ; // tell get_sync_collection_token that we have more entries
}
}
else
{
// return iterator, calling ourselves to return result in chunks
$files [ 'files' ] = $this -> propfind_generator ( $path , $filter , $files [ 'files' ]);
}
return true ;
}
/**
* Chunk - size for DB queries of profind_generator
*/
const CHUNK_SIZE = 500 ;
/**
* Generator for propfind with ability to skip reporting not found ids
*
* @ param string $path
* @ param array & $filter
* @ param array $extra extra resources like the collection itself
* @ param int | null $nresults option limit of number of results to report
* @ param boolean $report_not_found_multiget_ids = true
* @ return Generator < array with values for keys path and props >
*/
function propfind_generator ( $path , array & $filter , array $extra = [], $nresults = null , $report_not_found_multiget_ids = true )
{
//error_log(__METHOD__."('$path', ".array2string($filter).", ".array2string($start).", $report_not_found_multiget_ids)");
$starttime = microtime ( true );
$filter_in = $filter ;
// yield extra resources like the root itself
$yielded = 0 ;
foreach ( $extra as $resource )
{
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
return ;
}
yield $resource ;
}
if ( isset ( $filter [ 'order' ]))
{
$order = $filter [ 'order' ];
unset ( $filter [ 'order' ]);
}
else
{
$order = 'egw_timesheet.ts_id' ;
}
// detect sync-collection report
$sync_collection_report = $filter [ 'sync-collection' ];
unset ( $filter [ 'sync-collection' ]);
// stop output buffering switched on to log the response, if we should return more than 200 entries
if ( ! empty ( $this -> requested_multiget_ids ) && ob_get_level () && count ( $this -> requested_multiget_ids ) > 200 )
{
$this -> caldav -> log ( " ### " . count ( $this -> requested_multiget_ids ) . " resources requested in multiget REPORT --> turning logging off to allow streaming of the response " );
ob_end_flush ();
}
$search = $filter [ 'search' ] ? ? [];
unset ( $filter [ 'search' ]);
for ( $chunk = 0 ; ( $timesheets =& $this -> bo -> search ( $search , '*' , $order , '' , '' , False , 'AND' ,
[ $chunk * self :: CHUNK_SIZE , self :: CHUNK_SIZE ], $filter )); ++ $chunk )
{
// read custom-fields
if ( $this -> bo -> customfields )
{
$id2keys = array ();
foreach ( $timesheets as $key => & $timesheet )
{
$id2keys [ $timesheet [ 'ts_id' ]] = $key ;
}
if (( $cfs = $this -> bo -> read_customfields ( array_keys ( $id2keys ))))
{
foreach ( $cfs as $id => $data )
{
$timesheets [ $id2keys [ $id ]] += $data ;
}
}
}
foreach ( $timesheets as & $timesheet )
{
$content = JsTimesheet :: JsTimesheet ( $timesheet , false );
$timesheet = Api\Db :: strip_array_keys ( $timesheet , 'ts_' );
// remove contact from requested multiget ids, to be able to report not found urls
if ( ! empty ( $this -> requested_multiget_ids ) && ( $k = array_search ( $timesheet [ self :: $path_attr ], $this -> requested_multiget_ids )) !== false )
{
unset ( $this -> requested_multiget_ids [ $k ]);
}
// sync-collection report: deleted entry need to be reported without properties
if ( $timesheet [ 'ts_status' ] == \timesheet_bo :: DELETED_STATUS )
{
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
return ;
}
yield [ 'path' => $path . urldecode ( $this -> get_path ( $timesheet ))];
continue ;
}
$props = array (
'getcontenttype' => Api\CalDAV :: mkprop ( 'getcontenttype' , 'application/json' ),
'getlastmodified' => Api\DateTime :: user2server ( $timesheet [ 'modified' ]),
'displayname' => $timesheet [ 'title' ],
);
if ( true )
{
$props [ 'getcontentlength' ] = bytes ( is_array ( $content ) ? json_encode ( $content ) : $content );
$props [ 'data' ] = Api\CalDAV :: mkprop ( Api\CalDAV :: CARDDAV , 'data' , $content );
}
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
return ;
}
yield $this -> add_resource ( $path , $timesheet , $props );
}
// sync-collection report --> return modified of last contact as sync-token
if ( $sync_collection_report )
{
$this -> sync_collection_token = $timesheet [ 'modified' ];
}
}
// report not found multiget urls
if ( $report_not_found_multiget_ids && ! empty ( $this -> requested_multiget_ids ))
{
foreach ( $this -> requested_multiget_ids as $id )
{
if ( ++ $yielded && isset ( $nresults ) && $yielded > $nresults )
{
return ;
}
yield [ 'path' => $path . $id . self :: $path_extension ];
}
}
if ( $this -> debug )
{
error_log ( __METHOD__ . " ( $path , filter= " . json_encode ( $filter ) . ', extra=' . json_encode ( $extra ) .
" , nresults= $nresults , report_not_found= $report_not_found_multiget_ids ) took " .
( microtime ( true ) - $starttime ) . " to return $yielded resources " );
}
}
2024-02-01 21:16:36 +01:00
/**
* Process filter GET parameter :
* - filter [ < json - attribute - name > ] =< value >
* - filter [ % 23 < custom - field - name ] =< value >
* - filter [ search ] =< pattern > with string pattern like for search in the UI
* - filter [ search ][ % 23 < custom - field - name ] =< value >
* - filter [ search ][ < db - column > ] =< value >
*
* @ param array $filter
* @ return array
*/
protected function filter2col_filter ( array $filter )
{
$cols = [];
foreach ( $filter as $name => $value )
{
switch ( $name )
{
case 'search' :
$cols = array_merge ( $cols , $this -> bo -> search2criteria ( $value ));
break ;
case 'category' :
case 'pricelist' :
$cols [ $name === 'pricelist' ? 'pl_id' : 'cat_id' ] = $value ;
break ;
case 'status' :
$value = array_map ( function ( $val ) use ( $value )
{
if ( ! is_numeric ( $val ) || ( string )( int ) $val !== $val )
{
$val = array_search ( $val , $this -> bo -> status_labels , true );
}
elseif ( isset ( $this -> status_labels [ $val ]))
{
$val = ( int ) $val ;
}
else
{
$val = false ;
}
if ( $val === false )
{
throw new Api\CalDAV\JsParseException ( " Invalid status filter value " . json_encode ( $value ));
}
return ( int ) $val ;
}, ( array ) $value );
$cols [ 'ts_status' ] = count ( $value ) <= 1 ? array_pop ( $value ) : $value ;
break ;
default :
if ( $name [ 0 ] === '#' )
{
$cols [ $name ] = $value ;
}
else
{
$cols [ 'ts_' . $name ] = $value ;
}
break ;
}
}
return $cols ;
}
2023-11-29 14:47:27 +01:00
/**
* Process the filters from the CalDAV REPORT request
*
* @ param array $options
* @ param array & $filters
* @ param string $id
* @ param int & $nresult on return limit for number or results or unchanged / null
* @ return boolean true if filter could be processed
*/
function _report_filters ( $options , & $filters , $id , & $nresults )
{
// in case of JSON/REST API pass filters to report
if ( Api\CalDAV :: isJSON () && ! empty ( $options [ 'filters' ]) && is_array ( $options [ 'filters' ]))
{
2024-02-06 11:52:02 +01:00
$filters = $this -> filter2col_filter ( $options [ 'filters' ]) + $filters ; // + to allow overwriting default owner filter (BO ensures ACL!)
2023-11-29 14:47:27 +01:00
}
elseif ( ! empty ( $options [ 'filters' ]))
{
/* Example of a complex filter used by Mac Addressbook
< B : filter test = " anyof " >
< B : prop - filter name = " FN " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
< B : prop - filter name = " EMAIL " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
< B : prop - filter name = " NICKNAME " test = " allof " >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > becker </ B : text - match >
< B : text - match collation = " i;unicode-casemap " match - type = " contains " > ralf </ B : text - match >
</ B : prop - filter >
</ B : filter >
*/
$filter_test = isset ( $options [ 'filters' ][ 'attrs' ]) && isset ( $options [ 'filters' ][ 'attrs' ][ 'test' ]) ?
$options [ 'filters' ][ 'attrs' ][ 'test' ] : 'anyof' ;
$prop_filters = array ();
$matches = $prop_test = $column = null ;
foreach ( $options [ 'filters' ] as $n => $filter )
{
if ( ! is_int ( $n )) continue ; // eg. attributes of filter xml element
switch (( string ) $filter [ 'name' ])
{
case 'param-filter' :
$this -> caldav -> log ( __METHOD__ . " (...) param-filter=' { $filter [ 'attrs' ][ 'name' ] } ' not (yet) implemented! " );
break ;
case 'prop-filter' : // can be multiple prop-filter, see example
if ( $matches ) $prop_filters [] = implode ( $prop_test == 'allof' ? ' AND ' : ' OR ' , $matches );
$matches = array ();
$prop_filter = strtoupper ( $filter [ 'attrs' ][ 'name' ]);
$prop_test = isset ( $filter [ 'attrs' ][ 'test' ]) ? $filter [ 'attrs' ][ 'test' ] : 'anyof' ;
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (...) prop-filter=' $prop_filter ', test=' $prop_test ' " );
break ;
case 'is-not-defined' :
$matches [] = '(' . $column . " ='' OR " . $column . ' IS NULL)' ;
break ;
case 'text-match' : // prop-filter can have multiple text-match, see example
if ( ! isset ( $this -> filter_prop2cal [ $prop_filter ])) // eg. not existing NICKNAME in EGroupware
{
if ( $this -> debug || $prop_filter != 'NICKNAME' ) error_log ( __METHOD__ . " (...) text-match: $prop_filter { $filter [ 'attrs' ][ 'match-type' ] } ' { $filter [ 'data' ] } ' unknown property ' $prop_filter ' --> ignored " );
$column = false ; // to ignore following data too
}
else
{
switch ( $filter [ 'attrs' ][ 'collation' ]) // todo: which other collations allowed, we are always unicode
{
case 'i;unicode-casemap' :
default :
$comp = ' ' . $GLOBALS [ 'egw' ] -> db -> capabilities [ Api\Db :: CAPABILITY_CASE_INSENSITIV_LIKE ] . ' ' ;
break ;
}
$column = $this -> filter_prop2cal [ strtoupper ( $prop_filter )];
if ( strpos ( $column , '_' ) === false ) $column = 'contact_' . $column ;
if ( ! isset ( $filters [ 'order' ])) $filters [ 'order' ] = $column ;
$match_type = $filter [ 'attrs' ][ 'match-type' ];
$negate_condition = isset ( $filter [ 'attrs' ][ 'negate-condition' ]) && $filter [ 'attrs' ][ 'negate-condition' ] == 'yes' ;
}
break ;
case '' : // data of text-match element
if ( isset ( $filter [ 'data' ]) && isset ( $column ))
{
if ( $column ) // false for properties not known to EGroupware
{
$value = str_replace ( array ( '%' , '_' ), array ( '\\%' , '\\_' ), $filter [ 'data' ]);
switch ( $match_type )
{
case 'equals' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( $value );
break ;
default :
case 'contains' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( '%' . $value . '%' );
break ;
case 'starts-with' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( $value . '%' );
break ;
case 'ends-with' :
$sql_filter = $column . $comp . $GLOBALS [ 'egw' ] -> db -> quote ( '%' . $value );
break ;
}
$matches [] = ( $negate_condition ? 'NOT ' : '' ) . $sql_filter ;
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (...) text-match: $prop_filter $match_type ' ' { $filter [ 'data' ] } ' " );
}
unset ( $column );
break ;
}
// fall through
default :
$this -> caldav -> log ( __METHOD__ . " ( " . array2string ( $options ) . " ,, $id ) unknown filter= " . array2string ( $filter ) . ' --> ignored' );
break ;
}
}
if ( $matches ) $prop_filters [] = implode ( $prop_test == 'allof' ? ' AND ' : ' OR ' , $matches );
if ( $prop_filters )
{
$filters [] = $filter = '((' . implode ( $filter_test == 'allof' ? ') AND (' : ') OR (' , $prop_filters ) . '))' ;
if ( $this -> debug ) error_log ( __METHOD__ . " (path= $options[path] , ...) sql-filter: $filter " );
}
}
// parse limit from $options['other']
/* Example limit
< B : limit >
< B : nresults > 10 </ B : nresults >
</ B : limit >
*/
foreach (( array ) $options [ 'other' ] as $option )
{
switch ( $option [ 'name' ])
{
case 'nresults' :
$nresults = ( int ) $option [ 'data' ];
//error_log(__METHOD__."(...) options[other]=".array2string($options['other'])." --> nresults=$nresults");
break ;
case 'limit' :
break ;
case 'href' :
break ; // from addressbook-multiget, handled below
// rfc 6578 sync-report
case 'sync-token' :
if ( ! empty ( $option [ 'data' ]))
{
$parts = explode ( '/' , $option [ 'data' ]);
$sync_token = array_pop ( $parts );
$filters [] = 'contact_modified>' . ( int ) $sync_token ;
$filters [ 'tid' ] = null ; // to return deleted entries too
}
break ;
case 'sync-level' :
if ( $option [ 'data' ] != '1' )
{
$this -> caldav -> log ( __METHOD__ . " (...) only sync-level { $option [ 'data' ] } requested, but only 1 supported! options[other]= " . array2string ( $options [ 'other' ]));
}
break ;
default :
$this -> caldav -> log ( __METHOD__ . " (...) unknown xml tag ' { $option [ 'name' ] } ': options[other]= " . array2string ( $options [ 'other' ]));
break ;
}
}
// multiget --> fetch the url's
$this -> requested_multiget_ids = null ;
if ( $options [ 'root' ][ 'name' ] == 'addressbook-multiget' )
{
$this -> requested_multiget_ids = [];
foreach ( $options [ 'other' ] as $option )
{
if ( $option [ 'name' ] == 'href' )
{
$parts = explode ( '/' , $option [ 'data' ]);
if (( $id = urldecode ( array_pop ( $parts ))))
{
$this -> requested_multiget_ids [] = self :: $path_extension ? basename ( $id , self :: $path_extension ) : $id ;
}
}
}
if ( $this -> requested_multiget_ids ) $filters [ self :: $path_attr ] = $this -> requested_multiget_ids ;
if ( $this -> debug ) error_log ( __METHOD__ . " (...) addressbook-multiget: ids= " . implode ( ',' , $this -> requested_multiget_ids ));
}
elseif ( $id )
{
$filters [ self :: $path_attr ] = self :: $path_extension ? basename ( $id , self :: $path_extension ) : $id ;
}
//error_log(__METHOD__."() options[other]=".array2string($options['other'])." --> filters=".array2string($filters));
return true ;
}
/**
* Handle get request for an applications entry
*
* @ param array & $options
* @ param int $id
* @ param int $user = null account_id
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function get ( & $options , $id , $user = null )
{
header ( 'Content-Type: application/json' );
if ( ! is_array ( $timesheet = $this -> _common_get_put_delete ( 'GET' , $options , $id )))
{
return $timesheet ;
}
try
{
// jsContact or vCard
if (( $type = Api\CalDAV :: isJSON ()))
{
$options [ 'data' ] = JsTimesheet :: JsTimesheet ( $timesheet , $type );
$options [ 'mimetype' ] = 'application/json' ;
header ( 'Content-Encoding: identity' );
header ( 'ETag: "' . $this -> get_etag ( $timesheet ) . '"' );
return true ;
}
}
catch ( \Throwable $e ) {
return self :: handleException ( $e );
}
return '501 Not Implemented' ;
}
/**
* Handle exception by returning an appropriate HTTP status and JSON content with an error message
*
* @ param \Throwable $e
* @ return string
*/
protected function handleException ( \Throwable $e ) : string
{
_egw_log_exception ( $e );
header ( 'Content-Type: application/json' );
echo json_encode ([
'error' => $code = $e -> getCode () ? : 500 ,
'message' => $e -> getMessage (),
'details' => $e -> details ? ? null ,
'script' => $e -> script ? ? null ,
] + ( empty ( $GLOBALS [ 'egw_info' ][ 'server' ][ 'exception_show_trace' ]) ? [] : [
'trace' => array_map ( static function ( $trace )
{
$trace [ 'file' ] = str_replace ( EGW_SERVER_ROOT . '/' , '' , $trace [ 'file' ]);
return $trace ;
}, $e -> getTrace ())
]), self :: JSON_RESPONSE_OPTIONS );
return ( 400 <= $code && $code < 600 ? $code : 500 ) . ' ' . $e -> getMessage ();
}
/**
* Handle put request for a contact
*
* @ param array & $options
* @ param int $id
* @ param int $user = null account_id of owner , default null
* @ param string $prefix = null user prefix from path ( eg . / ralf from / ralf / addressbook )
* @ param string $method = 'PUT' also called for POST and PATCH
* @ param string $content_type = null
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function put ( & $options , $id , $user = null , $prefix = null , string $method = 'PUT' , string $content_type = null )
{
$old = $this -> _common_get_put_delete ( $method , $options , $id );
if ( ! is_null ( $old ) && ! is_array ( $old ))
{
if ( $this -> debug ) error_log ( __METHOD__ . " (,' $id ', $user , ' $prefix ') returning " . array2string ( $old ));
return $old ;
}
$type = null ;
$timesheet = JsTimesheet :: parseJsTimesheet ( $options [ 'content' ], $old ? : [], $content_type , $method );
/* uncomment to return parsed data for testing
header ( 'Content-Type: application/json' );
echo json_encode ( $timesheet , JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
return " 200 Ok " ;
*/
if ( is_array ( $old ))
{
$id = $old [ 'id' ];
$retval = true ;
}
else
{
// new entry
$id = - 1 ;
$retval = '201 Created' ;
}
if ( is_array ( $old ))
{
$timesheet [ 'ts_id' ] = $old [ 'id' ];
// don't allow the client to overwrite certain values
$timesheet [ 'ts_owner' ] = $old [ 'owner' ];
$timesheet [ 'ts_created' ] = $old [ 'created' ];
}
else
{
// only set owner, if user is explicitly specified in URL (check via prefix, NOT for /addressbook/) or sync-all-in-one!)
if ( $prefix && $user )
{
$timesheet [ 'ts_owner' ] = $user ;
}
else
{
$timesheet [ 'ts_owner' ] = $GLOBALS [ 'egw_info' ][ 'user' ][ 'account_id' ];
}
}
if ( $this -> http_if_match ) $timesheet [ 'etag' ] = self :: etag2value ( $this -> http_if_match );
if ( ! ( $save_ok = $this -> bo -> save ( $timesheet )))
{
if ( $this -> debug ) error_log ( __METHOD__ . " (, $id ) save( " . array2string ( $timesheet ) . " ) failed, Ok= $save_ok " );
if ( $save_ok === 0 )
{
// honor Prefer: return=representation for 412 too (no need for client to explicitly reload)
$this -> check_return_representation ( $options , $id , $user );
return '412 Precondition Failed' ;
}
return '403 Forbidden' ; // happens when writing new entries in AB's without ADD rights
}
// send evtl. necessary response headers: Location, etag, ...
$this -> put_response_headers ( $timesheet , $options [ 'path' ], $retval );
if ( $this -> debug > 1 ) error_log ( __METHOD__ . " (,' $id ', $user , ' $prefix ') returning " . array2string ( $retval ));
return $retval ;
}
/**
* Handle delete request for an applications entry
*
* @ param array & $options
* @ param int $id
* @ param int $user account_id of collection owner
* @ return mixed boolean true on success , false on failure or string with http status ( eg . '404 Not Found' )
*/
function delete ( & $options , $id , $user )
{
if ( ! is_array ( $timesheet = $this -> _common_get_put_delete ( 'DELETE' , $options , $id )))
{
return $timesheet ;
}
if (( $ok = $this -> bo -> delete ( $timesheet [ 'id' ], self :: etag2value ( $this -> http_if_match ))) === 0 )
{
return '412 Precondition Failed' ;
}
return $ok ;
}
/**
* Read an entry
*
* @ param string | int $id
* @ param string $path = null implementation can use it , used in call from _common_get_put_delete
* @ return array | boolean array with entry , false if no read rights , null if $id does not exist
*/
function read ( $id /*,$path=null*/ )
{
if (( $ret = $this -> bo -> read ( $id )))
{
$ret = Api\Db :: strip_array_keys ( $ret , 'ts_' );
}
return $ret ;
}
/**
* Check if user has the necessary rights on an entry
*
* @ param int $acl Api\Acl :: READ , Api\Acl :: EDIT or Api\Acl :: DELETE
* @ param array | int $entry entry - array or id
* @ return boolean null if entry does not exist , false if no access , true if access permitted
*/
function check_access ( $acl , $entry )
{
2024-02-05 20:06:18 +01:00
return $this -> bo -> check_acl ( $acl , is_array ( $entry ) ? $entry + [ 'ts_owner' => $entry [ 'owner' ]] : $entry );
2023-11-29 14:47:27 +01:00
}
}