Version 4.5.0
This commit is contained in:
1 parent
1a0c7fd3c2
commit
f79dcf067a
3791 files changed
+248032
-76372
No files matched your search
+58
-33
@@ -11,7 +11,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
@@ -50,7 +50,8 @@ abstract class _Controller
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param \IPS\Api\Key $apiKey The API key being used to access, if applicable
|
||||
* @param \IPS\Api\Key $apiKey The API key being used to access, if applicable
|
||||
* @param string $accessToken Access token for OAuth-authenticated requests
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( \IPS\Api\Key $apiKey = NULL, $accessToken = NULL )
|
||||
@@ -122,12 +123,12 @@ abstract class _Controller
|
||||
*/
|
||||
public function execute( $pathBits, &$shouldLog )
|
||||
{
|
||||
$method = ( isset( $_SERVER['REQUEST_METHOD'] ) and in_array( mb_strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'POST', 'PUT', 'DELETE' ) ) ) ? mb_strtoupper( $_SERVER['REQUEST_METHOD'] ) : 'GET';
|
||||
$method = ( isset( $_SERVER['REQUEST_METHOD'] ) and \in_array( mb_strtoupper( $_SERVER['REQUEST_METHOD'] ), array( 'GET', 'POST', 'PUT', 'DELETE' ) ) ) ? mb_strtoupper( $_SERVER['REQUEST_METHOD'] ) : 'GET';
|
||||
$params = array();
|
||||
|
||||
try
|
||||
{
|
||||
$endpointData = $this->_getEndpoint( $pathBits );
|
||||
$endpointData = $this->_getEndpoint( $pathBits, $method );
|
||||
}
|
||||
catch ( \RuntimeException $e )
|
||||
{
|
||||
@@ -136,7 +137,7 @@ abstract class _Controller
|
||||
|
||||
if ( method_exists( $this, "{$method}{$endpointData['endpoint']}" ) )
|
||||
{
|
||||
preg_match( '/^IPS\\\(.+?)\\\api\\\(.+?)$/', get_called_class(), $matches );
|
||||
preg_match( '/^IPS\\\(.+?)\\\api\\\(.+?)$/', \get_called_class(), $matches );
|
||||
|
||||
$shouldLog = $this->shouldLog( $matches[1], $matches[2], "{$method}{$endpointData['endpoint']}" );
|
||||
if ( !$this->canAccess( $matches[1], $matches[2], "{$method}{$endpointData['endpoint']}" ) )
|
||||
@@ -161,7 +162,10 @@ abstract class _Controller
|
||||
}
|
||||
}
|
||||
|
||||
return call_user_func_array( array( $this, "{$method}{$endpointData['endpoint']}" ), $endpointData['params'] );
|
||||
$methodName = "{$method}{$endpointData['endpoint']}";
|
||||
$parameters = $endpointData['params'];
|
||||
|
||||
return $this->$methodName( ...$parameters );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -173,34 +177,50 @@ abstract class _Controller
|
||||
* Get endpoint data
|
||||
*
|
||||
* @param array $pathBits The parts to the path called
|
||||
* @param string $method HTTP method verb
|
||||
* @return array
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function _getEndpoint( $pathBits )
|
||||
protected function _getEndpoint( $pathBits, $method = 'GET' )
|
||||
{
|
||||
$endpoint = NULL;
|
||||
$params = array();
|
||||
|
||||
if ( count( $pathBits ) === 0 )
|
||||
if ( \count( $pathBits ) === 0 )
|
||||
{
|
||||
$endpoint = 'index';
|
||||
}
|
||||
elseif ( count( $pathBits ) === 1 )
|
||||
elseif ( \count( $pathBits ) === 1 )
|
||||
{
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint = 'item';
|
||||
/* Sometimes we want to do something like /api/core/clubs/contenttypes but this is treated as an item */
|
||||
if( \method_exists( $this, $method . $pathBits[0] ) )
|
||||
{
|
||||
$endpoint = array_shift( $pathBits );
|
||||
}
|
||||
else
|
||||
{
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint = 'item';
|
||||
}
|
||||
}
|
||||
elseif ( count( $pathBits ) === 2 )
|
||||
elseif ( \count( $pathBits ) === 2 )
|
||||
{
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint = 'item_' . array_shift( $pathBits );
|
||||
}
|
||||
elseif ( count( $pathBits ) === 3 )
|
||||
elseif ( \count( $pathBits ) === 3 )
|
||||
{
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint = 'item_' . array_shift( $pathBits );
|
||||
$params[] = array_shift( $pathBits );
|
||||
}
|
||||
elseif ( \count( $pathBits ) === 4 )
|
||||
{
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint = 'item_' . array_shift( $pathBits );
|
||||
$params[] = array_shift( $pathBits );
|
||||
$endpoint .= '_' . array_shift( $pathBits );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
@@ -212,36 +232,40 @@ abstract class _Controller
|
||||
/**
|
||||
* Get all endpoints
|
||||
*
|
||||
* @param string|null $type If 'client' or 'member', will not include endpoints that are't applicable
|
||||
* @param string|null $type If 'client' or 'member', will not include endpoints that are't applicable
|
||||
* @param bool $includeAll Should also disabled applications be returned
|
||||
* @return array
|
||||
*/
|
||||
public static function getAllEndpoints( $type = NULL )
|
||||
public static function getAllEndpoints( $type = NULL, $includeAll = FALSE )
|
||||
{
|
||||
$return = array();
|
||||
foreach ( \IPS\Application::applications() as $app )
|
||||
{
|
||||
$apiDir = \IPS\ROOT_PATH . '/applications/' . $app->directory . '/api';
|
||||
if ( file_exists( $apiDir ) )
|
||||
if ( $app->enabled OR $includeAll )
|
||||
{
|
||||
$directory = new \DirectoryIterator( $apiDir );
|
||||
foreach ( $directory as $file )
|
||||
$apiDir = \IPS\ROOT_PATH . '/applications/' . $app->directory . '/api';
|
||||
if ( file_exists( $apiDir ) )
|
||||
{
|
||||
if ( !$file->isDot() and mb_substr( $file, 0, 1 ) != '.' )
|
||||
$directory = new \DirectoryIterator( $apiDir );
|
||||
foreach ( $directory as $file )
|
||||
{
|
||||
$controllerName = mb_substr( $file, 0, -4 );
|
||||
$class = 'IPS\\' . $app->directory . '\\api\\' . $controllerName;
|
||||
if( class_exists( $class ) )
|
||||
if ( !$file->isDot() and mb_substr( $file, 0, 1 ) != '.' )
|
||||
{
|
||||
$reflection = new \ReflectionClass( $class );
|
||||
foreach ( $reflection->getMethods() as $method )
|
||||
$controllerName = mb_substr( $file, 0, -4 );
|
||||
$class = 'IPS\\' . $app->directory . '\\api\\' . $controllerName;
|
||||
if( class_exists( $class ) )
|
||||
{
|
||||
if ( $method->getName() != 'execute' and !$method->isStatic() and $method->isPublic() and mb_substr( $method->getName(), 0, 1 ) != '_' )
|
||||
$reflection = new \ReflectionClass( $class );
|
||||
foreach ( $reflection->getMethods() as $method )
|
||||
{
|
||||
$details = static::decodeDocblock( $method->getDocComment() );
|
||||
|
||||
if ( ( $type !== 'client' or !isset( $details['details']['apimemberonly'] ) ) and ( $type !== 'member' or !isset( $details['details']['apiclientonly'] ) ) )
|
||||
if ( $method->getName() != 'execute' and !$method->isStatic() and $method->isPublic() and mb_substr( $method->getName(), 0, 1 ) != '_' )
|
||||
{
|
||||
$return[ $app->directory . '/' . $controllerName . '/' . $method->getName() ] = $details;
|
||||
$details = static::decodeDocblock( $method->getDocComment() );
|
||||
|
||||
if ( ( $type !== 'client' or !isset( $details['details']['apimemberonly'] ) ) and ( $type !== 'member' or !isset( $details['details']['apiclientonly'] ) ) )
|
||||
{
|
||||
$return[ $app->directory . '/' . $controllerName . '/' . $method->getName() ] = $details;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -277,7 +301,7 @@ abstract class _Controller
|
||||
$params = array();
|
||||
while ( $nextLine = array_shift( $comment ) )
|
||||
{
|
||||
if ( preg_match( '/^\s*\*\s*@([a-z]*)(\t+([^\t]*))?(\t+([^\t]*))?(\t+([^\t]*))?$/', $nextLine, $matches ) )
|
||||
if ( preg_match( '/^\s*\*\s*@([a-z]*)(\t+([^\t]*))?(\t+([^\t]*))?(\t+([^\t]*))?$/', trim( $nextLine ), $matches ) )
|
||||
{
|
||||
$details = array();
|
||||
foreach ( array( 3, 5, 7 ) as $k )
|
||||
@@ -311,7 +335,8 @@ abstract class _Controller
|
||||
*
|
||||
* @param string $endpoint The endpoint (e.g: GET /core/members)
|
||||
* @param string $size Size of badge to show
|
||||
* @return array
|
||||
* @param bool $includeBaseUrl Whether or not to include the URL base in the endpoint URL
|
||||
* @return string
|
||||
*/
|
||||
public static function parseEndpointForDisplay( $endpoint, $size='small', $includeBaseUrl=FALSE )
|
||||
{
|
||||
@@ -323,7 +348,7 @@ abstract class _Controller
|
||||
);
|
||||
|
||||
$pieces = explode( ' ', $endpoint );
|
||||
if ( !in_array( $pieces[0], array_keys( $badgeStyles ) ) )
|
||||
if ( !\in_array( $pieces[0], array_keys( $badgeStyles ) ) )
|
||||
{
|
||||
\IPS\Output::i()->error( \IPS\Member::loggedIn()->language()->addToStack( 'api_endpoint_phpdoc_error', FALSE, array( "sprintf" => array( $endpoint ) ) ), '3S291/4', 500 );
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: API query controller
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 15 Oct 2019
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Core controller for GraphQL API
|
||||
* @todo maybe this shouldn't be a class since it only has a static method?
|
||||
*/
|
||||
abstract class _AppQueries
|
||||
{
|
||||
|
||||
/**
|
||||
* Get the supported query types in this app
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function queries($app)
|
||||
{
|
||||
return [
|
||||
'moduleAccess' => new \IPS\Api\GraphQL\Queries\ModuleAccess($app)
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: Coverphoto field defintiion
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 10 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Fields;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ForumType for GraphQL API
|
||||
*/
|
||||
abstract class _CoverPhotoField
|
||||
{
|
||||
/**
|
||||
* Get root type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getDefinition($name)
|
||||
{
|
||||
return [
|
||||
'type' => new ObjectType([
|
||||
'name' => $name,
|
||||
'description' => 'Returns a cover photo',
|
||||
'fields' => [
|
||||
'image' => TypeRegistry::string(),
|
||||
'offset' => TypeRegistry::int()
|
||||
],
|
||||
'resolveField' => function( $value, $args, $context, $info ) {
|
||||
switch ($info->fieldName)
|
||||
{
|
||||
case 'image':
|
||||
return ( $value->file ) ? (string) $value->file->url : null;
|
||||
break;
|
||||
case 'offset':
|
||||
return ( $value->file ) ? $value->offset : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
]),
|
||||
'description' => 'Returns a cover photo',
|
||||
'resolve' => function ($thing) {
|
||||
return $thing->coverPhoto();
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: TagsEdit field defintiion
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 10 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Fields;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* TagsEditField for GraphQL API
|
||||
*/
|
||||
abstract class _TagsEditField
|
||||
{
|
||||
/**
|
||||
* Get root type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getDefinition($name)
|
||||
{
|
||||
return [
|
||||
'type' => new ObjectType([
|
||||
'name' => $name,
|
||||
'fields' => [
|
||||
'enabled' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($container, $args, $context) {
|
||||
$contentClass = $container::$contentItemClass;
|
||||
|
||||
if( \in_array( 'IPS\Content\Tags', class_implements( $contentClass ) ) ){
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
],
|
||||
'definedTags' => [
|
||||
'type' => TypeRegistry::listOf( TypeRegistry::string() ),
|
||||
'resolve' => function ($container, $args, $context) {
|
||||
$contentClass = $container::$contentItemClass;
|
||||
$tags = array_unique( $contentClass::definedTags( $container ) );
|
||||
|
||||
return $tags;
|
||||
}
|
||||
]
|
||||
]
|
||||
]),
|
||||
'description' => 'Returns tag editing capabilities',
|
||||
'resolve' => function ($container) {
|
||||
return $container;
|
||||
}
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: ModuleAccess query
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 10 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Queries;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleAccess query for GraphQL API
|
||||
*/
|
||||
class _ModuleAccess
|
||||
{
|
||||
public function __construct($app)
|
||||
{
|
||||
$this->app = $app;
|
||||
}
|
||||
|
||||
/*
|
||||
* @brief Query description
|
||||
*/
|
||||
public static $description = "Returns module view permissions";
|
||||
|
||||
/*
|
||||
* Query arguments
|
||||
*/
|
||||
public function args()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query return type
|
||||
*/
|
||||
public function type()
|
||||
{
|
||||
return TypeRegistry::moduleAccess();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves this query
|
||||
*
|
||||
* @param mixed Value passed into this resolver
|
||||
* @param array Arguments
|
||||
* @param array Context values
|
||||
* @return \IPS\core\Stream
|
||||
*/
|
||||
public function resolve($val, $args, $context)
|
||||
{
|
||||
return $this->app;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Safe GraphQL Exception (i.e. client can be informed of the error message)
|
||||
* @author <a href='https://www.invisioncommunity.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) Invision Power Services, Inc.
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @since 3 Dec 2015
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
use GraphQL\Error\ClientAware;
|
||||
|
||||
/**
|
||||
* API Exception
|
||||
*/
|
||||
class _SafeException extends \Exception implements ClientAware
|
||||
{
|
||||
/**
|
||||
* @brief Exception code
|
||||
*/
|
||||
public $exceptionCode;
|
||||
|
||||
/**
|
||||
* @brief OAUth Error
|
||||
*/
|
||||
public $oauthError;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param string $message Error Message
|
||||
* @param string $code Code
|
||||
* @param int $httpCode HTTP Error code
|
||||
* @param string $oauthError Error Message for OAuth
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $message, $code, $httpCode )
|
||||
{
|
||||
$this->exceptionCode = $code;
|
||||
return parent::__construct( $message, $httpCode );
|
||||
}
|
||||
|
||||
public function getCategory()
|
||||
{
|
||||
return 'clienterror';
|
||||
}
|
||||
|
||||
public function isClientSafe()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Type registry
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL;
|
||||
use GraphQL\Type\Definition\NonNull;
|
||||
use GraphQL\Type\Definition\ListOfType;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type registry
|
||||
*/
|
||||
class _TypeRegistry
|
||||
{
|
||||
protected static $query;
|
||||
protected static $mutation;
|
||||
protected static $itemState;
|
||||
protected static $image;
|
||||
protected static $reputation;
|
||||
protected static $richText;
|
||||
protected static $url;
|
||||
protected static $follow;
|
||||
protected static $moduleAccess;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
// Defined to suppress static warnings
|
||||
}
|
||||
|
||||
/**
|
||||
* @return QueryType
|
||||
*/
|
||||
public static function query()
|
||||
{
|
||||
return self::$query ?: (self::$query = new \IPS\Api\GraphQL\Types\QueryType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return MutationType
|
||||
*/
|
||||
public static function mutation()
|
||||
{
|
||||
return self::$mutation ?: (self::$mutation = new \IPS\Api\GraphQL\Types\MutationType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ItemStateType
|
||||
*/
|
||||
public static function itemState()
|
||||
{
|
||||
return self::$itemState ?: (self::$itemState = new \IPS\Api\GraphQL\Types\ItemStateType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ImageType
|
||||
*/
|
||||
public static function image()
|
||||
{
|
||||
return self::$image ?: (self::$image = new \IPS\Api\GraphQL\Types\ImageType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ReputationType
|
||||
*/
|
||||
public static function reputation()
|
||||
{
|
||||
return self::$reputation ?: (self::$reputation = new \IPS\Api\GraphQL\Types\ReputationType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RichTextType
|
||||
*/
|
||||
public static function richText()
|
||||
{
|
||||
return self::$richText ?: (self::$richText = new \IPS\Api\GraphQL\Types\RichTextType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return URLType
|
||||
*/
|
||||
public static function url()
|
||||
{
|
||||
return self::$url ?: (self::$url = new \IPS\Api\GraphQL\Types\UrlType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FollowType
|
||||
*/
|
||||
public static function follow()
|
||||
{
|
||||
return self::$follow ?: (self::$follow = new \IPS\Api\GraphQL\Types\FollowType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ModuleAccessType
|
||||
*/
|
||||
public static function moduleAccess()
|
||||
{
|
||||
return self::$moduleAccess ?: (self::$moduleAccess = new \IPS\Api\GraphQL\Types\ModuleAccessType());
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\IDType
|
||||
*/
|
||||
public static function id()
|
||||
{
|
||||
return Type::id();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\StringType
|
||||
*/
|
||||
public static function string()
|
||||
{
|
||||
return Type::string();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\IntType
|
||||
*/
|
||||
public static function int()
|
||||
{
|
||||
return Type::int();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\FloatType
|
||||
*/
|
||||
public static function float()
|
||||
{
|
||||
return Type::float();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\BooleanType
|
||||
*/
|
||||
public static function boolean()
|
||||
{
|
||||
return Type::boolean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\ListOfType
|
||||
*/
|
||||
public static function listOf($type)
|
||||
{
|
||||
return new ListOfType($type);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return \GraphQL\Type\Definition\EnumType
|
||||
*/
|
||||
public static function eNum($config)
|
||||
{
|
||||
return new EnumType($config);
|
||||
}
|
||||
|
||||
public static function inputObjectType($config)
|
||||
{
|
||||
return new InputObjectType($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Type $type
|
||||
* @return NonNull
|
||||
*/
|
||||
public static function nonNull($type)
|
||||
{
|
||||
return new NonNull($type);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: Follow type defintiion
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 3 Sept 2018
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* FollowType for GraphQL API
|
||||
*/
|
||||
class _FollowType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get root type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'Follow',
|
||||
'description' => 'Returns the follow data for a node, item or member',
|
||||
'fields' => [
|
||||
'id' => [
|
||||
'type' => TypeRegistry::id(),
|
||||
'resolve' => function ($followData) {
|
||||
// This field allows Apollo to more effectively cache results. We don't include
|
||||
// the member ID in the MD5 here though because it will change when logging in.
|
||||
return md5( $followData['app'] . ';' . $followData['area'] . ';' . $followData['id'] );
|
||||
}
|
||||
],
|
||||
'followID' => [
|
||||
'type' => TypeRegistry::id(),
|
||||
'resolve' => function ($followData) {
|
||||
return md5( $followData['app'] . ';' . $followData['area'] . ';' . $followData['id'] . ';' . \IPS\Member::loggedIn()->member_id );
|
||||
}
|
||||
],
|
||||
'isFollowing' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($followData) {
|
||||
return \IPS\Member::loggedIn()->following( $followData['app'], $followData['area'], $followData['id'] );
|
||||
}
|
||||
],
|
||||
'followType' => [
|
||||
'type' => TypeRegistry::eNum([
|
||||
'name' => 'core_Follow_followType',
|
||||
'values' => ['NOT_FOLLOWING', 'PUBLIC', 'ANONYMOUS']
|
||||
]),
|
||||
'resolve' => function ($followData) {
|
||||
if( !\IPS\Member::loggedIn()->following( $followData['app'], $followData['area'], $followData['id'] ) )
|
||||
{
|
||||
return 'NOT_FOLLOWING';
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$current = \IPS\Db::i()->select( 'follow_is_anon', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=? AND follow_member_id=?', $followData['app'], $followData['area'], $followData['id'], \IPS\Member::loggedIn()->member_id ), NULL, array(), NULL, NULL )->first();
|
||||
|
||||
return $current['follow_is_anon'] ? 'ANONYMOUS' : 'PUBLIC';
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
return 'NOT_FOLLOWING';
|
||||
}
|
||||
}
|
||||
],
|
||||
'followCount' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'resolve' => function ($followData) {
|
||||
if( isset( $followData['node'] ) && isset( $followData['nodeClass'] ) )
|
||||
{
|
||||
$nodeClass = $followData['nodeClass'];
|
||||
return $nodeClass::$contentItemClass::containerFollowerCount( $followData['node'] );
|
||||
}
|
||||
elseif( isset( $followData['member'] ) )
|
||||
{
|
||||
return $followData['member']->followersCount();
|
||||
}
|
||||
else
|
||||
{
|
||||
return $followData['item']->followersCount();
|
||||
}
|
||||
}
|
||||
],
|
||||
'anonFollowCount' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'resolve' => function ($followData) {
|
||||
if( isset( $followData['node'] ) && isset( $followData['nodeClass'] ) )
|
||||
{
|
||||
$nodeClass = $followData['nodeClass'];
|
||||
return $nodeClass::$contentItemClass::containerFollowerCount( $followData['node'], $nodeClass::$contentItemClass::FOLLOW_ANONYMOUS );
|
||||
}
|
||||
elseif( isset( $followData['member'] ) )
|
||||
{
|
||||
return $followData['member']->followersCount( \IPS\Member::FOLLOW_ANONYMOUS );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $followData['item']->followersCount( $followData['itemClass']::FOLLOW_ANONYMOUS );
|
||||
}
|
||||
}
|
||||
],
|
||||
'followers' => [
|
||||
'type' => TypeRegistry::listOf( \IPS\core\api\GraphQL\TypeRegistry::member() ),
|
||||
'args' => [
|
||||
'offset' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'defaultValue' => 0
|
||||
],
|
||||
'limit' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'defaultValue' => 25
|
||||
]
|
||||
],
|
||||
'resolve' => function ($followData, $args) {
|
||||
$offset = max( $args['offset'], 0 );
|
||||
$limit = min( $args['limit'], 50 );
|
||||
|
||||
if( isset( $followData['node'] ) && isset( $followData['nodeClass'] ) )
|
||||
{
|
||||
$nodeClass = $followData['nodeClass'];
|
||||
$_followers = $nodeClass::$contentItemClass::containerFollowers( $followData['node'], $nodeClass::$contentItemClass::FOLLOW_PUBLIC, array( 'none', 'immediate', 'daily', 'weekly' ), NULL, array( $offset, $limit ), 'name' );
|
||||
}
|
||||
elseif( isset( $followData['member'] ) )
|
||||
{
|
||||
$_followers = $followData['member']->followers( \IPS\Member::FOLLOW_PUBLIC, array( 'none', 'immediate', 'daily', 'weekly' ), NULL, array( $offset, $limit ), 'name' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$itemClass = $followData['itemClass'];
|
||||
$_followers = $followData['item']->followers( $itemClass::FOLLOW_PUBLIC, array( 'none', 'immediate', 'daily', 'weekly' ), NULL, array( $offset, $limit ), 'name' );
|
||||
}
|
||||
|
||||
if( $_followers === NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$followers = array();
|
||||
|
||||
foreach( $_followers as $followerRow )
|
||||
{
|
||||
$followers[] = \IPS\Member::load( $followerRow['follow_member_id'] );
|
||||
}
|
||||
|
||||
return $followers;
|
||||
}
|
||||
],
|
||||
// @todo this will need to be extensively modified when I add support for following clubs
|
||||
'followOptions' => [
|
||||
'type' => TypeRegistry::listOf( new ObjectType([
|
||||
'name' => 'core_Follow_Options',
|
||||
'fields' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'selected' => TypeRegistry::boolean(),
|
||||
'disabled' => TypeRegistry::boolean()
|
||||
],
|
||||
'resolveField' => function ($followOption, $args, $context, $info) {
|
||||
return $followOption[ $info->fieldName ];
|
||||
}
|
||||
]) ),
|
||||
'resolve' => function ($followData, $args) {
|
||||
// No options if we're a guest
|
||||
if( !\IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
return array();
|
||||
}
|
||||
|
||||
/* Do we follow it? */
|
||||
try
|
||||
{
|
||||
$current = \IPS\Db::i()->select( '*', 'core_follow', array( 'follow_app=? AND follow_area=? AND follow_rel_id=? AND follow_member_id=?', $followData['app'], $followData['area'], $followData['id'], \IPS\Member::loggedIn()->member_id ), NULL, array(), NULL, NULL )->first();
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$current = FALSE;
|
||||
}
|
||||
|
||||
// What kind of following are we doing?
|
||||
if( isset( $followData['node'] ) )
|
||||
{
|
||||
$followType = 'new_content';
|
||||
}
|
||||
elseif( isset( $followData['member'] ) )
|
||||
{
|
||||
$followType = 'follower_content';
|
||||
}
|
||||
else
|
||||
{
|
||||
$followType = 'new_comment';
|
||||
}
|
||||
|
||||
$notificationConfiguration = \IPS\Member::loggedIn()->notificationsConfiguration();
|
||||
$notificationConfiguration = isset( $notificationConfiguration[ $followType ] ) ? $notificationConfiguration[ $followType ] : array();
|
||||
|
||||
// Now figure out which options we can have
|
||||
$options = array( array(
|
||||
'type' => 'immediate',
|
||||
'disabled' => empty( $notificationConfiguration ), // If notification configuration is empty, show but disable 'immediate'
|
||||
'selected' => ( $current && $current['follow_notify_freq'] === 'immediate' )
|
||||
) );
|
||||
|
||||
if( !isset( $followData['member'] ) )
|
||||
{
|
||||
$options[] = array('type' => 'weekly', 'disabled' => false, 'selected' => ( $current && $current['follow_notify_freq'] === 'weekly' ) );
|
||||
$options[] = array('type' => 'daily', 'disabled' => false, 'selected' => ( $current && $current['follow_notify_freq'] === 'daily' ) );
|
||||
$options[] = array('type' => 'none', 'disabled' => false, 'selected' => ( $current && $current['follow_notify_freq'] === 'none' ) );
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
parent::__construct( $config );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: Image Type
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 22 May 2019
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ImageType for GraphQL API
|
||||
*/
|
||||
class _ImageType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get object type
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'Image',
|
||||
'description' => 'Represents an image with width/height data',
|
||||
'fields' => function () {
|
||||
return [
|
||||
'url' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "The URL to the image",
|
||||
],
|
||||
'width' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'description' => "The width in pixels",
|
||||
],
|
||||
'height' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'description' => "The height in pixels",
|
||||
],
|
||||
];
|
||||
},
|
||||
'resolveField' => function ($data, $args, $context, $info) {
|
||||
return $data[ $info->fieldName ];
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: Item State Type
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 21 Jun 2018
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ItemStateType for GraphQL API
|
||||
*/
|
||||
class _ItemStateType extends InputObjectType
|
||||
{
|
||||
/**
|
||||
* Get object type
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'ItemState',
|
||||
'fields' => [
|
||||
'locked' => TypeRegistry::boolean(),
|
||||
'featured' => TypeRegistry::boolean(),
|
||||
'pinned' => TypeRegistry::boolean(),
|
||||
'hidden' => TypeRegistry::eNum([
|
||||
'name' => 'hiddenState',
|
||||
'values' => [
|
||||
'visible' => 0,
|
||||
'unapproved' => -1,
|
||||
'hidden' => 1
|
||||
]
|
||||
])
|
||||
]
|
||||
];
|
||||
|
||||
parent::__construct( $config );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: ModuleAccessType
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 15 Oct 2019
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ModuleAccessType for GraphQL API
|
||||
*/
|
||||
class _ModuleAccessType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get object type
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => "ModuleAccess",
|
||||
'description' => "Returns permissions for modules in the platform",
|
||||
'fields' => function () {
|
||||
return [
|
||||
'view' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'args' => [
|
||||
'module' => TypeRegistry::string()
|
||||
],
|
||||
'description' => "Returns the member's access to this module",
|
||||
'resolve' => function( $app, $args, $context, $info ) {
|
||||
$canAccess = FALSE;
|
||||
try
|
||||
{
|
||||
$canAccess = \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( $app, $args['module'], 'front' ) );
|
||||
}
|
||||
catch (\Exception $e)
|
||||
{
|
||||
// Just fall through so we return false
|
||||
}
|
||||
|
||||
return $canAccess;
|
||||
}
|
||||
]
|
||||
];
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief API Exception
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Exception
|
||||
*/
|
||||
class _MutationType extends ObjectType
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'Mutation',
|
||||
'fields' => function () {
|
||||
$_fields = array();
|
||||
|
||||
foreach( \IPS\Application::enabledApplications() as $key => $app )
|
||||
{
|
||||
try
|
||||
{
|
||||
$appMutationClass = "\\IPS\\" . $app->directory . "\\api\\GraphQL\\Mutation";
|
||||
|
||||
if( !class_exists( $appMutationClass ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$mutationFields = array();
|
||||
$mutationType = $appMutationClass::mutations();
|
||||
|
||||
foreach( $mutationType as $type => $mutationClass )
|
||||
{
|
||||
$mutationFields[ $type ] = array(
|
||||
'type' => $mutationClass->type(),
|
||||
'description' => $mutationClass::$description,
|
||||
'args' => $mutationClass->args()
|
||||
);
|
||||
}
|
||||
|
||||
$_fields[ 'mutate' . ucfirst($app->directory) ] = new \GraphQL\Type\Definition\ObjectType( array(
|
||||
'name' => 'mutate_' . ucfirst($app->directory),
|
||||
'fields' => $mutationFields
|
||||
) );
|
||||
}
|
||||
catch ( \Exception $err )
|
||||
{ }
|
||||
}
|
||||
|
||||
return $_fields;
|
||||
},
|
||||
'resolveField' => function ($val, $args, $context, $info) {
|
||||
$name = str_replace( 'mutate', '', $info->fieldName );
|
||||
|
||||
try
|
||||
{
|
||||
$appMutationClass = '\\IPS\\' . $name . '\\api\\GraphQL\\Mutation';
|
||||
$mutationResolvers = array();
|
||||
$mutationType = $appMutationClass::mutations();
|
||||
|
||||
foreach( $mutationType as $type => $mutationClass )
|
||||
{
|
||||
$mutationResolvers[ $type ] = function($val, $args, $context, $info) use ($mutationClass)
|
||||
{
|
||||
return $mutationClass->resolve($val, $args, $context, $info);
|
||||
};
|
||||
}
|
||||
|
||||
return $mutationResolvers;
|
||||
|
||||
$mutationClass = new $className;
|
||||
return $mutationClass->resolve($val, $args, $context, $info);
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'NO_RESOLVE', '3S291/2', 405 );
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct( $config );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief API Exception
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 3 Dec 2015
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* API Exception
|
||||
*/
|
||||
class _QueryType extends ObjectType
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'Query',
|
||||
'fields' => function () {
|
||||
$_fields = array();
|
||||
|
||||
foreach( \IPS\Application::enabledApplications() as $key => $app )
|
||||
{
|
||||
try
|
||||
{
|
||||
$globalAppQueries = \IPS\Api\GraphQL\AppQueries::queries($app->directory);
|
||||
$appQueryClass = "\\IPS\\" . $app->directory . "\\api\\GraphQL\\Query";
|
||||
|
||||
if( !class_exists( $appQueryClass ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$queryFields = array();
|
||||
$queryTypes = $appQueryClass::queries();
|
||||
|
||||
// Add the queries that *every* app receives
|
||||
foreach( $globalAppQueries as $globalType => $globalQueryClass )
|
||||
{
|
||||
$queryFields[ $globalType ] = array(
|
||||
'type' => $globalQueryClass->type(),
|
||||
'description' => $globalQueryClass::$description,
|
||||
'args' => $globalQueryClass->args()
|
||||
);
|
||||
}
|
||||
|
||||
// Add each app's individual queries
|
||||
foreach( $queryTypes as $type => $queryClass )
|
||||
{
|
||||
$queryFields[ $type ] = array(
|
||||
'type' => $queryClass->type(),
|
||||
'description' => $queryClass::$description,
|
||||
'args' => $queryClass->args()
|
||||
);
|
||||
}
|
||||
|
||||
$_fields[ $app->directory ] = new \GraphQL\Type\Definition\ObjectType( array(
|
||||
'name' => $app->directory,
|
||||
'fields' => $queryFields
|
||||
) );
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
print_r( $e );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
return $_fields;
|
||||
},
|
||||
'resolveField' => function ($val, $args, $context, $info) {
|
||||
try
|
||||
{
|
||||
$globalAppQueries = \IPS\Api\GraphQL\AppQueries::queries($info->fieldName);
|
||||
$appQueryClass = '\\IPS\\' . $info->fieldName . '\\api\\GraphQL\\Query';
|
||||
$queryResolvers = array();
|
||||
$queryTypes = $appQueryClass::queries();
|
||||
|
||||
// Resolve the global app queries that every app receives
|
||||
foreach( $globalAppQueries as $globalType => $globalQueryClass )
|
||||
{
|
||||
$queryResolvers[ $globalType ] = function ($val, $args, $context, $info) use ($globalQueryClass)
|
||||
{
|
||||
return $globalQueryClass->resolve( $val, $args, $context, $info);
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve the individual per-app queries
|
||||
foreach( $queryTypes as $type => $queryClass )
|
||||
{
|
||||
$queryResolvers[ $type ] = function($val, $args, $context, $info) use ($queryClass)
|
||||
{
|
||||
return $queryClass->resolve($val, $args, $context, $info);
|
||||
};
|
||||
}
|
||||
|
||||
return $queryResolvers;
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'NO_RESOLVE', '3S291/2', 405 );
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct( $config );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: Reputation type defintiion
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 10 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* ReputationType for GraphQL API
|
||||
*/
|
||||
class _ReputationType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get root type
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'Reputation',
|
||||
'description' => 'Returns the reputation',
|
||||
'fields' => [
|
||||
'reactionCount' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'resolve' => function ($content, $args) {
|
||||
if( !\IPS\Settings::i()->reputation_enabled )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $content->reactionCount();
|
||||
}
|
||||
],
|
||||
'canViewReps' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($content, $args) {
|
||||
return \IPS\Settings::i()->reputation_enabled && \IPS\Member::loggedIn()->group['gbw_view_reps'];
|
||||
}
|
||||
],
|
||||
'canReact' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($content, $args) {
|
||||
return \IPS\Settings::i()->reputation_enabled && $content->canReact();
|
||||
}
|
||||
],
|
||||
'hasReacted' => [
|
||||
'type' => TypeRegistry::boolean()
|
||||
],
|
||||
'isLikeMode' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($content, $args) {
|
||||
return \IPS\Settings::i()->reputation_enabled && \IPS\Content\Reaction::isLikeMode();
|
||||
}
|
||||
],
|
||||
'givenReaction' => [
|
||||
'type' => \IPS\core\api\GraphQL\TypeRegistry::contentReaction()
|
||||
],
|
||||
'defaultReaction' => [
|
||||
'type' => \IPS\core\api\GraphQL\TypeRegistry::contentReaction(),
|
||||
],
|
||||
'availableReactions' => [
|
||||
'type' => TypeRegistry::listOf( \IPS\core\api\GraphQL\TypeRegistry::contentReaction() )
|
||||
],
|
||||
'reactions' => [
|
||||
'type' => TypeRegistry::listOf( \IPS\core\api\GraphQL\TypeRegistry::contentReaction() ),
|
||||
'resolve' => function ($content) {
|
||||
if( !\IPS\Settings::i()->reputation_enabled )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$return = array();
|
||||
$idColumn = $content::$databaseColumnId;
|
||||
|
||||
foreach( $content->reactBlurb() as $key => $count )
|
||||
{
|
||||
$return[] = array( 'id' => md5( \get_class( $content ) . '-' . $content->$idColumn ) . '-' . $key, 'reactionId' => $key, 'count' => $count );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
],
|
||||
'whoReacted' => [
|
||||
'type' => TypeRegistry::listOf( \IPS\core\api\GraphQL\TypeRegistry::member() ),
|
||||
'args' => [
|
||||
'id' => TypeRegistry::int(),
|
||||
'offset' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'defaultValue' => 0
|
||||
],
|
||||
'limit' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'defaultValue' => 25
|
||||
]
|
||||
],
|
||||
'resolve' => function ($content, $args) {
|
||||
if( !\IPS\Settings::i()->reputation_enabled || !\IPS\Member::loggedIn()->group['gbw_view_reps'] )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$reaction = NULL;
|
||||
$members = array();
|
||||
$offset = max( $args['offset'], 0 );
|
||||
$limit = min( $args['limit'], 50 );
|
||||
|
||||
if( isset( $args['id'] ) )
|
||||
{
|
||||
$reaction = \intval( $args['id']);
|
||||
}
|
||||
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_reputation_index', $content->getReactionWhereClause( $reaction ), 'rep_date desc', array( $offset, $limit ) )->join( 'core_reactions', 'reaction=reaction_id' ) AS $reaction )
|
||||
{
|
||||
$members[] = \IPS\Member::load( $reaction['member_id'] );
|
||||
}
|
||||
|
||||
return $members;
|
||||
}
|
||||
]
|
||||
],
|
||||
'resolveField' => function ($content, $args, $context, $info) {
|
||||
if( !\IPS\Settings::i()->reputation_enabled )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
$defaultReaction = reset( $enabledReactions );
|
||||
$reacted = $content->reacted();
|
||||
|
||||
switch( $info->fieldName )
|
||||
{
|
||||
case 'hasReacted':
|
||||
return ( $reacted and isset( $enabledReactions[ $reacted->id ] ) );
|
||||
break;
|
||||
case 'givenReaction':
|
||||
if( !$reacted )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$reaction = isset( $enabledReactions[ $reacted->id ] ) ? $enabledReactions[ $reacted->id ] : $defaultReaction;
|
||||
return array(
|
||||
'id' => $reaction->id,
|
||||
'reaction' => $reaction
|
||||
);
|
||||
break;
|
||||
case 'defaultReaction':
|
||||
return array(
|
||||
'id' => $defaultReaction->id,
|
||||
'reaction' => $defaultReaction
|
||||
);
|
||||
break;
|
||||
case 'availableReactions':
|
||||
$reactions = array();
|
||||
|
||||
foreach( $enabledReactions as $reaction )
|
||||
{
|
||||
$reactions[] = array(
|
||||
'id' => $reaction->id,
|
||||
'reaction' => $reaction
|
||||
);
|
||||
}
|
||||
|
||||
return $reactions;
|
||||
break;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct( $config );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: RichText Type
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 7 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* RichTextType for GraphQL API
|
||||
*/
|
||||
class _RichTextType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get object type
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'core_RichText',
|
||||
'description' => 'Represents an internal URL',
|
||||
'fields' => [
|
||||
'plain' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "Returns plain-text version of the text",
|
||||
'args' => [
|
||||
'singleLine' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'description' => "Should linebreaks be removed?",
|
||||
'defaultValue' => TRUE
|
||||
],
|
||||
'truncateLength' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'description' => 'Characters to truncate on. Only available if stripped option is true.',
|
||||
'defaultValue' => 0
|
||||
]
|
||||
],
|
||||
'resolve' => function ($string, $args) {
|
||||
|
||||
/* Remove stuff we don't want to include (quotes, spoilers, scripts) */
|
||||
$string = \IPS\Text\Parser::removeElements( $string, array( 'blockquote', 'script', 'div[class=ipsSpoiler]' ) );
|
||||
|
||||
/* Put a break in places where we actually want one */
|
||||
$string = str_replace( array( '</p>', '</h1>', '</h2>', '</h3>', '</h4>', '</h5>', '</h6>', '</li>' ), '<br>', $string );
|
||||
|
||||
/* Strip all other tags */
|
||||
$string = strip_tags( $string, '<br>' );
|
||||
$string = html_entity_decode( $string );
|
||||
|
||||
/* Fix linebreaks */
|
||||
$string = preg_replace( "/\r|\n|\t/", '', $string );
|
||||
$string = str_replace( '<br>', $args['singleLine'] ? ' ' : "\n", $string );
|
||||
|
||||
/* Trim whitespace */
|
||||
$string = trim( $string );
|
||||
|
||||
/* Truncate */
|
||||
if( $args['truncateLength'] )
|
||||
{
|
||||
$string = mb_substr( $string, 0, $args['truncateLength'] );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return $string;
|
||||
}
|
||||
],
|
||||
'original' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "Returns original rich text (i.e. containing full rich markup)",
|
||||
'args' => [
|
||||
'removeLazyLoad' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'description' => "Remove lazy-load placeholders?",
|
||||
'defaultValue' => TRUE
|
||||
]
|
||||
],
|
||||
'resolve' => function ($string, $args) {
|
||||
if( $args['removeLazyLoad'] )
|
||||
{
|
||||
$string = \IPS\Text\Parser::removeLazyLoad( $string );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->parseFileObjectUrls( $string );
|
||||
return $string;
|
||||
}
|
||||
]
|
||||
]
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief GraphQL: URL Type
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 7 May 2017
|
||||
* @version SVN_VERSION_NUMBER
|
||||
*/
|
||||
|
||||
namespace IPS\Api\GraphQL\Types;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* URLType for GraphQL API
|
||||
*/
|
||||
class _UrlType extends ObjectType
|
||||
{
|
||||
/**
|
||||
* Get object type
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$config = [
|
||||
'name' => 'URL',
|
||||
'description' => 'Represents an internal URL',
|
||||
'fields' => function () {
|
||||
return [
|
||||
'full' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "The full URL",
|
||||
'resolve' => function ($url, $args, $context) {
|
||||
return (string) $url;
|
||||
}
|
||||
],
|
||||
'app' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "Application",
|
||||
],
|
||||
'module' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "Module",
|
||||
],
|
||||
'controller' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'description' => "Controller",
|
||||
],
|
||||
'query' => [
|
||||
'type' => TypeRegistry::listOf( new ObjectType([
|
||||
'name' => 'URL_query',
|
||||
'fields' => [
|
||||
'key' => TypeRegistry::string(),
|
||||
'value' => TypeRegistry::string()
|
||||
],
|
||||
'resolveField' => function ($query, $args, $context, $info) {
|
||||
return $query[ $info->fieldName ];
|
||||
}
|
||||
])),
|
||||
'resolve' => function ($url) {
|
||||
if( !( $url instanceof \IPS\Http\Url ) )
|
||||
{
|
||||
$url = \IPS\Http\Url::createFromString( $url );
|
||||
}
|
||||
|
||||
$pieces = array();
|
||||
|
||||
foreach( $url->queryString as $key => $value )
|
||||
{
|
||||
$pieces[] = array(
|
||||
'key' => $key,
|
||||
'value' => $value
|
||||
);
|
||||
}
|
||||
|
||||
return $pieces;
|
||||
}
|
||||
]
|
||||
];
|
||||
},
|
||||
'resolveField' => function ($url, $args, $context, $info) {
|
||||
if( !( $url instanceof \IPS\Http\Url ) )
|
||||
{
|
||||
$url = \IPS\Http\Url::createFromString( $url );
|
||||
}
|
||||
|
||||
$urlPieces = $url->hiddenQueryString;
|
||||
if( \in_array( $info->fieldName, array('app', 'module', 'controller') ) )
|
||||
{
|
||||
return $urlPieces[ $info->fieldName ];
|
||||
}
|
||||
|
||||
return (string) $url;
|
||||
}
|
||||
];
|
||||
|
||||
parent::__construct($config);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -11,7 +11,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
@@ -128,7 +128,7 @@ class _Key extends \IPS\Node\Model
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'api_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? "core_api_name_{$this->id}" : NULL ) ) ) );
|
||||
|
||||
/* Allowed IPs */
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'api_enable_ip_restriction', intval( $this->allowed_ips !== NULL ), FALSE, array(
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'api_enable_ip_restriction', \intval( $this->allowed_ips !== NULL ), FALSE, array(
|
||||
'options' => array(
|
||||
0 => 'api_enable_ip_restriction_off',
|
||||
1 => 'api_enable_ip_restriction_on',
|
||||
@@ -147,7 +147,7 @@ class _Key extends \IPS\Node\Model
|
||||
{
|
||||
if ( $val )
|
||||
{
|
||||
foreach ( array_filter( is_array( $val ) ? $val : array( $val ) ) as $ip )
|
||||
foreach ( array_filter( \is_array( $val ) ? $val : array( $val ) ) as $ip )
|
||||
{
|
||||
if ( filter_var( $ip, FILTER_VALIDATE_IP ) === FALSE )
|
||||
{
|
||||
|
||||
+292
-165
@@ -12,7 +12,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
@@ -120,158 +120,168 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
$form->addTab('oauth_basic_settings');
|
||||
$form->addHeader('oauth_basic_settings');
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'oauth_client_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->client_id ? "core_oauth_client_{$this->client_id}" : NULL ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_client_type', $type, TRUE, array(
|
||||
'options' => array(
|
||||
'invision' => 'client_type_invision',
|
||||
'wordpress' => 'client_type_wordpress',
|
||||
'confidential' => 'client_type_confidential',
|
||||
'public' => 'client_type_public',
|
||||
),
|
||||
'toggles' => array(
|
||||
'invision' => array( 'oauth_grant_types_invision', 'oauth_invision_endpoint' ),
|
||||
'wordpress' => array( 'oauth_wordpress_endpoint' ),
|
||||
'confidential' => array( 'oauth_grant_types_confidential', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
|
||||
'public' => array( 'oauth_grant_types_public', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
|
||||
)
|
||||
) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_invision_grant_type', $this->client_id ? $this->grant_types : 'authorization_code', NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'invision_grant_type_server_authorization_code',
|
||||
'password' => 'invision_grant_type_server_password',
|
||||
),
|
||||
), NULL, NULL, NULL, 'oauth_grant_types_invision' ) );
|
||||
$confidentialGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_confidential', $this->client_id ? explode( ',', $this->grant_types ) : array( 'authorization_code' ), NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'grant_type_authorization_code',
|
||||
'implicit' => 'grant_type_implicit',
|
||||
'password' => 'grant_type_password',
|
||||
'client_credentials' => 'grant_type_client_credentials'
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'oauth_use_refresh_tokens' )
|
||||
)
|
||||
), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type === 'confidential' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_grant_types_confidential' );
|
||||
$confidentialGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
|
||||
$form->add( $confidentialGrant );
|
||||
$publicGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_public', $this->client_id ? explode( ',', $this->grant_types ) : array( 'implicit' ), NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'grant_type_authorization_code',
|
||||
'implicit' => 'grant_type_implicit',
|
||||
'password' => 'grant_type_password',
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'oauth_use_refresh_tokens' )
|
||||
)
|
||||
), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type === 'public' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_grant_types_public' );
|
||||
$publicGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
|
||||
$form->add( $publicGrant );
|
||||
$redirectUris = json_decode( $this->redirect_uris, TRUE );
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'oauth_invision_endpoint', isset( $redirectUris[0] ) ? preg_replace( '#/oauth/callback/$#i', '/', $redirectUris[0] ) : NULL, NULL, array( 'placeholder' => 'https://othercommunity.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type == 'invision' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
|
||||
throw new \DomainException('oauth_redirect_uris_no_fragment');
|
||||
}
|
||||
if ( $val and rtrim( (string) $val, '/' ) === rtrim( \IPS\Settings::i()->base_url, '/' ) ) {
|
||||
throw new \DomainException('oauth_invision_endpoint_internal');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_invision_endpoint' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'oauth_wordpress_endpoint', isset( $redirectUris[0] ) ? preg_replace( '#/oauthcallback/$#i', '', $redirectUris[0] ) : NULL, NULL, array( 'placeholder' => 'https://wordpress.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type == 'wordpress' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
|
||||
throw new \DomainException('oauth_redirect_uris_no_fragment');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_wordpress_endpoint' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Stack( 'oauth_redirect_uris', $redirectUris, NULL, array( 'stackFieldType' => 'Url', 'placeholder' => 'https://www.example.com/redirect_uri', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !in_array( \IPS\Request::i()->oauth_client_type, array('invision', 'wordpress') ) ) {
|
||||
$chosenGrantTypes = \IPS\Request::i()->oauth_client_type === 'public' ? \IPS\Request::i()->oauth_grant_types_public : \IPS\Request::i()->oauth_grant_types_confidential;
|
||||
if ( !$val and ( isset( $chosenGrantTypes['authorization_code'] ) or isset( $chosenGrantTypes['implicit'] ) ) ) {
|
||||
if ( $type !== 'mobile' )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_client_type', $type, TRUE, array(
|
||||
'options' => array(
|
||||
'invision' => 'client_type_invision',
|
||||
'wordpress' => 'client_type_wordpress',
|
||||
'confidential' => 'client_type_confidential',
|
||||
'public' => 'client_type_public',
|
||||
),
|
||||
'toggles' => array(
|
||||
'invision' => array( 'oauth_grant_types_invision', 'oauth_invision_endpoint' ),
|
||||
'wordpress' => array( 'oauth_wordpress_endpoint' ),
|
||||
'confidential' => array( 'oauth_grant_types_confidential', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
|
||||
'public' => array( 'oauth_grant_types_public', 'oauth_redirect_uris', 'oauth_choose_scopes', 'oauth_header_oauth_access_tokens', 'oauth_access_token_length', 'oauth_tab_oauth_scopes' ),
|
||||
)
|
||||
) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_invision_grant_type', $this->client_id ? $this->grant_types : 'authorization_code', NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'invision_grant_type_server_authorization_code',
|
||||
'password' => 'invision_grant_type_server_password',
|
||||
),
|
||||
), NULL, NULL, NULL, 'oauth_grant_types_invision' ) );
|
||||
$confidentialGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_confidential', $this->client_id ? explode( ',', $this->grant_types ) : array( 'authorization_code' ), NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'grant_type_authorization_code',
|
||||
'implicit' => 'grant_type_implicit',
|
||||
'password' => 'grant_type_password',
|
||||
'client_credentials' => 'grant_type_client_credentials'
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
|
||||
)
|
||||
), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type === 'confidential' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_grant_types_confidential' );
|
||||
$confidentialGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
|
||||
$form->add( $confidentialGrant );
|
||||
$publicGrant = new \IPS\Helpers\Form\CheckboxSet( 'oauth_grant_types_public', $this->client_id ? explode( ',', $this->grant_types ) : array( 'implicit' ), NULL, array(
|
||||
'options' => array(
|
||||
'authorization_code' => 'grant_type_authorization_code',
|
||||
'implicit' => 'grant_type_implicit',
|
||||
'password' => 'grant_type_password',
|
||||
),
|
||||
'toggles' => array(
|
||||
'authorization_code' => array( 'oauth_pkce', 'oauth_use_refresh_tokens' )
|
||||
)
|
||||
), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type === 'public' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_grant_types_public' );
|
||||
$publicGrant->label = \IPS\Member::loggedIn()->language()->addToStack('oauth_grant_types');
|
||||
$form->add( $publicGrant );
|
||||
$redirectUris = json_decode( $this->redirect_uris, TRUE );
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'oauth_invision_endpoint', isset( $redirectUris[0] ) ? preg_replace( '#/oauth/callback/$#i', '/', $redirectUris[0] ) : NULL, NULL, array( 'placeholder' => 'https://othercommunity.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type == 'invision' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
|
||||
throw new \DomainException('oauth_redirect_uris_no_fragment');
|
||||
}
|
||||
}
|
||||
}, NULL, NULL, 'oauth_redirect_uris' ) );
|
||||
if ( $val and rtrim( (string) $val, '/' ) === rtrim( \IPS\Settings::i()->base_url, '/' ) ) {
|
||||
throw new \DomainException('oauth_invision_endpoint_internal');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_invision_endpoint' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'oauth_wordpress_endpoint', isset( $redirectUris[0] ) ? $redirectUris[0] : NULL, NULL, array( 'placeholder' => 'https://wordpress.example.com/', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !$val and \IPS\Request::i()->oauth_client_type == 'wordpress' ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
|
||||
throw new \DomainException('oauth_redirect_uris_no_fragment');
|
||||
}
|
||||
}, NULL, NULL, 'oauth_wordpress_endpoint' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_pkce', $this->pkce ?: 'none', FALSE, array( 'options' => array( 'S256' => 'oauth_pkce_256', 'plain' => 'oauth_pkce_plain', 'none' => 'oauth_pkce_none' ) ), NULL, NULL, NULL, 'oauth_pkce' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Stack( 'oauth_redirect_uris', $redirectUris, NULL, array( 'stackFieldType' => 'Url', 'placeholder' => 'https://www.example.com/redirect_uri', 'allowedProtocols' => NULL ), function( $val ) {
|
||||
if ( !\in_array( \IPS\Request::i()->oauth_client_type, array('invision', 'wordpress') ) ) {
|
||||
$chosenGrantTypes = \IPS\Request::i()->oauth_client_type === 'public' ? \IPS\Request::i()->oauth_grant_types_public : \IPS\Request::i()->oauth_grant_types_confidential;
|
||||
if ( !$val and ( isset( $chosenGrantTypes['authorization_code'] ) or isset( $chosenGrantTypes['implicit'] ) ) ) {
|
||||
throw new \DomainException('form_required');
|
||||
}
|
||||
if ( $val and $val instanceof \IPS\Http\Url and $val->data[ \IPS\Http\Url::COMPONENT_FRAGMENT ] ) {
|
||||
throw new \DomainException('oauth_redirect_uris_no_fragment');
|
||||
}
|
||||
}
|
||||
}, NULL, NULL, 'oauth_redirect_uris' ) );
|
||||
}
|
||||
|
||||
$form->addHeader('oauth_authorization_screen');
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_prompt', $this->prompt, FALSE, array( 'options' => array( 'automatic' => 'oauth_prompt_automatic', 'reauthorize' => 'oauth_prompt_reauthorize', 'login' => 'oauth_prompt_login' ) ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_choose_scopes', $this->choose_scopes, FALSE, array(), NULL, NULL, NULL, 'oauth_choose_scopes' ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_ucp', $this->ucp, FALSE ) );
|
||||
$form->addHeader('oauth_access_tokens');
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'oauth_access_token_length', $this->access_token_length, NULL, array( 'unlimited' => 0, 'unlimitedLang' => 'never' ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('hours'), 'oauth_access_token_length' ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_use_refresh_tokens', $this->use_refresh_tokens, NULL, array( 'togglesOn' => array( 'oauth_refresh_token_length' ) ), NULL, NULL, NULL, 'oauth_use_refresh_tokens' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Number( 'oauth_refresh_token_length', $this->refresh_token_length, NULL, array( 'unlimited' => 0, 'unlimitedLang' => 'never' ), NULL, NULL, \IPS\Member::loggedIn()->language()->addToStack('days'), 'oauth_refresh_token_length' ) );
|
||||
|
||||
$form->addTab('oauth_scopes');
|
||||
$form->addMessage('oauth_scopes_blurb');
|
||||
$matrix = new \IPS\Helpers\Form\Matrix;
|
||||
$matrix->classes[] = 'cApiPermissionsMatrix';
|
||||
$matrix->langPrefix = 'oauth_scope_';
|
||||
$matrix->columns = array(
|
||||
'name' => function( $key, $value, $data )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Custom( $key, $value, FALSE, array(
|
||||
'getHtml' => function( $field )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'api' )->oauthScopeField( $field->name, $field->value );
|
||||
}
|
||||
) );
|
||||
},
|
||||
'endpoints' => function( $key, $value, $data )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Custom( $key, $value ?: array(), FALSE, array(
|
||||
'getHtml' => function( $field )
|
||||
{
|
||||
$endpoints = \IPS\Api\Controller::getAllEndpoints('member');
|
||||
foreach ( $endpoints as $key => $endpoint )
|
||||
{
|
||||
$pieces = explode('/', $key);
|
||||
$endpointTree[ $pieces[0] ][ $pieces[1] ][ $key ] = $endpoint;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'api' )->permissionsFieldHtml( $endpointTree, $field->name, $field->value );
|
||||
}
|
||||
) );
|
||||
}
|
||||
);
|
||||
if ( !$this->client_id )
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'oauth_prompt', $this->prompt, FALSE, array( 'options' => array( 'none' => 'oauth_prompt_none', 'automatic' => 'oauth_prompt_automatic', 'reauthorize' => 'oauth_prompt_reauthorize', 'login' => 'oauth_prompt_login' ) ) ) );
|
||||
if ( $type !== 'mobile' )
|
||||
{
|
||||
$matrix->rows[] = array(
|
||||
'name' => array( 'key' => 'profile', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_profile') ),
|
||||
'endpoints' => array(
|
||||
'core/me/GETindex' => array( 'access' => TRUE, 'log' => FALSE ),
|
||||
)
|
||||
);
|
||||
$matrix->rows[] = array(
|
||||
'name' => array( 'key' => 'email', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_email') ),
|
||||
'endpoints' => array(
|
||||
'core/me/GETitem' => array( 'access' => TRUE, 'log' => FALSE ),
|
||||
)
|
||||
);
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_choose_scopes', $this->choose_scopes, FALSE, array(), NULL, NULL, NULL, 'oauth_choose_scopes' ) );
|
||||
}
|
||||
elseif ( $this->scopes and $scopes = json_decode( $this->scopes, TRUE ) )
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_ucp', $this->ucp, FALSE ) );
|
||||
if ( $type !== 'mobile' )
|
||||
{
|
||||
foreach ( $scopes as $key => $data )
|
||||
$form->addHeader('oauth_access_tokens');
|
||||
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_access_token_length', $this->access_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::HOURS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_access_token_length' ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'oauth_use_refresh_tokens', $this->use_refresh_tokens, NULL, array( 'togglesOn' => array( 'oauth_refresh_token_length' ) ), NULL, NULL, NULL, 'oauth_use_refresh_tokens' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Interval( 'oauth_refresh_token_length', $this->refresh_token_length, NULL, array( 'valueAs' => \IPS\Helpers\Form\Interval::DAYS, 'unlimited' => 0, 'unlimitedLang' => 'forever' ), NULL, NULL, NULL, 'oauth_refresh_token_length' ) );
|
||||
|
||||
$form->addTab('oauth_scopes');
|
||||
$form->addMessage('oauth_scopes_blurb');
|
||||
$matrix = new \IPS\Helpers\Form\Matrix;
|
||||
$matrix->classes[] = 'cApiPermissionsMatrix';
|
||||
$matrix->langPrefix = 'oauth_scope_';
|
||||
$matrix->columns = array(
|
||||
'name' => function( $key, $value, $data )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Custom( $key, $value, FALSE, array(
|
||||
'getHtml' => function( $field )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'api' )->oauthScopeField( $field->name, $field->value );
|
||||
}
|
||||
) );
|
||||
},
|
||||
'endpoints' => function( $key, $value, $data )
|
||||
{
|
||||
return new \IPS\Helpers\Form\Custom( $key, $value ?: array(), FALSE, array(
|
||||
'getHtml' => function( $field )
|
||||
{
|
||||
$endpoints = \IPS\Api\Controller::getAllEndpoints();
|
||||
foreach ( $endpoints as $key => $endpoint )
|
||||
{
|
||||
$pieces = explode('/', $key);
|
||||
$endpointTree[ $pieces[0] ][ $pieces[1] ][ $key ] = $endpoint;
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'api' )->permissionsFieldHtml( $endpointTree, $field->name, $field->value );
|
||||
}
|
||||
) );
|
||||
}
|
||||
);
|
||||
if ( !$this->client_id )
|
||||
{
|
||||
$matrix->rows[] = array(
|
||||
'name' => array( 'key' => $key, 'desc' => $data['description'] ),
|
||||
'endpoints' => $data['endpoints']
|
||||
'name' => array( 'key' => 'profile', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_profile') ),
|
||||
'endpoints' => array(
|
||||
'core/me/GETindex' => array( 'access' => TRUE, 'log' => FALSE ),
|
||||
)
|
||||
);
|
||||
$matrix->rows[] = array(
|
||||
'name' => array( 'key' => 'email', 'desc' => \IPS\Member::loggedIn()->language()->get('oauth_default_scope_email') ),
|
||||
'endpoints' => array(
|
||||
'core/me/GETitem' => array( 'access' => TRUE, 'log' => FALSE ),
|
||||
)
|
||||
);
|
||||
}
|
||||
elseif ( $this->scopes and $scopes = json_decode( $this->scopes, TRUE ) )
|
||||
{
|
||||
foreach ( $scopes as $key => $data )
|
||||
{
|
||||
$matrix->rows[] = array(
|
||||
'name' => array( 'key' => $key, 'desc' => $data['description'] ),
|
||||
'endpoints' => $data['endpoints']
|
||||
);
|
||||
}
|
||||
}
|
||||
$form->addMatrix( 'scopes', $matrix );
|
||||
}
|
||||
$form->addMatrix( 'scopes', $matrix );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -288,8 +298,21 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
/* Normalise the settings */
|
||||
$originalClientType = $values['oauth_client_type'];
|
||||
if ( $values['oauth_client_type'] === 'invision' )
|
||||
$originalClientType = isset( $values['oauth_client_type'] ) ? $values['oauth_client_type'] : $this->type;
|
||||
if ( $this->type === 'mobile' )
|
||||
{
|
||||
$values['oauth_client_type'] = 'mobile';
|
||||
|
||||
foreach ( static::mobileAppValues() as $k => $v )
|
||||
{
|
||||
$values["oauth_{$k}"] = $v;
|
||||
}
|
||||
|
||||
$values['oauth_grant_types_public'] = $values['oauth_grant_types'];
|
||||
$values['scopes'] = $values['oauth_scopes'];
|
||||
$values['oauth_pkce'] = 'S256';
|
||||
}
|
||||
elseif ( $values['oauth_client_type'] === 'invision' )
|
||||
{
|
||||
$values['oauth_client_type'] = 'confidential';
|
||||
$values['oauth_grant_types_confidential'] = array( $values['oauth_invision_grant_type'] );
|
||||
@@ -313,12 +336,13 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
)
|
||||
);
|
||||
$values['oauth_type'] = 'invision';
|
||||
$values['oauth_pkce'] = 'none';
|
||||
}
|
||||
elseif ( $values['oauth_client_type'] === 'wordpress' )
|
||||
{
|
||||
$values['oauth_client_type'] = 'confidential';
|
||||
$values['oauth_grant_types_confidential'] = array( 'authorization_code' );
|
||||
$values['oauth_redirect_uris'] = array( rtrim( $values['oauth_wordpress_endpoint'], '/' ) . '/oauthcallback' );
|
||||
$values['oauth_redirect_uris'] = array( rtrim( $values['oauth_wordpress_endpoint'], '/' ) );
|
||||
$values['oauth_choose_scopes'] = FALSE;
|
||||
$values['oauth_access_token_length'] = 168;
|
||||
$values['oauth_use_refresh_tokens'] = TRUE;
|
||||
@@ -333,6 +357,7 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
)
|
||||
);
|
||||
$values['oauth_type'] = 'wordpress';
|
||||
$values['oauth_pkce'] = 'none';
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -341,7 +366,7 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
unset( $values['oauth_invision_grant_type'] );
|
||||
unset( $values['oauth_invision_endpoint'] );
|
||||
unset( $values['oauth_wordpress_endpoint'] );
|
||||
|
||||
|
||||
/* Generate Client ID */
|
||||
if ( !$this->client_id )
|
||||
{
|
||||
@@ -425,6 +450,27 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Does the currently logged in user have permission to delete this node?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canDelete()
|
||||
{
|
||||
return $this->type !== 'mobile'; // Admin can deregister community which will in turn delete the client
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get whether or not this node is locked to current enabled/disabled status
|
||||
*
|
||||
* @note Return value NULL indicates the node cannot be enabled/disabled
|
||||
* @return bool|null
|
||||
*/
|
||||
protected function get__locked()
|
||||
{
|
||||
return $this->type === 'mobile';
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get buttons to display in tree
|
||||
* Example code explains return value
|
||||
@@ -475,13 +521,18 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
/**
|
||||
* Generate or renew an access token
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member or NULL for client_credentials
|
||||
* @param array|null $scopes Array of scopes or NULL if none were requested
|
||||
* @param bool $skipRefreshToken If TRUE, will not generater a refresh token (for example, when using implicit grant type)
|
||||
* @param string $authorizationCode The authorization code which generated the token, if applicable
|
||||
* @param \IPS\Member|NULL $member The member or NULL for client_credentials
|
||||
* @param array|null $scopes Array of scopes or NULL if none were requested
|
||||
* @param string $grantType Type of grant
|
||||
* @param bool $skipRefreshToken If TRUE, will not generate a refresh token (for example, when using implicit grant type)
|
||||
* @param string|NULL $authorizationCode The authorization code which generated the token, if applicable
|
||||
* @param string|NULL $userAgent The user agent that the user performed authentication on, if known
|
||||
* @param string|NULL $issueUserAgent The user agent that the access token was issued to, if known
|
||||
* @param \IPS\Member\Device|NULL $device The device used to obtain this access token, if known
|
||||
* @param array $tokenToRefresh If we are refreshing, the existing access token to refresh
|
||||
* @return array
|
||||
*/
|
||||
public function generateAccessToken( \IPS\Member $member = NULL, $scopes, $grantType, $skipRefreshToken = FALSE, $authorizationCode = NULL )
|
||||
public function generateAccessToken( \IPS\Member $member = NULL, $scopes, $grantType, $skipRefreshToken = FALSE, $authorizationCode = NULL, $authUserAgent = NULL, $grantUserAgent = NULL, \IPS\Member\Device $device = NULL, $tokenToRefresh = NULL )
|
||||
{
|
||||
do
|
||||
{
|
||||
@@ -497,33 +548,57 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
'refresh_token' => NULL,
|
||||
'refresh_token_expires' => $this->refresh_token_length ? ( time() + ( $this->refresh_token_length * 86400 ) ) : NULL,
|
||||
'scope' => $scopes ? json_encode( $scopes ) : NULL,
|
||||
'authorization_code' => $authorizationCode,
|
||||
'issued' => time()
|
||||
'authorization_code' => $authorizationCode,
|
||||
'issued' => time(),
|
||||
'auth_user_agent' => $authUserAgent,
|
||||
'issue_user_agent' => $grantUserAgent,
|
||||
'device_key' => $device ? $device->device_key : NULL
|
||||
);
|
||||
|
||||
if ( $this->use_refresh_tokens and !$skipRefreshToken )
|
||||
if ( $this->use_refresh_tokens )
|
||||
{
|
||||
do
|
||||
if ( !$skipRefreshToken )
|
||||
{
|
||||
$data['refresh_token'] = \IPS\Login::generateRandomString( 64 );
|
||||
do
|
||||
{
|
||||
$data['refresh_token'] = \IPS\Login::generateRandomString( 64 );
|
||||
}
|
||||
while ( $this->validateRefreshToken( $data['refresh_token'] ) );
|
||||
|
||||
if ( $this->refresh_token_length )
|
||||
{
|
||||
$data['refresh_token_expires'] = time() + ( $this->refresh_token_length * 86400 );
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['refresh_token_expires'] = NULL;
|
||||
}
|
||||
}
|
||||
while ( $this->validateRefreshToken( $data['refresh_token'] ) );
|
||||
|
||||
if ( $this->refresh_token_length )
|
||||
elseif ( $tokenToRefresh )
|
||||
{
|
||||
$data['refresh_token_expires'] = time() + ( $this->refresh_token_length * 86400 );
|
||||
}
|
||||
else
|
||||
{
|
||||
$data['refresh_token_expires'] = NULL;
|
||||
$data['refresh_token'] = $tokenToRefresh['refresh_token'];
|
||||
$data['refresh_token_expires'] = $tokenToRefresh['refresh_token_expires'];
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\Db::i()->insert( 'core_oauth_server_access_tokens', $data );
|
||||
|
||||
if ( $member )
|
||||
if ( $grantType === 'refresh_token' and $tokenToRefresh )
|
||||
{
|
||||
$member->logHistory( 'core', 'oauth', array( 'type' => 'issued_access_token', 'client' => $this->client_id, 'grant' => $grantType, 'scopes' => $scopes ), FALSE );
|
||||
\IPS\Db::i()->update( 'core_oauth_server_access_tokens', $data, array( 'client_id=? AND access_token=?', $tokenToRefresh['client_id'], $tokenToRefresh['access_token'] ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_oauth_server_access_tokens', $data );
|
||||
if ( $member )
|
||||
{
|
||||
$member->logHistory( 'core', 'oauth', array( 'type' => 'issued_access_token', 'client' => $this->client_id, 'grant' => $grantType, 'scopes' => $scopes ), FALSE );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $device )
|
||||
{
|
||||
$device->last_seen = time();
|
||||
$device->save();
|
||||
$device->logIpAddress( \IPS\Request::i()->ipAddress() );
|
||||
}
|
||||
|
||||
$data['access_token'] = $this->client_id . '_' . $data['access_token'];
|
||||
@@ -531,6 +606,32 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get access token details, checking it isn't expired
|
||||
*
|
||||
* @param string $accessToken The access token
|
||||
* @return \IPS\Member|NULL
|
||||
* @throws \UnderflowException
|
||||
* @throws \IPS\Api\Exception
|
||||
*/
|
||||
public static function accessTokenDetails( $accessToken )
|
||||
{
|
||||
$exploded = explode( '_', $accessToken );
|
||||
|
||||
if ( !isset( $exploded[0] ) or !isset( $exploded[1] ) )
|
||||
{
|
||||
throw new \UnderflowException;
|
||||
}
|
||||
|
||||
$return = \IPS\Db::i()->select( '*', 'core_oauth_server_access_tokens', array( 'client_id=? AND access_token=?', $exploded[0], $exploded[1] ) )->first();
|
||||
if ( $return['access_token_expires'] and $return['access_token_expires'] < time() )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'EXPIRED_ACCESS_TOKEN', '1S290/E', 401, 'invalid_token' );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate an access token
|
||||
*
|
||||
@@ -582,7 +683,7 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( array_diff( $scopes, $row['scope'] ? json_decode( $row['scope'] ) : array() ) ) )
|
||||
if ( \count( array_diff( $scopes, $row['scope'] ? json_decode( $row['scope'] ) : array() ) ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -666,5 +767,31 @@ class _OAuthClient extends \IPS\Node\Model
|
||||
|
||||
return parent::delete();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get special values for mobile app
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
final public static function mobileAppValues()
|
||||
{
|
||||
$redirect_uris = [ \IPS\APP_MULTICOMMUNITY_SCHEMA . ':///auth' ];
|
||||
if ( \IPS\Settings::i()->mobile_app_redirect_scheme )
|
||||
{
|
||||
$redirect_uris[] = \IPS\Settings::i()->mobile_app_redirect_scheme . ':///auth';
|
||||
}
|
||||
|
||||
return array(
|
||||
'grant_types' => array('authorization_code'),
|
||||
'redirect_uris' => $redirect_uris,
|
||||
'access_token_length' => 168,
|
||||
'use_refresh_tokens' => TRUE,
|
||||
'refresh_token_length' => NULL,
|
||||
'scopes' => array(),
|
||||
'choose_scopes' => FALSE,
|
||||
'type' => 'mobile',
|
||||
'graphql' => TRUE
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
@@ -28,9 +28,9 @@ class _PaginatedResponse extends Response
|
||||
public $httpCode;
|
||||
|
||||
/**
|
||||
* @brief Select query
|
||||
* @brief Iterator (usually a select query)
|
||||
*/
|
||||
protected $select;
|
||||
protected $iterator;
|
||||
|
||||
/**
|
||||
* @brief Current page
|
||||
@@ -61,7 +61,7 @@ class _PaginatedResponse extends Response
|
||||
* Constructor
|
||||
*
|
||||
* @param int $httpCode HTTP Response code
|
||||
* @param \IPS\Db\Select $select Select query
|
||||
* @param \Iterator $iterator Select query or \IPS\Patterns\ActiveRecordIterator instance
|
||||
* @param int $page Current page
|
||||
* @param string $activeRecordClass ActiveRecord class
|
||||
* @param int $count Total Count
|
||||
@@ -69,11 +69,11 @@ class _PaginatedResponse extends Response
|
||||
* @param int $perPage Number of results per page
|
||||
* @return void
|
||||
*/
|
||||
public function __construct( $httpCode, $select, $page, $activeRecordClass, $count, \IPS\Member $authorizedMember = NULL, $perPage=NULL )
|
||||
public function __construct( $httpCode, $iterator, $page, $activeRecordClass, $count, \IPS\Member $authorizedMember = NULL, $perPage=NULL )
|
||||
{
|
||||
$this->httpCode = $httpCode;
|
||||
$this->page = (int) $page;
|
||||
$this->select = $select;
|
||||
$this->iterator = $iterator;
|
||||
$this->activeRecordClass = $activeRecordClass;
|
||||
$this->count = (int) $count;
|
||||
$this->resultsPerPage = $perPage ? (int) $perPage : 25;
|
||||
@@ -88,18 +88,32 @@ class _PaginatedResponse extends Response
|
||||
public function getOutput()
|
||||
{
|
||||
$results = array();
|
||||
$this->select->query .= \IPS\Db::i()->compileLimitClause( array( ( $this->page - 1 ) * $this->resultsPerPage, $this->resultsPerPage ) );
|
||||
|
||||
if( $this->iterator instanceof \IPS\Db\Select )
|
||||
{
|
||||
$this->iterator->query .= \IPS\Db::i()->compileLimitClause( array( ( $this->page - 1 ) * $this->resultsPerPage, $this->resultsPerPage ) );
|
||||
}
|
||||
|
||||
if ( $this->activeRecordClass )
|
||||
{
|
||||
foreach ( new \IPS\Patterns\ActiveRecordIterator( $this->select, $this->activeRecordClass ) as $result )
|
||||
if( $this->iterator instanceof \IPS\Patterns\ActiveRecordIterator )
|
||||
{
|
||||
$results[] = $result->apiOutput( $this->authorizedMember );
|
||||
foreach ( $this->iterator as $result )
|
||||
{
|
||||
$results[] = $result->apiOutput( $this->authorizedMember );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( new \IPS\Patterns\ActiveRecordIterator( $this->iterator, $this->activeRecordClass ) as $result )
|
||||
{
|
||||
$results[] = $result->apiOutput( $this->authorizedMember );
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( $this->select as $result )
|
||||
foreach ( $this->iterator as $result )
|
||||
{
|
||||
$results[] = $result;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
@@ -48,7 +48,7 @@ class _Response
|
||||
/**
|
||||
* Data to output
|
||||
*
|
||||
* @retrun string
|
||||
* @return string
|
||||
*/
|
||||
public function getOutput()
|
||||
{
|
||||
|
||||
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Webhook
|
||||
* @author <a href='http://www.invisionpower.com'>Invision Power Services, Inc.</a>
|
||||
* @copyright (c) 2001 - 2016 Invision Power Services, Inc.
|
||||
* @license http://www.invisionpower.com/legal/standards/
|
||||
* @package IPS Community Suite
|
||||
* @since 5 Feb 2020
|
||||
*/
|
||||
|
||||
namespace IPS\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook
|
||||
*/
|
||||
class _Webhook extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
*/
|
||||
protected static $multitons;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Table
|
||||
*/
|
||||
public static $databaseTable = 'core_api_webhooks';
|
||||
|
||||
/**
|
||||
* @brief cache for get_url()
|
||||
*/
|
||||
protected $_url;
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Caches
|
||||
* @note Defined cache keys will be cleared automatically as needed
|
||||
*/
|
||||
protected $caches = array('webhooks');
|
||||
|
||||
/**
|
||||
* Fire all webhooks for given event
|
||||
*
|
||||
* @param string $event The event key
|
||||
* @param mixed $data Data
|
||||
* @param array $filters Filters
|
||||
* @return void
|
||||
*/
|
||||
public static function fire( $event, $data = NULL, $filters = array() )
|
||||
{
|
||||
/* Normalise data */
|
||||
if ( \is_object( $data ) and method_exists( $data, 'apiOutput' ) )
|
||||
{
|
||||
$data = $data->apiOutput();
|
||||
}
|
||||
$data = $data ? json_encode( $data ) : NULL;
|
||||
|
||||
/* Get our webhooks from cache */
|
||||
if ( !isset( \IPS\Data\Store::i()->webhooks ) )
|
||||
{
|
||||
$webhooks = array();
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_api_webhooks', array('enabled=1') ) as $row )
|
||||
{
|
||||
foreach ( explode( ',', $row['events'] ) as $e )
|
||||
{
|
||||
if ( !isset( $webhooks[ $e ] ) )
|
||||
{
|
||||
$webhooks[ $e ] = array();
|
||||
}
|
||||
|
||||
$webhooks[ $e ][ $row['id'] ] = array(
|
||||
'key' => $row['api_key'],
|
||||
'url' => $row['url'],
|
||||
'filters' => json_decode( $row['filters'], TRUE )
|
||||
);
|
||||
}
|
||||
}
|
||||
\IPS\Data\Store::i()->webhooks = $webhooks;
|
||||
}
|
||||
|
||||
/* If we have webhooks for this event... */
|
||||
$enable = FALSE;
|
||||
if ( isset( \IPS\Data\Store::i()->webhooks[ $event ] ) )
|
||||
{
|
||||
/* Loop through each one... */
|
||||
foreach ( \IPS\Data\Store::i()->webhooks[ $event ] as $id => $webhook )
|
||||
{
|
||||
/* Skip over it if the filters don't match */
|
||||
if ( isset( $webhook['filters'][ $event ] ) )
|
||||
{
|
||||
foreach ( $webhook['filters'][ $event ] as $k => $v )
|
||||
{
|
||||
if ( isset( $filters[ $k ] ) )
|
||||
{
|
||||
if ( \is_array( $v ) and !\is_array( $filters[ $k ] ) )
|
||||
{
|
||||
if ( !\in_array( $filters[ $k ], $v ) )
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( $filters[ $k ] != $v )
|
||||
{
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Insert it */
|
||||
$enable = TRUE;
|
||||
\IPS\Db::i()->insert( 'core_api_webhook_fires', [
|
||||
'webhook' => $id,
|
||||
'event' => $event,
|
||||
'data' => $data,
|
||||
'time' => time(),
|
||||
'status' => 'pending'
|
||||
] );
|
||||
}
|
||||
}
|
||||
|
||||
/* Make sure the task is enabled */
|
||||
if ( $enable )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_tasks', array( 'enabled' => 1 ), array( '`key`=?', 'webhooks' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get API Key
|
||||
*
|
||||
* @return \IPS\Api\Key
|
||||
*/
|
||||
public function get_api_key()
|
||||
{
|
||||
return \IPS\Api\Key::load( $this->_data['api_key'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set API Key
|
||||
*
|
||||
* @param \IPS\Api\Key $key The API Key
|
||||
* @return void
|
||||
*/
|
||||
public function set_api_key( \IPS\Api\Key $key )
|
||||
{
|
||||
$this->_data['api_key'] = $key->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Events
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_events()
|
||||
{
|
||||
return explode( ',', $this->_data['events'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Events
|
||||
*
|
||||
* @param array $events List of events
|
||||
* @return void
|
||||
*/
|
||||
public function set_events( array $events )
|
||||
{
|
||||
$this->_data['events'] = implode( ',', $events );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get URL
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_url()
|
||||
{
|
||||
if ( $this->_url === NULL )
|
||||
{
|
||||
$this->_url = new \IPS\Http\Url( $this->_data['url'] );
|
||||
}
|
||||
|
||||
return $this->_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Events
|
||||
*
|
||||
* @param \IPS\Http\Url $url The URL
|
||||
* @return void
|
||||
*/
|
||||
public function set_url( \IPS\Http\Url $url )
|
||||
{
|
||||
$this->_url = $url;
|
||||
$this->_data['url'] = (string) $this->_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_filters()
|
||||
{
|
||||
return $this->_data['filters'] ? json_decode( $this->_data['filters'], TRUE ) : array();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set Filters
|
||||
*
|
||||
* @param array $filters Filter
|
||||
* @return void
|
||||
*/
|
||||
public function set_filters( array $filters )
|
||||
{
|
||||
$this->_data['filters'] = json_encode( $filters );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get output for API
|
||||
*
|
||||
* @return array
|
||||
* @apiresponse int id ID number
|
||||
* @apiresponse array events List of events to subscribe to
|
||||
* @apiresponse string url URL to send webhook to
|
||||
*/
|
||||
public function apiOutput()
|
||||
{
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'events' => $this->events,
|
||||
'url' => (string) $this->url
|
||||
);
|
||||
}
|
||||
}
|
||||
Whitespace-only changes.
Reference in new issue
Block a user