2019-12-05 13:12:59 +00:00
< ? php
2020-02-09 14:45:36 +00:00
/**
2021-03-29 06:40:20 +00:00
* @ copyright Copyright ( C ) 2010 - 2021 , the Friendica project
2020-02-09 14:45:36 +00:00
*
* @ license GNU AGPL version 3 or any later version
*
* This program is free software : you can redistribute it and / or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation , either version 3 of the
* License , or ( at your option ) any later version .
*
* This program is distributed in the hope that it will be useful ,
* but WITHOUT ANY WARRANTY ; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE . See the
* GNU Affero General Public License for more details .
*
* You should have received a copy of the GNU Affero General Public License
* along with this program . If not , see < https :// www . gnu . org / licenses />.
*
*/
2019-12-05 13:12:59 +00:00
2020-01-28 02:18:42 +00:00
namespace Friendica\Module ;
2019-12-05 13:12:59 +00:00
use Friendica\BaseModule ;
2021-05-08 09:14:19 +00:00
use Friendica\Core\Logger ;
use Friendica\Core\System ;
2019-12-15 21:34:11 +00:00
use Friendica\DI ;
2021-07-08 13:47:46 +00:00
use Friendica\Model\Post ;
2019-12-05 13:12:59 +00:00
use Friendica\Network\HTTPException ;
2021-06-08 06:32:24 +00:00
use Friendica\Security\BasicAuth ;
use Friendica\Security\OAuth ;
2021-11-08 22:10:07 +00:00
use Friendica\Util\Arrays ;
2021-07-08 13:47:46 +00:00
use Friendica\Util\DateTimeFormat ;
2021-05-28 06:10:32 +00:00
use Friendica\Util\HTTPInputData ;
2021-11-08 21:35:41 +00:00
use Friendica\Util\XML ;
2019-12-05 13:12:59 +00:00
2020-01-28 02:18:42 +00:00
require_once __DIR__ . '/../../include/api.php' ;
2019-12-05 13:12:59 +00:00
2020-01-28 02:18:42 +00:00
class BaseApi extends BaseModule
2019-12-05 13:12:59 +00:00
{
2021-05-16 07:37:11 +00:00
const SCOPE_READ = 'read' ;
const SCOPE_WRITE = 'write' ;
const SCOPE_FOLLOW = 'follow' ;
const SCOPE_PUSH = 'push' ;
2021-06-16 15:02:33 +00:00
/**
* @ var array
*/
protected static $boundaries = [];
/**
* @ var array
*/
protected static $request = [];
2019-12-05 13:12:59 +00:00
public static function init ( array $parameters = [])
{
}
2021-05-08 09:14:19 +00:00
public static function delete ( array $parameters = [])
{
2021-06-08 20:41:46 +00:00
self :: checkAllowedScope ( self :: SCOPE_WRITE );
2021-05-08 09:14:19 +00:00
2021-08-08 19:30:21 +00:00
if ( ! DI :: app () -> isLoggedIn ()) {
2021-05-08 09:14:19 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Permission denied.' ));
}
}
public static function patch ( array $parameters = [])
{
2021-06-08 20:41:46 +00:00
self :: checkAllowedScope ( self :: SCOPE_WRITE );
2021-05-08 09:14:19 +00:00
2021-08-08 19:30:21 +00:00
if ( ! DI :: app () -> isLoggedIn ()) {
2021-05-08 09:14:19 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Permission denied.' ));
}
}
2019-12-05 13:12:59 +00:00
public static function post ( array $parameters = [])
{
2021-06-08 20:41:46 +00:00
self :: checkAllowedScope ( self :: SCOPE_WRITE );
2019-12-05 13:12:59 +00:00
2021-08-08 19:30:21 +00:00
if ( ! DI :: app () -> isLoggedIn ()) {
2020-01-18 19:52:34 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Permission denied.' ));
2019-12-05 13:12:59 +00:00
}
}
2021-05-08 09:14:19 +00:00
public static function put ( array $parameters = [])
{
2021-06-08 20:41:46 +00:00
self :: checkAllowedScope ( self :: SCOPE_WRITE );
2021-05-08 09:14:19 +00:00
2021-08-08 19:30:21 +00:00
if ( ! DI :: app () -> isLoggedIn ()) {
2021-05-08 09:14:19 +00:00
throw new HTTPException\ForbiddenException ( DI :: l10n () -> t ( 'Permission denied.' ));
}
}
2021-05-12 12:08:30 +00:00
/**
* Quit execution with the message that the endpoint isn ' t implemented
*
* @ param string $method
* @ return void
*/
2021-05-08 09:14:19 +00:00
public static function unsupported ( string $method = 'all' )
{
$path = DI :: args () -> getQueryString ();
2021-05-28 06:10:32 +00:00
Logger :: info ( 'Unimplemented API call' , [ 'method' => $method , 'path' => $path , 'agent' => $_SERVER [ 'HTTP_USER_AGENT' ] ? ? '' , 'request' => HTTPInputData :: process ()]);
2021-05-08 12:23:47 +00:00
$error = DI :: l10n () -> t ( 'API endpoint %s %s is not implemented' , strtoupper ( $method ), $path );
2021-05-12 12:08:30 +00:00
$error_description = DI :: l10n () -> t ( 'The API endpoint is currently not implemented but might be in the future.' );
2021-05-08 09:14:19 +00:00
$errorobj = new \Friendica\Object\Api\Mastodon\Error ( $error , $error_description );
System :: jsonError ( 501 , $errorobj -> toArray ());
}
2021-05-18 06:31:22 +00:00
/**
* Processes data from GET requests and sets defaults
*
* @ return array request data
*/
2021-05-29 10:40:47 +00:00
public static function getRequest ( array $defaults )
{
2021-05-28 06:10:32 +00:00
$httpinput = HTTPInputData :: process ();
$input = array_merge ( $httpinput [ 'variables' ], $httpinput [ 'files' ], $_REQUEST );
2021-06-16 15:02:33 +00:00
self :: $request = $input ;
self :: $boundaries = [];
unset ( self :: $request [ 'pagename' ]);
2021-05-18 06:31:22 +00:00
$request = [];
foreach ( $defaults as $parameter => $defaultvalue ) {
if ( is_string ( $defaultvalue )) {
2021-05-28 06:10:32 +00:00
$request [ $parameter ] = $input [ $parameter ] ? ? $defaultvalue ;
2021-05-18 06:31:22 +00:00
} elseif ( is_int ( $defaultvalue )) {
2021-05-28 06:10:32 +00:00
$request [ $parameter ] = ( int )( $input [ $parameter ] ? ? $defaultvalue );
2021-05-18 06:31:22 +00:00
} elseif ( is_float ( $defaultvalue )) {
2021-05-28 06:10:32 +00:00
$request [ $parameter ] = ( float )( $input [ $parameter ] ? ? $defaultvalue );
2021-05-18 06:31:22 +00:00
} elseif ( is_array ( $defaultvalue )) {
2021-05-28 06:10:32 +00:00
$request [ $parameter ] = $input [ $parameter ] ? ? [];
2021-05-18 06:31:22 +00:00
} elseif ( is_bool ( $defaultvalue )) {
2021-05-28 06:10:32 +00:00
$request [ $parameter ] = in_array ( strtolower ( $input [ $parameter ] ? ? '' ), [ 'true' , '1' ]);
2021-05-18 06:31:22 +00:00
} else {
Logger :: notice ( 'Unhandled default value type' , [ 'parameter' => $parameter , 'type' => gettype ( $defaultvalue )]);
}
}
2021-05-28 06:10:32 +00:00
foreach ( $input ? ? [] as $parameter => $value ) {
2021-05-18 06:31:22 +00:00
if ( $parameter == 'pagename' ) {
continue ;
}
if ( ! in_array ( $parameter , array_keys ( $defaults ))) {
Logger :: notice ( 'Unhandled request field' , [ 'parameter' => $parameter , 'value' => $value , 'command' => DI :: args () -> getCommand ()]);
}
}
Logger :: debug ( 'Got request parameters' , [ 'request' => $request , 'command' => DI :: args () -> getCommand ()]);
return $request ;
}
2021-06-16 15:02:33 +00:00
/**
* Set boundaries for the " link " header
* @ param array $boundaries
* @ param int $id
* @ return array
*/
protected static function setBoundaries ( int $id )
{
if ( ! isset ( self :: $boundaries [ 'min' ])) {
self :: $boundaries [ 'min' ] = $id ;
}
if ( ! isset ( self :: $boundaries [ 'max' ])) {
self :: $boundaries [ 'max' ] = $id ;
}
self :: $boundaries [ 'min' ] = min ( self :: $boundaries [ 'min' ], $id );
self :: $boundaries [ 'max' ] = max ( self :: $boundaries [ 'max' ], $id );
}
/**
* Set the " link " header with " next " and " prev " links
* @ return void
*/
protected static function setLinkHeader ()
{
if ( empty ( self :: $boundaries )) {
return ;
}
$request = self :: $request ;
unset ( $request [ 'min_id' ]);
unset ( $request [ 'max_id' ]);
unset ( $request [ 'since_id' ]);
$prev_request = $next_request = $request ;
2021-06-16 17:57:01 +00:00
$prev_request [ 'min_id' ] = self :: $boundaries [ 'max' ];
$next_request [ 'max_id' ] = self :: $boundaries [ 'min' ];
2021-06-16 15:02:33 +00:00
$command = DI :: baseUrl () . '/' . DI :: args () -> getCommand ();
$prev = $command . '?' . http_build_query ( $prev_request );
$next = $command . '?' . http_build_query ( $next_request );
header ( 'Link: <' . $next . '>; rel="next", <' . $prev . '>; rel="prev"' );
}
2021-05-15 22:40:57 +00:00
/**
2021-06-08 06:32:24 +00:00
* Get current application token
2021-05-15 22:40:57 +00:00
*
* @ return array token
*/
protected static function getCurrentApplication ()
{
2021-06-08 06:32:24 +00:00
$token = OAuth :: getCurrentApplicationToken ();
2021-05-11 06:30:20 +00:00
2021-06-08 06:32:24 +00:00
if ( empty ( $token )) {
$token = BasicAuth :: getCurrentApplicationToken ();
2021-05-11 19:15:05 +00:00
}
2021-05-15 22:40:57 +00:00
return $token ;
2021-05-11 06:30:20 +00:00
}
2021-05-12 12:08:30 +00:00
/**
2021-06-08 06:32:24 +00:00
* Get current user id , returns 0 if not logged in
2021-05-12 12:08:30 +00:00
*
2021-06-08 06:32:24 +00:00
* @ return int User ID
2021-05-12 12:08:30 +00:00
*/
2021-06-08 20:45:58 +00:00
protected static function getCurrentUserID ()
2021-05-11 06:30:20 +00:00
{
2021-06-08 06:32:24 +00:00
$uid = OAuth :: getCurrentUserID ();
2021-05-28 06:10:32 +00:00
2021-06-08 06:32:24 +00:00
if ( empty ( $uid )) {
$uid = BasicAuth :: getCurrentUserID ( false );
2021-05-11 06:30:20 +00:00
}
2021-06-08 06:32:24 +00:00
return ( int ) $uid ;
2021-05-11 06:30:20 +00:00
}
2021-05-12 06:50:27 +00:00
2021-06-08 09:11:56 +00:00
/**
* Check if the provided scope does exist .
* halts execution on missing scope or when not logged in .
*
* @ param string $scope the requested scope ( read , write , follow , push )
*/
public static function checkAllowedScope ( string $scope )
{
$token = self :: getCurrentApplication ();
if ( empty ( $token )) {
Logger :: notice ( 'Empty application token' );
DI :: mstdnError () -> Forbidden ();
}
if ( ! isset ( $token [ $scope ])) {
Logger :: warning ( 'The requested scope does not exist' , [ 'scope' => $scope , 'application' => $token ]);
DI :: mstdnError () -> Forbidden ();
}
if ( empty ( $token [ $scope ])) {
Logger :: warning ( 'The requested scope is not allowed' , [ 'scope' => $scope , 'application' => $token ]);
DI :: mstdnError () -> Forbidden ();
}
}
2021-07-08 13:47:46 +00:00
public static function checkThrottleLimit ()
{
$uid = self :: getCurrentUserID ();
// Check for throttling (maximum posts per day, week and month)
$throttle_day = DI :: config () -> get ( 'system' , 'throttle_limit_day' );
if ( $throttle_day > 0 ) {
$datefrom = date ( DateTimeFormat :: MYSQL , time () - 24 * 60 * 60 );
$condition = [ " `gravity` = ? AND `uid` = ? AND `wall` AND `received` > ? " , GRAVITY_PARENT , $uid , $datefrom ];
2021-07-08 17:32:41 +00:00
$posts_day = Post :: countThread ( $condition );
2021-07-08 13:47:46 +00:00
if ( $posts_day > $throttle_day ) {
Logger :: info ( 'Daily posting limit reached' , [ 'uid' => $uid , 'posts' => $posts_day , 'limit' => $throttle_day ]);
$error = DI :: l10n () -> t ( 'Too Many Requests' );
$error_description = DI :: l10n () -> tt ( " Daily posting limit of %d post reached. The post was rejected. " , " Daily posting limit of %d posts reached. The post was rejected. " , $throttle_day );
$errorobj = new \Friendica\Object\Api\Mastodon\Error ( $error , $error_description );
System :: jsonError ( 429 , $errorobj -> toArray ());
}
}
$throttle_week = DI :: config () -> get ( 'system' , 'throttle_limit_week' );
if ( $throttle_week > 0 ) {
$datefrom = date ( DateTimeFormat :: MYSQL , time () - 24 * 60 * 60 * 7 );
$condition = [ " `gravity` = ? AND `uid` = ? AND `wall` AND `received` > ? " , GRAVITY_PARENT , $uid , $datefrom ];
2021-07-08 17:32:41 +00:00
$posts_week = Post :: countThread ( $condition );
2021-07-08 13:47:46 +00:00
if ( $posts_week > $throttle_week ) {
Logger :: info ( 'Weekly posting limit reached' , [ 'uid' => $uid , 'posts' => $posts_week , 'limit' => $throttle_week ]);
$error = DI :: l10n () -> t ( 'Too Many Requests' );
$error_description = DI :: l10n () -> tt ( " Weekly posting limit of %d post reached. The post was rejected. " , " Weekly posting limit of %d posts reached. The post was rejected. " , $throttle_week );
$errorobj = new \Friendica\Object\Api\Mastodon\Error ( $error , $error_description );
System :: jsonError ( 429 , $errorobj -> toArray ());
}
}
$throttle_month = DI :: config () -> get ( 'system' , 'throttle_limit_month' );
if ( $throttle_month > 0 ) {
$datefrom = date ( DateTimeFormat :: MYSQL , time () - 24 * 60 * 60 * 30 );
$condition = [ " `gravity` = ? AND `uid` = ? AND `wall` AND `received` > ? " , GRAVITY_PARENT , $uid , $datefrom ];
2021-07-08 17:32:41 +00:00
$posts_month = Post :: countThread ( $condition );
2021-07-08 13:47:46 +00:00
if ( $posts_month > $throttle_month ) {
Logger :: info ( 'Monthly posting limit reached' , [ 'uid' => $uid , 'posts' => $posts_month , 'limit' => $throttle_month ]);
$error = DI :: l10n () -> t ( 'Too Many Requests' );
$error_description = DI :: l10n () -> t ( " Monthly posting limit of %d post reached. The post was rejected. " , " Monthly posting limit of %d posts reached. The post was rejected. " , $throttle_month );
$errorobj = new \Friendica\Object\Api\Mastodon\Error ( $error , $error_description );
System :: jsonError ( 429 , $errorobj -> toArray ());
}
}
}
2019-12-05 13:12:59 +00:00
/**
2020-01-19 06:05:23 +00:00
* Get user info array .
2019-12-05 13:12:59 +00:00
*
* @ param int | string $contact_id Contact ID or URL
* @ return array | bool
* @ throws HTTPException\BadRequestException
* @ throws HTTPException\InternalServerErrorException
* @ throws HTTPException\UnauthorizedException
* @ throws \ImagickException
*/
protected static function getUser ( $contact_id = null )
{
2021-11-08 21:35:41 +00:00
return api_get_user ( $contact_id );
2019-12-05 13:12:59 +00:00
}
2020-01-23 14:08:37 +00:00
/**
2021-11-09 06:42:59 +00:00
* Outputs formatted data according to the data type and then exits the execution .
2020-01-23 14:08:37 +00:00
*
* @ param string $root_element
2021-11-09 19:40:39 +00:00
* @ param array $data An array with a single element containing the returned result
* @ param string $format Output format ( xml , json , rss , atom )
2020-01-23 14:08:37 +00:00
* @ return false | string
*/
2021-11-09 19:40:39 +00:00
protected static function exit ( string $root_element , array $data , string $format = null )
2019-12-05 13:12:59 +00:00
{
2021-11-09 19:40:39 +00:00
$format = $format ? ? 'json' ;
$return = self :: formatData ( $root_element , $format , $data );
2020-01-23 14:08:37 +00:00
2021-11-09 19:40:39 +00:00
switch ( $format ) {
2021-11-08 21:35:41 +00:00
case 'xml' :
header ( 'Content-Type: text/xml' );
2019-12-05 13:12:59 +00:00
break ;
2021-11-08 21:35:41 +00:00
case 'json' :
header ( 'Content-Type: application/json' );
2020-01-23 14:08:37 +00:00
if ( ! empty ( $return )) {
$json = json_encode ( end ( $return ));
if ( ! empty ( $_GET [ 'callback' ])) {
2021-11-08 21:35:41 +00:00
$json = $_GET [ 'callback' ] . '(' . $json . ')' ;
2020-01-23 14:08:37 +00:00
}
$return = $json ;
}
break ;
2021-11-08 21:35:41 +00:00
case 'rss' :
header ( 'Content-Type: application/rss+xml' );
2020-01-23 14:08:37 +00:00
$return = '<?xml version="1.0" encoding="UTF-8"?>' . " \n " . $return ;
break ;
2021-11-08 21:35:41 +00:00
case 'atom' :
header ( 'Content-Type: application/atom+xml' );
2020-01-23 14:08:37 +00:00
$return = '<?xml version="1.0" encoding="UTF-8"?>' . " \n " . $return ;
2019-12-05 13:12:59 +00:00
break ;
}
2021-05-11 06:30:20 +00:00
2021-11-09 06:42:59 +00:00
echo $return ;
exit ;
2020-01-23 14:08:37 +00:00
}
2019-12-05 13:12:59 +00:00
2021-11-08 21:35:41 +00:00
/**
* Formats the data according to the data type
*
* @ param string $root_element Name of the root element
* @ param string $type Return type ( atom , rss , xml , json )
* @ param array $data JSON style array
*
* @ return array | string ( string | array ) XML data or JSON data
*/
public static function formatData ( $root_element , string $type , array $data )
{
switch ( $type ) {
case 'atom' :
case 'rss' :
case 'xml' :
$ret = self :: createXML ( $data , $root_element );
break ;
case 'json' :
default :
$ret = $data ;
break ;
}
return $ret ;
}
/**
* Callback function to transform the array in an array that can be transformed in a XML file
*
* @ param mixed $item Array item value
* @ param string $key Array key
*
* @ return boolean
*/
public static function reformatXML ( & $item , & $key )
{
if ( is_bool ( $item )) {
$item = ( $item ? 'true' : 'false' );
}
if ( substr ( $key , 0 , 10 ) == 'statusnet_' ) {
$key = 'statusnet:' . substr ( $key , 10 );
} elseif ( substr ( $key , 0 , 10 ) == 'friendica_' ) {
$key = 'friendica:' . substr ( $key , 10 );
}
return true ;
}
2020-01-23 14:08:37 +00:00
/**
* Creates the XML from a JSON style array
*
2021-11-08 21:35:41 +00:00
* @ param array $data JSON style array
* @ param string $root_element Name of the root element
*
* @ return string The XML data
2020-01-23 14:08:37 +00:00
*/
2021-11-08 21:35:41 +00:00
public static function createXML ( array $data , $root_element )
2020-01-23 14:08:37 +00:00
{
2021-11-08 21:35:41 +00:00
$childname = key ( $data );
$data2 = array_pop ( $data );
$namespaces = [ '' => 'http://api.twitter.com' ,
'statusnet' => 'http://status.net/schema/api/1/' ,
'friendica' => 'http://friendi.ca/schema/api/1/' ,
'georss' => 'http://www.georss.org/georss' ];
/// @todo Auto detection of needed namespaces
if ( in_array ( $root_element , [ 'ok' , 'hash' , 'config' , 'version' , 'ids' , 'notes' , 'photos' ])) {
$namespaces = [];
}
if ( is_array ( $data2 )) {
$key = key ( $data2 );
2021-11-08 22:10:07 +00:00
Arrays :: walkRecursive ( $data2 , [ 'Friendica\Module\BaseApi' , 'reformatXML' ]);
2021-11-08 21:35:41 +00:00
if ( $key == '0' ) {
$data4 = [];
$i = 1 ;
foreach ( $data2 as $item ) {
$data4 [ $i ++ . ':' . $childname ] = $item ;
}
$data2 = $data4 ;
}
}
$data3 = [ $root_element => $data2 ];
$ret = XML :: fromArray ( $data3 , $xml , false , $namespaces );
return $ret ;
2019-12-05 13:12:59 +00:00
}
}