Version 5.0.0 beta 1
This commit is contained in:
1 parent
25ddeb65d6
commit
15c7beabc5
6736 files changed
+627902
-497943
No files matched your search
@@ -11,29 +11,35 @@
|
||||
namespace IPS\Node\Api\GraphQL;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Api\GraphQL\SafeException;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Base mutator class for nodes
|
||||
*/
|
||||
class _NodeMutator
|
||||
class NodeMutator
|
||||
{
|
||||
/**
|
||||
* Mark a node as read
|
||||
*
|
||||
* @param int $reactionID ID of reaction to add
|
||||
* @param \IPS\Content\Comment $comment The comment to add a reaction on
|
||||
* @return void
|
||||
* @param Model $node The comment to add a reaction on
|
||||
* @return Model
|
||||
*/
|
||||
protected function _markRead( $node )
|
||||
protected function _markRead( Model $node ): Model
|
||||
{
|
||||
if( !\IPS\Member::loggedIn()->member_id )
|
||||
if( !Member::loggedIn()->member_id )
|
||||
{
|
||||
throw new \IPS\Api\GraphQL\SafeException( 'NOT_LOGGED_IN', 'GQL/0004/1', 403 );
|
||||
throw new SafeException( 'NOT_LOGGED_IN', 'GQL/0004/1', 403 );
|
||||
}
|
||||
|
||||
try
|
||||
@@ -41,9 +47,9 @@ class _NodeMutator
|
||||
$itemClass = $node::$contentItemClass;
|
||||
$itemClass::markContainerRead( $node );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
throw new \IPS\Api\GraphQL\SafeException( 'INVALID_NODE', 'GQL/0004/2', 403 );
|
||||
throw new SafeException( 'INVALID_NODE', 'GQL/0004/2', 403 );
|
||||
}
|
||||
|
||||
return $node;
|
||||
|
||||
@@ -9,35 +9,44 @@
|
||||
*/
|
||||
|
||||
namespace IPS\Node\Api\GraphQL;
|
||||
use Exception;
|
||||
use GraphQL\Type\Definition\ObjectType;
|
||||
use IPS\Api\GraphQL\Fields\TagsEditField;
|
||||
use IPS\Api\GraphQL\TypeRegistry;
|
||||
use IPS\Content\Item;
|
||||
use IPS\Member;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use function defined;
|
||||
use function is_array;
|
||||
|
||||
/* 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' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Base class for Nodes
|
||||
*/
|
||||
class _NodeType extends ObjectType
|
||||
class NodeType extends ObjectType
|
||||
{
|
||||
/*
|
||||
/**
|
||||
* @brief The item classname we use for this type
|
||||
* @var Model|string
|
||||
*/
|
||||
protected static $nodeClass = '\IPS\Node\Model';
|
||||
protected static string $nodeClass = '\IPS\Node\Model';
|
||||
|
||||
/*
|
||||
* @brief GraphQL type name
|
||||
*/
|
||||
protected static $typeName = 'core_Node';
|
||||
protected static string $typeName = 'core_Node';
|
||||
|
||||
/*
|
||||
* @brief GraphQL type description
|
||||
*/
|
||||
protected static $typeDescription = 'A generic node';
|
||||
protected static string $typeDescription = 'A generic node';
|
||||
|
||||
|
||||
public function __construct()
|
||||
@@ -58,7 +67,7 @@ class _NodeType extends ObjectType
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function fields()
|
||||
public function fields(): array
|
||||
{
|
||||
return array(
|
||||
'id' => [
|
||||
@@ -70,7 +79,7 @@ class _NodeType extends ObjectType
|
||||
'name' => [
|
||||
'type' => TypeRegistry::string(),
|
||||
'resolve' => function ($node) {
|
||||
return $node->getTitleForLanguage( \IPS\Member::loggedIn()->language() );
|
||||
return $node->getTitleForLanguage( Member::loggedIn()->language() );
|
||||
}
|
||||
],
|
||||
'url' => [
|
||||
@@ -82,12 +91,14 @@ class _NodeType extends ObjectType
|
||||
'itemCount' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'resolve' => function ($node) {
|
||||
/* @var Item $contentItemClass */
|
||||
return static::$nodeClass::$contentItemClass::contentCount($node, TRUE, FALSE);
|
||||
}
|
||||
],
|
||||
'commentCount' => [
|
||||
'type' => TypeRegistry::int(),
|
||||
'resolve' => function ($node) {
|
||||
/* @var Item $contentItemClass */
|
||||
return static::$nodeClass::$contentItemClass::contentCount($node, FALSE, TRUE);
|
||||
}
|
||||
],
|
||||
@@ -100,6 +111,7 @@ class _NodeType extends ObjectType
|
||||
'hasUnread' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($node) {
|
||||
/* @var Item $contentItemClass */
|
||||
return static::$nodeClass::$contentItemClass::containerUnread( $node );
|
||||
}
|
||||
],
|
||||
@@ -113,7 +125,7 @@ class _NodeType extends ObjectType
|
||||
'follow' => [
|
||||
'type' => TypeRegistry::follow(),
|
||||
'resolve' => function ($node) {
|
||||
if( isset( static::$followData ) && \is_array( static::$followData ) ){
|
||||
if( isset( static::$followData ) && is_array( static::$followData ) ){
|
||||
return array_merge( static::$followData, array(
|
||||
'id' => $node->_id,
|
||||
'node' => $node,
|
||||
@@ -142,7 +154,7 @@ class _NodeType extends ObjectType
|
||||
return array( 'postKey' => $args['postKey'] );
|
||||
}
|
||||
],
|
||||
'tagPermissions' => \IPS\Api\GraphQL\Fields\TagsEditField::getDefinition(static::$typeName . '_tags')
|
||||
'tagPermissions' => TagsEditField::getDefinition(static::$typeName . '_tags')
|
||||
);
|
||||
}
|
||||
|
||||
@@ -151,7 +163,7 @@ class _NodeType extends ObjectType
|
||||
*
|
||||
* @return ObjectType
|
||||
*/
|
||||
public static function getItemType()
|
||||
public static function getItemType(): ObjectType
|
||||
{
|
||||
return \IPS\Content\Api\GraphQL\TypeRegistry::item();
|
||||
}
|
||||
@@ -161,19 +173,20 @@ class _NodeType extends ObjectType
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getNodePermissionFields()
|
||||
public static function getNodePermissionFields(): array
|
||||
{
|
||||
return array(
|
||||
'canCreate' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($node, $args, $context) {
|
||||
return $node->can('add', \IPS\Member::loggedIn(), FALSE);
|
||||
return $node->can('add', Member::loggedIn(), FALSE);
|
||||
}
|
||||
],
|
||||
'itemsRequireApproval' => [
|
||||
'type' => TypeRegistry::boolean(),
|
||||
'resolve' => function ($node, $args, $context) {
|
||||
return static::$nodeClass::$contentItemClass::moderateNewItems( \IPS\Member::loggedIn(), $node, FALSE );
|
||||
/* @var Item $contentItemClass */
|
||||
return static::$nodeClass::$contentItemClass::moderateNewItems( Member::loggedIn(), $node, FALSE );
|
||||
}
|
||||
],
|
||||
'commentsRequireApproval' => [
|
||||
@@ -196,10 +209,12 @@ class _NodeType extends ObjectType
|
||||
/**
|
||||
* Resolve children field
|
||||
*
|
||||
* @param \IPS\Node\Model
|
||||
* @return array
|
||||
* @param Model $node
|
||||
* @param array $args
|
||||
* @param array $context
|
||||
* @return array
|
||||
*/
|
||||
protected static function children($node, $args, $context)
|
||||
protected static function children( Model $node, array $args, array $context): array
|
||||
{
|
||||
return $node->children('view');
|
||||
}
|
||||
@@ -207,11 +222,11 @@ class _NodeType extends ObjectType
|
||||
/**
|
||||
* Resolve the topics field
|
||||
*
|
||||
* @param \IPS\forums\Forum
|
||||
* @param array Arguments passed to this resolver
|
||||
* @return array
|
||||
* @param Model $node
|
||||
* @param array $args Arguments passed to this resolver
|
||||
* @return ActiveRecordIterator|int
|
||||
*/
|
||||
protected static function items($node, $args)
|
||||
protected static function items( Model $node, array $args): ActiveRecordIterator|int
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -221,20 +236,24 @@ class _NodeType extends ObjectType
|
||||
}
|
||||
else
|
||||
{
|
||||
/* @var Item $contentItemClass */
|
||||
/* @var array $databaseColumnMap */
|
||||
$orderBy = static::$nodeClass::$contentItemClass::$databaseColumnMap[ $args['orderBy'] ];
|
||||
}
|
||||
|
||||
if( $args['orderBy'] === 'last_comment' )
|
||||
{
|
||||
$orderBy = \is_array( $orderBy ) ? array_pop( $orderBy ) : $orderBy;
|
||||
$orderBy = is_array( $orderBy ) ? array_pop( $orderBy ) : $orderBy;
|
||||
}
|
||||
}
|
||||
catch (\Exception $e)
|
||||
catch (Exception $e)
|
||||
{
|
||||
$orderBy = 'title';
|
||||
}
|
||||
|
||||
$where = array();
|
||||
|
||||
/* @var Item $class */
|
||||
$class = static::$nodeClass::$contentItemClass;
|
||||
$sortBy = $class::$databaseTable . '.' . $class::$databasePrefix . "{$orderBy} {$args['orderDir']}";
|
||||
|
||||
|
||||
@@ -10,21 +10,21 @@
|
||||
*/
|
||||
|
||||
namespace IPS\Node\Api\GraphQL;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use function defined;
|
||||
|
||||
/* 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' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* \IPS\Node base types
|
||||
*/
|
||||
class _TypeRegistry
|
||||
class TypeRegistry
|
||||
{
|
||||
protected static $node;
|
||||
protected static ?NodeType $node = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -35,10 +35,10 @@ class _TypeRegistry
|
||||
}
|
||||
|
||||
/**
|
||||
* @return CommentType
|
||||
* @return NodeType
|
||||
*/
|
||||
public static function node()
|
||||
public static function node() : NodeType
|
||||
{
|
||||
return self::$node ?: (self::$node = new \IPS\Node\Api\GraphQL\NodeType());
|
||||
return self::$node ?: (self::$node = new NodeType());
|
||||
}
|
||||
}
|
||||
@@ -11,30 +11,46 @@
|
||||
namespace IPS\Node\Api;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Api\Controller;
|
||||
use IPS\Api\Exception;
|
||||
use IPS\Api\PaginatedResponse;
|
||||
use IPS\Api\Response;
|
||||
use IPS\Db;
|
||||
use IPS\IPS;
|
||||
use IPS\Node\Model;
|
||||
use IPS\Node\Permissions;
|
||||
use IPS\Request;
|
||||
use OutOfRangeException;
|
||||
use function defined;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Base API endpoint for Nodes
|
||||
*/
|
||||
class _NodeController extends \IPS\Api\Controller
|
||||
class NodeController extends Controller
|
||||
{
|
||||
/**
|
||||
* List
|
||||
*
|
||||
* @param array $where Extra WHERE clause
|
||||
* @return \IPS\Api\PaginatedResponse
|
||||
* @return PaginatedResponse
|
||||
*/
|
||||
protected function _list( $where = array() )
|
||||
protected function _list( array $where = array() ) : PaginatedResponse
|
||||
{
|
||||
$class = $this->class;
|
||||
|
||||
if ( $this->member and \in_array( 'IPS\Node\Permissions', class_implements( $class ) ) )
|
||||
|
||||
/* @var array $permissionMap */
|
||||
if ( $this->member and in_array( 'IPS\Node\Permissions', class_implements( $class ) ) )
|
||||
{
|
||||
$where[] = array( '(' . \IPS\Db::i()->findInSet( 'core_permission_index.perm_' . $class::$permissionMap['view'], $this->member->permissionArray() ) . ' OR ' . 'core_permission_index.perm_' . $class::$permissionMap['view'] . '=? )', '*' );
|
||||
$where[] = array( '(' . Db::i()->findInSet( 'core_permission_index.perm_' . $class::$permissionMap['view'], $this->member->permissionArray() ) . ' OR ' . 'core_permission_index.perm_' . $class::$permissionMap['view'] . '=? )', '*' );
|
||||
if ( $class::$databaseColumnEnabledDisabled )
|
||||
{
|
||||
$where[] = array( $class::$databasePrefix . $class::$databaseColumnEnabledDisabled . '=1' );
|
||||
@@ -42,28 +58,28 @@ class _NodeController extends \IPS\Api\Controller
|
||||
}
|
||||
|
||||
/* Exclude clubs? */
|
||||
if ( \IPS\IPS::classUsesTrait( $class, 'IPS\Content\ClubContainer' ) AND isset( \IPS\Request::i()->clubs ) AND !\IPS\Request::i()->clubs )
|
||||
if ( IPS::classUsesTrait( $class, 'IPS\Content\ClubContainer' ) AND isset( Request::i()->clubs ) AND !Request::i()->clubs )
|
||||
{
|
||||
$where[] = array( $class::$databasePrefix . $class::clubIdColumn() . ' IS NULL' );
|
||||
}
|
||||
|
||||
$select = \IPS\Db::i()->select( '*', $class::$databaseTable, $where, $class::$databaseColumnOrder ? $class::$databasePrefix . $class::$databaseColumnOrder . " asc" : NULL );
|
||||
$select = Db::i()->select( '*', $class::$databaseTable, $where, $class::$databaseColumnOrder ? $class::$databasePrefix . $class::$databaseColumnOrder . " asc" : NULL );
|
||||
|
||||
/* Return permissions */
|
||||
if ( \in_array( 'IPS\Node\Permissions', class_implements( $class ) ) )
|
||||
if ( in_array( 'IPS\Node\Permissions', class_implements( $class ) ) )
|
||||
{
|
||||
$select->join( 'core_permission_index', array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=" . $class::$databaseTable . "." . $class::$databasePrefix . $class::$databaseColumnId, $class::$permApp, $class::$permType ) );
|
||||
}
|
||||
|
||||
/* Return */
|
||||
return new \IPS\Api\PaginatedResponse(
|
||||
return new PaginatedResponse(
|
||||
200,
|
||||
$select,
|
||||
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
|
||||
isset( Request::i()->page ) ? Request::i()->page : 1,
|
||||
$class,
|
||||
NULL,
|
||||
$this->member,
|
||||
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
|
||||
isset( Request::i()->perPage ) ? Request::i()->perPage : NULL
|
||||
);
|
||||
}
|
||||
|
||||
@@ -71,19 +87,19 @@ class _NodeController extends \IPS\Api\Controller
|
||||
* View
|
||||
*
|
||||
* @param int $id ID Number
|
||||
* @return \IPS\Api\Response
|
||||
* @return Response
|
||||
*/
|
||||
protected function _view( $id )
|
||||
protected function _view( int $id ) : Response
|
||||
{
|
||||
$class = $this->class;
|
||||
|
||||
$node = $class::load( $id );
|
||||
if ( $this->member and !$node->can( 'view', $this->member ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
|
||||
return new \IPS\Api\Response( 200, $node->apiOutput( $this->member ) );
|
||||
return new Response( 200, $node->apiOutput( $this->member ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,9 +110,9 @@ class _NodeController extends \IPS\Api\Controller
|
||||
* @throws 1S359/1 INVALID_ID The node ID does not exist
|
||||
* @throws 1S359/2 INVALID_TARGET The target node cannot be deleted because the new parent node does not exist
|
||||
* @throws 1S359/3 HAS_CHILDREN The target node cannot be deleted because it has children (pass deleteChildrenOrMove in the request to specify how to handle the children)
|
||||
* @return \IPS\Api\Response
|
||||
* @return Response
|
||||
*/
|
||||
protected function _delete( $id, $deleteChildrenOrMove = NULL )
|
||||
protected function _delete( int $id, ?int $deleteChildrenOrMove = NULL ) : Response
|
||||
{
|
||||
$class = $this->class;
|
||||
|
||||
@@ -118,15 +134,15 @@ class _NodeController extends \IPS\Api\Controller
|
||||
$target = $class::load( $deleteChildrenOrMove );
|
||||
$node->deleteOrMoveFormSubmit( array( 'node_move_children' => TRUE, 'node_destination' => $target->_id, 'node_move_content' => $target ) );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'INVALID_TARGET', '1S359/2', 404 );
|
||||
throw new Exception( 'INVALID_TARGET', '1S359/2', 404 );
|
||||
}
|
||||
}
|
||||
/* Or return an exception if no action was set */
|
||||
if ( !$deleteChildrenOrMove )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'HAS_CHILDREN', '1S359/3', 404 );
|
||||
throw new Exception( 'HAS_CHILDREN', '1S359/3', 404 );
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -134,25 +150,25 @@ class _NodeController extends \IPS\Api\Controller
|
||||
$node->delete();
|
||||
}
|
||||
|
||||
return new \IPS\Api\Response( 200, NULL );
|
||||
return new Response( 200, NULL );
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
catch ( OutOfRangeException $e )
|
||||
{
|
||||
throw new \IPS\Api\Exception( 'INVALID_ID', '1S359/1', 404 );
|
||||
throw new Exception( 'INVALID_ID', '1S359/1', 404 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update node
|
||||
*
|
||||
* @param \IPS\node\Model $node The node
|
||||
* @return \IPS\node\Model
|
||||
* @param Model $node The node
|
||||
* @return Model
|
||||
*/
|
||||
protected function _createOrUpdate( \IPS\Node\Model $node )
|
||||
protected function _createOrUpdate( Model $node ): Model
|
||||
{
|
||||
$node->save();
|
||||
|
||||
if( $node instanceof \IPS\Node\Permissions AND isset( \IPS\Request::i()->permissions ) AND \IPS\Request::i()->permissions )
|
||||
if( $node instanceof Permissions AND isset( Request::i()->permissions ) AND Request::i()->permissions )
|
||||
{
|
||||
$insert = array(
|
||||
'app' => $node::$permApp,
|
||||
@@ -162,9 +178,9 @@ class _NodeController extends \IPS\Api\Controller
|
||||
|
||||
foreach( $node::$permissionMap as $key => $field )
|
||||
{
|
||||
if( isset( \IPS\Request::i()->permissions[ $key ] ) )
|
||||
if( isset( Request::i()->permissions[ $key ] ) )
|
||||
{
|
||||
$insert[ 'perm_' . $field ] = \is_array( \IPS\Request::i()->permissions[ $key ] ) ? implode( ',', \IPS\Request::i()->permissions[ $key ] ) : \IPS\Request::i()->permissions[ $key ];
|
||||
$insert[ 'perm_' . $field ] = is_array( Request::i()->permissions[ $key ] ) ? implode( ',', Request::i()->permissions[ $key ] ) : Request::i()->permissions[ $key ];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,9 +194,9 @@ class _NodeController extends \IPS\Api\Controller
|
||||
/**
|
||||
* Create
|
||||
*
|
||||
* @return \IPS\Content\Node
|
||||
* @return Model
|
||||
*/
|
||||
protected function _create()
|
||||
protected function _create() : Model
|
||||
{
|
||||
$class = $this->class;
|
||||
|
||||
@@ -190,14 +206,13 @@ class _NodeController extends \IPS\Api\Controller
|
||||
if( isset( $node::$databaseColumnOrder ) AND $node::$automaticPositionDetermination === TRUE )
|
||||
{
|
||||
$orderColumn = $node::$databaseColumnOrder;
|
||||
$node->$orderColumn = \IPS\Db::i()->select( 'MAX(' . $node::$databasePrefix . $orderColumn . ')', $node::$databaseTable )->first() + 1;
|
||||
$node->$orderColumn = Db::i()->select( 'MAX(' . $node::$databasePrefix . $orderColumn . ')', $node::$databaseTable )->first() + 1;
|
||||
}
|
||||
|
||||
$node->save();
|
||||
$node = $this->_createOrUpdate( $node );
|
||||
|
||||
/* Output */
|
||||
return $node;
|
||||
return $this->_createOrUpdate( $node );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -213,9 +228,9 @@ class _NodeController extends \IPS\Api\Controller
|
||||
$where = $where ?: [];
|
||||
$idField = $class::$databaseTable . '.' . $class::$databasePrefix . '.' . $class::$databaseColumnId;
|
||||
|
||||
if ( isset( \IPS\Request::i()->ids ) )
|
||||
if ( isset( Request::i()->ids ) )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->in( $idField, array_map( 'intval', explode(',', \IPS\Request::i()->ids ) ) ) );
|
||||
$where[] = array( Db::i()->in( $idField, array_map( 'intval', explode(',', Request::i()->ids ) ) ) );
|
||||
}
|
||||
|
||||
return $where;
|
||||
|
||||
+42
-24
@@ -11,9 +11,15 @@
|
||||
namespace IPS\Node;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use IPS\Member;
|
||||
use IPS\Theme;
|
||||
use function defined;
|
||||
use function substr;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -25,24 +31,24 @@ trait Colorize
|
||||
/**
|
||||
* @brief Table column name that holds the feature color hex
|
||||
*/
|
||||
public static $featureColumnName = 'feature_color';
|
||||
public static string $featureColumnName = 'feature_color';
|
||||
|
||||
/**
|
||||
* @brief Cache the calculated formatted title for this node
|
||||
*/
|
||||
protected $formattedTitle = NULL;
|
||||
protected ?string $formattedTitle = NULL;
|
||||
|
||||
/**
|
||||
* @brief Cache the formatted text colour for this node
|
||||
*/
|
||||
protected $featureTextColor = NULL;
|
||||
protected ?string $featureTextColor = NULL;
|
||||
|
||||
/**
|
||||
* Get HTML formatted title. Allows apps or nodes to format the title, such as adding different colours, etc
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get__formattedTitle()
|
||||
public function get__formattedTitle(): string
|
||||
{
|
||||
if ( $this->formattedTitle === NULL )
|
||||
{
|
||||
@@ -54,19 +60,30 @@ trait Colorize
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->formattedTitle = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->formattedTitle( $this );
|
||||
$this->formattedTitle = Theme::i()->getTemplate( 'global', 'core', 'front' )->formattedTitle( $this );
|
||||
}
|
||||
}
|
||||
|
||||
return $this->formattedTitle;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the featured color for use in CSS
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function get__featureColor(): ?string
|
||||
{
|
||||
$columnName = static::$featureColumnName;
|
||||
return $this->$columnName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the featured text color for use in CSS
|
||||
*
|
||||
* @return string
|
||||
* @return string|null
|
||||
*/
|
||||
public function get__featureTextColor()
|
||||
public function get__featureTextColor(): ?string
|
||||
{
|
||||
if ( $this->featureTextColor === NULL )
|
||||
{
|
||||
@@ -83,26 +100,27 @@ trait Colorize
|
||||
|
||||
return $this->featureTextColor;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Static method to fetch the text colour based on the the contrast from the background color
|
||||
*
|
||||
* @return string
|
||||
* @param $featureColor
|
||||
* @return string
|
||||
*/
|
||||
public static function featureTextColor( $featureColor )
|
||||
public static function featureTextColor( $featureColor ): string
|
||||
{
|
||||
$hexColor = str_replace( '#', '', $featureColor );
|
||||
$blackColor = '000000';
|
||||
$textColor = '#ffffff';
|
||||
|
||||
/* Use luminosity contrast algorithm to determine font color */
|
||||
$r1 = hexdec( \substr( $hexColor, 0, 2 ) );
|
||||
$g1 = hexdec( \substr( $hexColor, 2, 2 ) );
|
||||
$b1 = hexdec( \substr( $hexColor, 4, 2 ) );
|
||||
$r1 = hexdec( substr( $hexColor, 0, 2 ) );
|
||||
$g1 = hexdec( substr( $hexColor, 2, 2 ) );
|
||||
$b1 = hexdec( substr( $hexColor, 4, 2 ) );
|
||||
|
||||
$r2 = hexdec( \substr( $blackColor, 0, 2 ) );
|
||||
$g2 = hexdec( \substr( $blackColor, 2, 2 ) );
|
||||
$b2 = hexdec( \substr( $blackColor, 4, 2 ) );
|
||||
$r2 = hexdec( substr( $blackColor, 0, 2 ) );
|
||||
$g2 = hexdec( substr( $blackColor, 2, 2 ) );
|
||||
$b2 = hexdec( substr( $blackColor, 4, 2 ) );
|
||||
|
||||
$l1 = 0.2126 * pow( $r1 / 255, 2.2 ) + 0.7152 * pow( $g1 / 255, 2.2 ) + 0.0722 * pow( $b1 / 255, 2.2 );
|
||||
$l2 = 0.2126 * pow( $r2 / 255, 2.2 ) + 0.7152 * pow( $g2 / 255, 2.2 ) + 0.0722 * pow( $b2 / 255, 2.2 );
|
||||
@@ -133,9 +151,9 @@ trait Colorize
|
||||
* @param array $itemData Basic data about the item. Only includes columns returned by item::basicDataColumns()
|
||||
* @param array|NULL $containerData Basic data about the container. Only includes columns returned by container::basicDataColumns()
|
||||
* @param bool $escape If the title should be escaped for HTML output. If FALSE, the feature color will not be used, because that requires HTML output
|
||||
* @return \IPS\Http\Url
|
||||
* @return mixed
|
||||
*/
|
||||
public static function titleFromIndexData( $indexData, $itemData, $containerData, $escape = TRUE )
|
||||
public static function titleFromIndexData(array $indexData, array $itemData, ?array $containerData, bool $escape = TRUE ): mixed
|
||||
{
|
||||
if ( !$escape or !isset( $containerData[ static::$databasePrefix . static::$featureColumnName ] ) )
|
||||
{
|
||||
@@ -143,17 +161,17 @@ trait Colorize
|
||||
}
|
||||
|
||||
$node = $containerData;
|
||||
$node['title'] = \IPS\Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $indexData['index_container_id'], 'NULL', array( 'escape' => true ) );
|
||||
$node['title'] = Member::loggedIn()->language()->addToStack( static::$titleLangPrefix . $indexData['index_container_id'], 'NULL', array( 'escape' => true ) );
|
||||
$node['text_color'] = static::featureTextColor( $node[ static::$databasePrefix . static::$featureColumnName ] );
|
||||
|
||||
/* Normalize */
|
||||
$node['feature_color'] = $node[ static::$databasePrefix . static::$featureColumnName ];
|
||||
|
||||
$title = \IPS\Theme::i()->getTemplate( 'global', 'core', 'front' )->formattedTitle( $node );
|
||||
$title = Theme::i()->getTemplate( 'global', 'core', 'front' )->formattedTitle( $node );
|
||||
|
||||
if ( $indexData['index_club_id'] and isset( $containerData['_club'] ) )
|
||||
{
|
||||
return \IPS\Member::loggedIn()->language()->addToStack( 'club_container_title', FALSE, array( 'sprintf' => array( $containerData['_club']['name'] ), 'htmlsprintf' => array( $title ) ) );
|
||||
return Member::loggedIn()->language()->addToStack( 'club_container_title', FALSE, array( 'sprintf' => array( $containerData['_club']['name'] ), 'htmlsprintf' => array( $title ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+354
-327
File diff suppressed because it is too large.
Load diff
@@ -7,16 +7,19 @@
|
||||
* @license https://www.invisioncommunity.com/legal/standards/
|
||||
* @package Invision Community
|
||||
* @subpackage
|
||||
* @since 6/25/2024
|
||||
* @since 7/23/2024
|
||||
*/
|
||||
|
||||
namespace IPS\Node;
|
||||
|
||||
use BadMethodCallException;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Data\Store;
|
||||
use IPS\Redis;
|
||||
use OutOfRangeException;
|
||||
use BadMethodCallException;
|
||||
use function get_called_class;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
@@ -30,7 +33,7 @@ trait DelayedCount
|
||||
*/
|
||||
protected function getStorageId() : string
|
||||
{
|
||||
return \get_called_class() . '_' . $this->_id;
|
||||
return get_called_class() . '_' . $this->_id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +74,7 @@ trait DelayedCount
|
||||
Redis::i()->expire( 'nodeSyncTimes', ( 2 * 3600 ) );
|
||||
}
|
||||
}
|
||||
catch( BadMethodCallException | \RedisException )
|
||||
catch( BadMethodCallException )
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -93,10 +96,7 @@ trait DelayedCount
|
||||
/* Bubble up to parent nodes */
|
||||
try
|
||||
{
|
||||
if( $parent = $this->parent() )
|
||||
{
|
||||
$parent->storeUpdateTime();
|
||||
}
|
||||
$this->parent()?->storeUpdateTime();
|
||||
}
|
||||
catch( \OutOfRangeException )
|
||||
{}
|
||||
@@ -156,7 +156,7 @@ trait DelayedCount
|
||||
/**
|
||||
* Count all comments, items, etc
|
||||
*
|
||||
* @return mixed
|
||||
* @return void
|
||||
*/
|
||||
abstract protected function recount();
|
||||
}
|
||||
abstract protected function recount() : void;
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Icon
|
||||
* @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
|
||||
* @subpackage
|
||||
* @since 3/26/2024
|
||||
*/
|
||||
|
||||
namespace IPS\Node;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use ErrorException;
|
||||
use Exception;
|
||||
use IPS\File;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Icon as FormIcon;
|
||||
use IPS\Helpers\Form\Radio;
|
||||
use IPS\Helpers\Form\Upload;
|
||||
use IPS\Theme;
|
||||
use function is_array;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
trait Icon
|
||||
{
|
||||
/**
|
||||
* @brief The column that holds the icon data, without the prefix
|
||||
*/
|
||||
public static string $iconColumn = 'icon';
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function get_icon_type() : string
|
||||
{
|
||||
$iconColumn = static::$iconColumn;
|
||||
if ( $this->$iconColumn and $iconData = json_decode( $this->$iconColumn, TRUE ) )
|
||||
{
|
||||
return 'icon';
|
||||
}
|
||||
|
||||
return ( $this->$iconColumn ) ? 'upload' : 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function iconFormPrefix() : string
|
||||
{
|
||||
return static::$iconFormPrefix ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public static function iconStorageExtension() : string
|
||||
{
|
||||
return static::$iconStorageExtension ?? '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add icon fields to the node form
|
||||
*
|
||||
* @param Form $form
|
||||
* @return void
|
||||
*/
|
||||
public function iconFormFields( Form &$form ) : void
|
||||
{
|
||||
$prefix = static::iconFormPrefix();
|
||||
$form->add( new Radio( $prefix . 'icon_choose', $this->icon_type, null, [
|
||||
'options' => [
|
||||
'none' => $prefix . 'icon_choose_icon_none',
|
||||
'icon' => $prefix . 'icon_choose_icon',
|
||||
'upload' => $prefix . 'icon_choose_upload'
|
||||
],
|
||||
'toggles' => [
|
||||
'icon' => [ $prefix . 'icon_picker' ], 'upload' => [ $prefix . 'icon_upload' ] ]
|
||||
] ) );
|
||||
|
||||
$form->add( new Upload( $prefix . 'icon_upload', ( $this->icon_type == 'upload' ) ? File::get( static::iconStorageExtension(), $this->icon ) : NULL, FALSE, array( 'image' => TRUE, 'storageExtension' => static::iconStorageExtension() ), NULL, NULL, NULL, $prefix . 'icon_upload' ) );
|
||||
|
||||
$form->add( new FormIcon( $prefix . 'icon_picker', ( $this->icon_type == 'icon' ? json_decode( $this->icon, true ) : null ), FALSE, array(), NULL, NULL, NULL, $prefix . 'icon_picker' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the above icon fields
|
||||
*
|
||||
* @param array $values
|
||||
* @return array
|
||||
*/
|
||||
public function formatIconFieldValues( array $values ) : array
|
||||
{
|
||||
/* Icon? Me? No. */
|
||||
$iconValue = null;
|
||||
$prefix = static::iconFormPrefix();
|
||||
if ( isset( $values[ $prefix . 'icon_choose' ] ) )
|
||||
{
|
||||
switch ( $values[ $prefix . 'icon_choose' ] )
|
||||
{
|
||||
case 'upload':
|
||||
if ( $values[ $prefix . 'icon_upload'] )
|
||||
{
|
||||
$iconValue = (string) $values[ $prefix . 'icon_upload'];
|
||||
}
|
||||
break;
|
||||
case 'icon':
|
||||
if ( is_array( $values[ $prefix . 'icon_picker'] ) )
|
||||
{
|
||||
$iconValue = json_encode( $values[ $prefix . 'icon_picker'] );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$values[ $prefix . static::$iconColumn ] = $iconValue;
|
||||
|
||||
unset( $values[ $prefix . 'icon_choose' ], $values[ $prefix . 'icon_upload' ], $values[ $prefix . 'icon_picker' ] );
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the icon was uploaded, return the file
|
||||
*
|
||||
* @return File|null
|
||||
*/
|
||||
public function getIconFile() : ?File
|
||||
{
|
||||
$iconColumn = static::$iconColumn;
|
||||
if( $this->icon_type == 'upload' )
|
||||
{
|
||||
try
|
||||
{
|
||||
return File::get( static::iconStorageExtension(), $this->$iconColumn );
|
||||
}
|
||||
catch( Exception $e ){}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return icon as a string
|
||||
*
|
||||
* @return string
|
||||
* @throws ErrorException
|
||||
*/
|
||||
public function getIcon(): string
|
||||
{
|
||||
$iconColumn = static::$iconColumn;
|
||||
if ( $iconData = json_decode( $this->$iconColumn, true ) )
|
||||
{
|
||||
$iconData = $iconData[0];
|
||||
|
||||
if ( isset( $iconData['html'] ) )
|
||||
{
|
||||
return $iconData['html'];
|
||||
}
|
||||
}
|
||||
else if ( ! empty( $this->$iconColumn ) and $iconFile = $this->getIconFile() )
|
||||
{
|
||||
/* Must be an upload */
|
||||
return Theme::i()->getTemplate( 'global', 'core', 'front' )->uploadedIcon( $iconFile );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
+904
-727
File diff suppressed because it is too large.
Load diff
@@ -11,9 +11,12 @@
|
||||
namespace IPS\Node;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
|
||||
use function defined;
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( isset( $_SERVER['SERVER_PROTOCOL'] ) ? $_SERVER['SERVER_PROTOCOL'] : 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
+128
-7
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Rating Interface for Nodes
|
||||
* @brief Rating Trait for Nodes
|
||||
* @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/
|
||||
@@ -10,18 +10,139 @@
|
||||
|
||||
namespace IPS\Node;
|
||||
|
||||
use IPS\Member;
|
||||
use IPS\Db;
|
||||
use IPS\Helpers\Form;
|
||||
use IPS\Helpers\Form\Rating;
|
||||
use IPS\Request;
|
||||
use IPS\Output;
|
||||
use IPS\Theme;
|
||||
use UnderflowException;
|
||||
|
||||
|
||||
use function defined;
|
||||
use function header;
|
||||
use function get_called_class;
|
||||
use function round;
|
||||
use function time;
|
||||
|
||||
/* 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' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Rating Interface for Nodes
|
||||
*
|
||||
* @note Node classes will gain special functionality by implementing this interface
|
||||
* Rating Trait for Nodes
|
||||
*/
|
||||
interface Ratings
|
||||
trait Ratings
|
||||
{
|
||||
/**
|
||||
* Can Rate?
|
||||
*
|
||||
* @param Member|NULL $member The member to check for (NULL for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canRate( Member|null $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: Member::loggedIn();
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
Db::i()->select( '*', 'core_ratings', array( 'class=? AND item_id=? AND `member`=?', get_called_class(), $this->$idColumn, $member->member_id ) )->first();
|
||||
return FALSE;
|
||||
}
|
||||
catch ( UnderflowException $e )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get average rating
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function averageRating(): int
|
||||
{
|
||||
if ( isset( static::$ratingColumnMap['rating_average'] ) )
|
||||
{
|
||||
$column = static::$ratingColumnMap['rating_average'];
|
||||
return $this->$column;
|
||||
}
|
||||
elseif ( isset( static::$ratingColumnMap['rating_total'] ) and isset( static::$ratingColumnMap['rating_hits'] ) )
|
||||
{
|
||||
$hits = static::$ratingColumnMap['rating_hits'];
|
||||
$total = static::$ratingColumnMap['rating_total'];
|
||||
return $this->$hits ? round( $this->$total / $this->$hits, 1 ) : 0;
|
||||
}
|
||||
else
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
return round( Db::i()->select( 'AVG(rating)', 'core_ratings', array( 'class=? AND item_id=?', get_called_class(), $this->$idColumn ) )->first(), 1 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display rating (will just display stars if member cannot rate)
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rating(): string
|
||||
{
|
||||
if ( $this->canRate() )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
$form = new Form('rating');
|
||||
$form->add( new Rating( 'rating', $this->averageRating() ) );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
Db::i()->insert( 'core_ratings', array(
|
||||
'class' => get_called_class(),
|
||||
'item_id' => $this->$idColumn,
|
||||
'member' => Member::loggedIn()->member_id,
|
||||
'rating' => $values['rating'],
|
||||
'ip' => Request::i()->ipAddress(),
|
||||
'rating_date' => time()
|
||||
), TRUE );
|
||||
|
||||
if ( isset( static::$ratingColumnMap['rating_average'] ) )
|
||||
{
|
||||
$column = static::$ratingColumnMap['rating_average'];
|
||||
$this->$column = round( Db::i()->select( 'AVG(rating)', 'core_ratings', array( 'class=? AND item_id=?', get_called_class(), $this->$idColumn ) )->first(), 1 );
|
||||
}
|
||||
if ( isset( static::$ratingColumnMap['rating_total'] ) )
|
||||
{
|
||||
$column = static::$ratingColumnMap['rating_total'];
|
||||
$this->$column = Db::i()->select( 'SUM(rating)', 'core_ratings', array( 'class=? AND item_id=?', get_called_class(), $this->$idColumn ) )->first();
|
||||
}
|
||||
if ( isset( static::$ratingColumnMap['rating_hits'] ) )
|
||||
{
|
||||
$column = static::$ratingColumnMap['rating_hits'];
|
||||
$this->$column = Db::i()->select( 'COUNT(*)', 'core_ratings', array( 'class=? AND item_id=?', get_called_class(), $this->$idColumn ) )->first();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
|
||||
if ( Request::i()->isAjax() )
|
||||
{
|
||||
Output::i()->json( 'OK' );
|
||||
}
|
||||
}
|
||||
|
||||
return $form->customTemplate( array( Theme::i()->getTemplate( 'forms', 'core' ), 'ratingTemplate' ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
return Theme::i()->getTemplate( 'global', 'core' )->rating( 'veryLarge', $this->averageRating() );
|
||||
}
|
||||
}
|
||||
}
|
||||
+72
-68
@@ -13,10 +13,17 @@ namespace IPS\Node;
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use Exception;
|
||||
use IPS\Content\Comment;
|
||||
use IPS\Content\Item;
|
||||
use IPS\Db;
|
||||
use IPS\Member;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function is_array;
|
||||
|
||||
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' );
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
exit;
|
||||
}
|
||||
|
||||
@@ -28,7 +35,7 @@ trait Statistics
|
||||
/**
|
||||
* @brief Cached array of IDs a member has posted in
|
||||
*/
|
||||
protected $authorsPostedIn = array();
|
||||
protected array $authorsPostedIn = array();
|
||||
|
||||
/**
|
||||
* Get authors posted in
|
||||
@@ -36,7 +43,7 @@ trait Statistics
|
||||
* @param array $inSet The item_ids to check
|
||||
* @return array
|
||||
*/
|
||||
public function authorsPostedIn( array $inSet )
|
||||
public function authorsPostedIn( array $inSet ): array
|
||||
{
|
||||
if ( !isset( static::$contentItemClass ) )
|
||||
{
|
||||
@@ -59,12 +66,12 @@ trait Statistics
|
||||
return array();
|
||||
}
|
||||
|
||||
$items = iterator_to_array( \IPS\Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array( array( 'map_class=?', $contentItemClass ), array( \IPS\Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
|
||||
$items = iterator_to_array( Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array( array( 'map_class=?', $contentItemClass ), array( Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
|
||||
$reload = FALSE;
|
||||
$itemIds = array();
|
||||
|
||||
/* Do we have the items we need? */
|
||||
if ( \count( $items ) )
|
||||
if ( count( $items ) )
|
||||
{
|
||||
foreach ( $items as $row )
|
||||
{
|
||||
@@ -73,7 +80,7 @@ trait Statistics
|
||||
|
||||
$diff = array_diff( $inSet, $itemIds );
|
||||
|
||||
if ( \count( $diff ) )
|
||||
if ( count( $diff ) )
|
||||
{
|
||||
$reload = TRUE;
|
||||
$this->rebuildPostedIn( $diff );
|
||||
@@ -89,10 +96,10 @@ trait Statistics
|
||||
/* Do we need to reload? */
|
||||
if ( $reload )
|
||||
{
|
||||
$items = iterator_to_array( \IPS\Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array(array('map_class=?', $contentItemClass), array(\IPS\Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
|
||||
$items = iterator_to_array( Db::i()->select( 'map_item_id, map_member_id', 'core_item_member_map', array(array('map_class=?', $contentItemClass), array(Db::i()->in( 'map_item_id', $inSet ) ) ) ) );
|
||||
$itemIds = [];
|
||||
|
||||
if ( \count( $items ) )
|
||||
if ( count( $items ) )
|
||||
{
|
||||
foreach ( $items as $row )
|
||||
{
|
||||
@@ -105,11 +112,11 @@ trait Statistics
|
||||
/* Check to see if there are any item IDs that can't be set because of missing posts, etc. This prevents an attempted rebuild from occuring over and over again */
|
||||
$diff = array_diff( $inSet, $itemIds );
|
||||
|
||||
if ( \count( $diff ) )
|
||||
if ( count( $diff ) )
|
||||
{
|
||||
foreach( $diff as $id )
|
||||
{
|
||||
\IPS\Db::i()->replace( 'core_item_member_map', array( 'map_class' => $contentItemClass, 'map_item_id' => $id, 'map_member_id' => NULL ), TRUE );
|
||||
Db::i()->replace( 'core_item_member_map', array( 'map_class' => $contentItemClass, 'map_item_id' => $id, 'map_member_id' => NULL ), TRUE );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,22 +136,22 @@ trait Statistics
|
||||
/**
|
||||
* @brief Cached array of IDs a member has posted in (calculated the expensive way so cached separately)
|
||||
*/
|
||||
protected $contentPostedIn = array();
|
||||
protected array $contentPostedIn = array();
|
||||
|
||||
/**
|
||||
* Retrieve an array of IDs a member has posted in.
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member (NULL for currently logged in member)
|
||||
* @param array|NULL $inSet If supplied, checks will be restricted to only the ids provided
|
||||
* @param array|NULL $additionalWhere Additional where clause
|
||||
* @param array|NULL $commentJoinWhere Additional join clause for comments table
|
||||
* @param Member|null $member The member (NULL for currently logged in member)
|
||||
* @param array|null $inSet If supplied, checks will be restricted to only the ids provided
|
||||
* @param array|null $additionalWhere Additional where clause
|
||||
* @param array|null $commentJoinWhere Additional join clause for comments table
|
||||
* @return array An array of content item ids
|
||||
*/
|
||||
public function contentPostedIn( $member=NULL, $inSet=NULL, $additionalWhere=NULL, $commentJoinWhere=NULL )
|
||||
public function contentPostedIn(Member $member=NULL, array $inSet=NULL, array $additionalWhere=NULL, array $commentJoinWhere=NULL ): array
|
||||
{
|
||||
if ( $member === NULL )
|
||||
{
|
||||
$member = \IPS\Member::loggedIn();
|
||||
$member = Member::loggedIn();
|
||||
}
|
||||
|
||||
if( !$member->member_id )
|
||||
@@ -157,7 +164,7 @@ trait Statistics
|
||||
return array();
|
||||
}
|
||||
|
||||
if ( \is_array( $inSet ) AND $additionalWhere === NULL AND $commentJoinWhere === NULL )
|
||||
if ( is_array( $inSet ) AND $additionalWhere === NULL AND $commentJoinWhere === NULL )
|
||||
{
|
||||
/* Hooray, the easy efficient way */
|
||||
$members = $this->authorsPostedIn( $inSet );
|
||||
@@ -172,7 +179,10 @@ trait Statistics
|
||||
else
|
||||
{
|
||||
/* The complicated way, which doesn't happen that often */
|
||||
/* @var Item $contentItemClass */
|
||||
$contentItemClass = static::$contentItemClass;
|
||||
|
||||
/* @var Comment $commentClass */
|
||||
$commentClass = $contentItemClass::$commentClass;
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$_key = md5( $member->member_id . json_encode( $inSet ) );
|
||||
@@ -184,6 +194,7 @@ trait Statistics
|
||||
|
||||
$where = array();
|
||||
|
||||
/* @var array $databaseColumnMap */
|
||||
if ( $contentItemClass::$firstCommentRequired )
|
||||
{
|
||||
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? )', $member->member_id );
|
||||
@@ -194,7 +205,7 @@ trait Statistics
|
||||
$where[] = array( '(' . $commentClass::$databaseTable . '.' . $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'] . '=? OR ' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['author'] . '=?)', $member->member_id, $member->member_id );
|
||||
}
|
||||
|
||||
if( \is_array( $inSet ) AND \count( $inSet ) )
|
||||
if( is_array( $inSet ) AND count( $inSet ) )
|
||||
{
|
||||
$where[] = array( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId . ' IN(' . implode( ',', $inSet ) . ')' );
|
||||
}
|
||||
@@ -206,7 +217,7 @@ trait Statistics
|
||||
|
||||
$joinClause = array( $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'] . '=' . $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId );
|
||||
|
||||
$items = \IPS\Db::i()->select( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $contentItemClass::$databaseTable, $where, NULL, NULL, NULL, NULL, \IPS\Db::SELECT_DISTINCT );
|
||||
$items = Db::i()->select( $contentItemClass::$databaseTable . '.' . $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $contentItemClass::$databaseTable, $where, NULL, NULL, NULL, NULL, Db::SELECT_DISTINCT );
|
||||
$items->join( $commentClass::$databaseTable, ( $commentJoinWhere !== NULL ) ? array( $joinClause, $commentJoinWhere ) : $joinClause );
|
||||
|
||||
$ids = array();
|
||||
@@ -227,30 +238,32 @@ trait Statistics
|
||||
* Note that we do fetch guests content here. That is to prevent content from being rebuilt on each click if it solely populated by guests
|
||||
*
|
||||
* @param array $inSet Array of item IDs
|
||||
* @param array $members Optional array of member objects to limit by
|
||||
* @param array $members Optional array of member objects to limit by\
|
||||
* @return void
|
||||
*/
|
||||
public function rebuildPostedIn( array $inSet, $members=array() )
|
||||
public function rebuildPostedIn( array $inSet, array $members=array() ) : void
|
||||
{
|
||||
/* @var array $databaseColumnMap */
|
||||
$contentItemClass = static::$contentItemClass;
|
||||
$commentClass = $contentItemClass::$commentClass;
|
||||
$commentItemField = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['item'];
|
||||
$authorColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['author'];
|
||||
$where = array();
|
||||
$memberIds = array();
|
||||
|
||||
$deleteWhere = array( array( \IPS\Db::i()->in( 'map_item_id', $inSet ) ) );
|
||||
if ( \count( $members ) )
|
||||
$deleteWhere = array( array( Db::i()->in( 'map_item_id', $inSet ) ) );
|
||||
if ( count( $members ) )
|
||||
{
|
||||
$memberIds = array();
|
||||
foreach ( $members as $member )
|
||||
{
|
||||
$memberIds[] = $member->member_id;
|
||||
}
|
||||
|
||||
$where[] = array(\IPS\Db::i()->in( $authorColumn, $memberIds ));
|
||||
$deleteWhere[] = array( \IPS\Db::i()->in( 'map_member_id', $memberIds ) );
|
||||
$where[] = array(Db::i()->in( $authorColumn, $memberIds ));
|
||||
$deleteWhere[] = array( Db::i()->in( 'map_member_id', $memberIds ) );
|
||||
}
|
||||
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', $deleteWhere );
|
||||
Db::i()->delete( 'core_item_member_map', $deleteWhere );
|
||||
|
||||
/* Do the item first if the first comment is not required */
|
||||
if ( ! $contentItemClass::$firstCommentRequired )
|
||||
@@ -261,18 +274,38 @@ trait Statistics
|
||||
if ( isset( $contentItemClass::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$approvedColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['approved'];
|
||||
$itemWhere[] = array( \IPS\Db::i()->in( $approvedColumn, array( 1, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
|
||||
$itemWhere[] = array( Db::i()->in( $approvedColumn, array( 1, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
|
||||
}
|
||||
if ( isset( $contentItemClass::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$hiddenColumn = $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnMap['hidden'];
|
||||
$itemWhere[] = array( \IPS\Db::i()->in( $hiddenColumn, array( 0, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
|
||||
$itemWhere[] = array( Db::i()->in( $hiddenColumn, array( 0, 2 ) ) ); # We want approved comments but also comments hidden because the item is hidden
|
||||
}
|
||||
|
||||
$itemWhere[] = array( \IPS\Db::i()->in( $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $inSet ) );
|
||||
$itemWhere[] = array( Db::i()->in( $contentItemClass::$databasePrefix . $contentItemClass::$databaseColumnId, $inSet ) );
|
||||
|
||||
$insertFromSelect = \IPS\Db::i()->select( "'" . str_replace( '\\', '\\\\', $contentItemClass ) . "', `{$contentItemClass::$databasePrefix}{$contentItemClass::$databaseColumnId}`, `{$itemAuthorColumn}`, `{$itemDateColumn}`" , $contentItemClass::$databaseTable, $itemWhere );
|
||||
\IPS\Db::i()->replace( 'core_item_member_map', $insertFromSelect, TRUE );
|
||||
try
|
||||
{
|
||||
$seen = array();
|
||||
/* We don't want a complex query on a large table (forums_posts) hitting a write server, so split this for forums to select (read server) and insert (write server) */
|
||||
foreach( Db::i()->select( "`{$authorColumn}`, `{$commentItemField}`", $commentClass::$databaseTable, $where, NULL, 1 ) as $row )
|
||||
{
|
||||
$key = $row[ $authorColumn ] . '_' . $row[ $commentItemField ];
|
||||
if ( isset( $seen[ $key ] ) or ! $row[ $authorColumn ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$seen[ $key ] = true;
|
||||
|
||||
Db::i()->insert( 'core_item_member_map', [
|
||||
'map_member_id' => $row[ $authorColumn ],
|
||||
'map_item_id' => $row[ $commentItemField ],
|
||||
'map_latest_date' => time(),
|
||||
'map_class' => $contentItemClass
|
||||
] );
|
||||
}
|
||||
}
|
||||
catch( Exception ) { }
|
||||
}
|
||||
|
||||
/* If forums are being used for Pages comments, we don't want to look at those */
|
||||
@@ -285,12 +318,12 @@ trait Statistics
|
||||
if ( isset( $commentClass::$databaseColumnMap['approved'] ) )
|
||||
{
|
||||
$approvedColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['approved'];
|
||||
$where[] = array(\IPS\Db::i()->in( $approvedColumn, array(1, 2) )); # We want approved comments but also comments hidden because the item is hidden
|
||||
$where[] = array(Db::i()->in( $approvedColumn, array(1, 2) )); # We want approved comments but also comments hidden because the item is hidden
|
||||
}
|
||||
if ( isset( $commentClass::$databaseColumnMap['hidden'] ) )
|
||||
{
|
||||
$hiddenColumn = $commentClass::$databasePrefix . $commentClass::$databaseColumnMap['hidden'];
|
||||
$where[] = array(\IPS\Db::i()->in( $hiddenColumn, array(0, 2) )); # We want approved comments but also comments hidden because the item is hidden
|
||||
$where[] = array(Db::i()->in( $hiddenColumn, array(0, 2) )); # We want approved comments but also comments hidden because the item is hidden
|
||||
}
|
||||
|
||||
if ( $commentClass::commentWhere() !== NULL )
|
||||
@@ -298,39 +331,10 @@ trait Statistics
|
||||
$where[] = $commentClass::commentWhere();
|
||||
}
|
||||
|
||||
$where[] = array(\IPS\Db::i()->in( $commentItemField, $inSet ));
|
||||
$where[] = array(Db::i()->in( $commentItemField, $inSet ));
|
||||
|
||||
try
|
||||
{
|
||||
$seen = array();
|
||||
|
||||
/* If we don't have member(s) in the where clause, or we have a lot of member ids in the where clause, we can't do individual inserts as this could be 10,000+ inserts on very large topics */
|
||||
if ( ! count( $memberIds ) or ( count( $memberIds ) > 100 ) )
|
||||
{
|
||||
$insertFromSelect = \IPS\Db::i()->select( "DISTINCT `{$authorColumn}`, `{$commentItemField}`, UNIX_TIMESTAMP(), '" . str_replace( '\\', '\\\\', $contentItemClass ) . "'", $commentClass::$databaseTable, $where );
|
||||
\IPS\Db::i()->replace( 'core_item_member_map', [ 'map_member_id, map_item_id, map_latest_date, map_class', $insertFromSelect ], TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( \IPS\Db::i()->select( "DISTINCT `{$authorColumn}` as {$authorColumn}, `{$commentItemField}`", $commentClass::$databaseTable, $where ) as $row )
|
||||
{
|
||||
$key = $row[ $authorColumn ] . '_' . $row[ $commentItemField ];
|
||||
if ( isset( $seen[ $key ] ) or ! $row[ $authorColumn ] )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
$seen[ $key ] = true;
|
||||
|
||||
\IPS\Db::i()->insert( 'core_item_member_map', [
|
||||
'map_member_id' => $row[ $authorColumn ],
|
||||
'map_item_id' => $row[ $commentItemField ],
|
||||
'map_latest_date' => time(),
|
||||
'map_class' => $contentItemClass
|
||||
] );
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( Exception ) { }
|
||||
$insertFromSelect = Db::i()->select( "DISTINCT `{$authorColumn}`, `{$commentItemField}`, UNIX_TIMESTAMP(), '" . str_replace( '\\', '\\\\', $contentItemClass ) . "'", $commentClass::$databaseTable, $where );
|
||||
Db::i()->replace( 'core_item_member_map', [ 'map_member_id, map_item_id, map_latest_date, map_class', $insertFromSelect ], TRUE );
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user