Version 4.7.6
This commit is contained in:
1 parent
8ba7d43898
commit
7b7404e741
1085 files changed
+30590
-7837
No files matched your search
@@ -10,6 +10,7 @@ use GraphQL\Language\Source;
|
||||
use GraphQL\Language\SourceLocation;
|
||||
use GraphQL\Utils\Utils;
|
||||
use JsonSerializable;
|
||||
use ReturnTypeWillChange;
|
||||
use Throwable;
|
||||
use Traversable;
|
||||
use function array_filter;
|
||||
@@ -367,6 +368,7 @@ class Error extends Exception implements JsonSerializable, ClientAware
|
||||
* @return mixed data which can be serialized by <b>json_encode</b>,
|
||||
* which is a value of any type other than a resource.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
|
||||
+5
-5
@@ -39,7 +39,7 @@ final class Warning
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function setWarningHandler(?callable $warningHandler = null)
|
||||
public static function setWarningHandler(?callable $warningHandler = null) : void
|
||||
{
|
||||
self::$warningHandler = $warningHandler;
|
||||
}
|
||||
@@ -56,7 +56,7 @@ final class Warning
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function suppress($suppress = true)
|
||||
public static function suppress($suppress = true) : void
|
||||
{
|
||||
if ($suppress === true) {
|
||||
self::$enableWarnings = 0;
|
||||
@@ -81,7 +81,7 @@ final class Warning
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function enable($enable = true)
|
||||
public static function enable($enable = true) : void
|
||||
{
|
||||
if ($enable === true) {
|
||||
self::$enableWarnings = self::ALL;
|
||||
@@ -94,7 +94,7 @@ final class Warning
|
||||
}
|
||||
}
|
||||
|
||||
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null)
|
||||
public static function warnOnce(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
$messageLevel = $messageLevel ?? E_USER_WARNING;
|
||||
|
||||
@@ -107,7 +107,7 @@ final class Warning
|
||||
}
|
||||
}
|
||||
|
||||
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null)
|
||||
public static function warn(string $errorMessage, int $warningId, ?int $messageLevel = null) : void
|
||||
{
|
||||
$messageLevel = $messageLevel ?? E_USER_WARNING;
|
||||
|
||||
|
||||
@@ -115,7 +115,7 @@ class ExecutionResult implements JsonSerializable
|
||||
/**
|
||||
* @return mixed[]
|
||||
*/
|
||||
public function jsonSerialize()
|
||||
public function jsonSerialize() : array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
|
||||
+48
-49
@@ -55,18 +55,18 @@ use function sprintf;
|
||||
class ReferenceExecutor implements ExecutorImplementation
|
||||
{
|
||||
/** @var object */
|
||||
private static $UNDEFINED;
|
||||
protected static $UNDEFINED;
|
||||
|
||||
/** @var ExecutionContext */
|
||||
private $exeContext;
|
||||
protected $exeContext;
|
||||
|
||||
/** @var SplObjectStorage */
|
||||
private $subFieldCache;
|
||||
protected $subFieldCache;
|
||||
|
||||
private function __construct(ExecutionContext $context)
|
||||
protected function __construct(ExecutionContext $context)
|
||||
{
|
||||
if (! self::$UNDEFINED) {
|
||||
self::$UNDEFINED = Utils::undefined();
|
||||
if (! static::$UNDEFINED) {
|
||||
static::$UNDEFINED = Utils::undefined();
|
||||
}
|
||||
$this->exeContext = $context;
|
||||
$this->subFieldCache = new SplObjectStorage();
|
||||
@@ -87,7 +87,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
?string $operationName,
|
||||
callable $fieldResolver
|
||||
) : ExecutorImplementation {
|
||||
$exeContext = self::buildExecutionContext(
|
||||
$exeContext = static::buildExecutionContext(
|
||||
$schema,
|
||||
$documentNode,
|
||||
$rootValue,
|
||||
@@ -116,7 +116,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
};
|
||||
}
|
||||
|
||||
return new self($exeContext);
|
||||
return new static($exeContext);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +129,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return ExecutionContext|array<Error>
|
||||
*/
|
||||
private static function buildExecutionContext(
|
||||
protected static function buildExecutionContext(
|
||||
Schema $schema,
|
||||
DocumentNode $documentNode,
|
||||
$rootValue,
|
||||
@@ -227,7 +227,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return ExecutionResult|Promise
|
||||
*/
|
||||
private function buildResponse($data)
|
||||
protected function buildResponse($data)
|
||||
{
|
||||
if ($this->isPromise($data)) {
|
||||
return $data->then(function ($resolved) {
|
||||
@@ -248,7 +248,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return array<mixed>|Promise|stdClass|null
|
||||
*/
|
||||
private function executeOperation(OperationDefinitionNode $operation, $rootValue)
|
||||
protected function executeOperation(OperationDefinitionNode $operation, $rootValue)
|
||||
{
|
||||
$type = $this->getOperationRootType($this->exeContext->schema, $operation);
|
||||
$fields = $this->collectFields($type, $operation->selectionSet, new ArrayObject(), new ArrayObject());
|
||||
@@ -290,7 +290,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType
|
||||
protected function getOperationRootType(Schema $schema, OperationDefinitionNode $operation) : ObjectType
|
||||
{
|
||||
switch ($operation->operation) {
|
||||
case 'query':
|
||||
@@ -339,7 +339,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
* returns an Interface or Union type, the "runtime type" will be the actual
|
||||
* Object type returned by that field.
|
||||
*/
|
||||
private function collectFields(
|
||||
protected function collectFields(
|
||||
ObjectType $runtimeType,
|
||||
SelectionSetNode $selectionSet,
|
||||
ArrayObject $fields,
|
||||
@@ -352,7 +352,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
if (! $this->shouldIncludeNode($selection)) {
|
||||
break;
|
||||
}
|
||||
$name = self::getFieldEntryKey($selection);
|
||||
$name = static::getFieldEntryKey($selection);
|
||||
if (! isset($fields[$name])) {
|
||||
$fields[$name] = new ArrayObject();
|
||||
}
|
||||
@@ -402,7 +402,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @param FragmentSpreadNode|FieldNode|InlineFragmentNode $node
|
||||
*/
|
||||
private function shouldIncludeNode(SelectionNode $node) : bool
|
||||
protected function shouldIncludeNode(SelectionNode $node) : bool
|
||||
{
|
||||
$variableValues = $this->exeContext->variableValues;
|
||||
$skipDirective = Directive::skipDirective();
|
||||
@@ -427,7 +427,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
/**
|
||||
* Implements the logic to compute the key of a given fields entry
|
||||
*/
|
||||
private static function getFieldEntryKey(FieldNode $node) : string
|
||||
protected static function getFieldEntryKey(FieldNode $node) : string
|
||||
{
|
||||
return $node->alias === null ? $node->name->value : $node->alias->value;
|
||||
}
|
||||
@@ -437,7 +437,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @param FragmentDefinitionNode|InlineFragmentNode $fragment
|
||||
*/
|
||||
private function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool
|
||||
protected function doesFragmentConditionMatch(Node $fragment, ObjectType $type) : bool
|
||||
{
|
||||
$typeConditionNode = $fragment->typeCondition;
|
||||
if ($typeConditionNode === null) {
|
||||
@@ -463,7 +463,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return array<mixed>|Promise|stdClass
|
||||
*/
|
||||
private function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
|
||||
protected function executeFieldsSerially(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
|
||||
{
|
||||
$result = $this->promiseReduce(
|
||||
array_keys($fields->getArrayCopy()),
|
||||
@@ -472,7 +472,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
$fieldPath = $path;
|
||||
$fieldPath[] = $responseName;
|
||||
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
|
||||
if ($result === self::$UNDEFINED) {
|
||||
if ($result === static::$UNDEFINED) {
|
||||
return $results;
|
||||
}
|
||||
$promise = $this->getPromise($result);
|
||||
@@ -492,11 +492,11 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
|
||||
if ($this->isPromise($result)) {
|
||||
return $result->then(static function ($resolvedResults) {
|
||||
return self::fixResultsIfEmptyArray($resolvedResults);
|
||||
return static::fixResultsIfEmptyArray($resolvedResults);
|
||||
});
|
||||
}
|
||||
|
||||
return self::fixResultsIfEmptyArray($result);
|
||||
return static::fixResultsIfEmptyArray($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -511,14 +511,14 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return array<mixed>|Throwable|mixed|null
|
||||
*/
|
||||
private function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path)
|
||||
protected function resolveField(ObjectType $parentType, $rootValue, ArrayObject $fieldNodes, array $path)
|
||||
{
|
||||
$exeContext = $this->exeContext;
|
||||
$fieldNode = $fieldNodes[0];
|
||||
$fieldName = $fieldNode->name->value;
|
||||
$fieldDef = $this->getFieldDef($exeContext->schema, $parentType, $fieldName);
|
||||
if ($fieldDef === null) {
|
||||
return self::$UNDEFINED;
|
||||
return static::$UNDEFINED;
|
||||
}
|
||||
$returnType = $fieldDef->getType();
|
||||
// The resolve function's optional 3rd argument is a context value that
|
||||
@@ -574,7 +574,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
* added to the query type, but that would require mutating type
|
||||
* definitions, which would cause issues.
|
||||
*/
|
||||
private function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
|
||||
protected function getFieldDef(Schema $schema, ObjectType $parentType, string $fieldName) : ?FieldDefinition
|
||||
{
|
||||
static $schemaMetaFieldDef, $typeMetaFieldDef, $typeNameMetaFieldDef;
|
||||
$schemaMetaFieldDef = $schemaMetaFieldDef ?? Introspection::schemaMetaFieldDef();
|
||||
@@ -591,9 +591,8 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
if ($fieldName === $typeNameMetaFieldDef->name) {
|
||||
return $typeNameMetaFieldDef;
|
||||
}
|
||||
$tmp = $parentType->getFields();
|
||||
|
||||
return $tmp[$fieldName] ?? null;
|
||||
return $parentType->findField($fieldName);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -604,7 +603,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return Throwable|Promise|mixed
|
||||
*/
|
||||
private function resolveFieldValueOrError(
|
||||
protected function resolveFieldValueOrError(
|
||||
FieldDefinition $fieldDef,
|
||||
FieldNode $fieldNode,
|
||||
callable $resolveFn,
|
||||
@@ -636,7 +635,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return array<mixed>|Promise|stdClass|null
|
||||
*/
|
||||
private function completeValueCatchingError(
|
||||
protected function completeValueCatchingError(
|
||||
Type $returnType,
|
||||
ArrayObject $fieldNodes,
|
||||
ResolveInfo $info,
|
||||
@@ -676,7 +675,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void
|
||||
protected function handleFieldError($rawError, ArrayObject $fieldNodes, array $path, Type $returnType) : void
|
||||
{
|
||||
$error = Error::createLocatedError(
|
||||
$rawError,
|
||||
@@ -723,7 +722,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
* @throws Error
|
||||
* @throws Throwable
|
||||
*/
|
||||
private function completeValue(
|
||||
protected function completeValue(
|
||||
Type $returnType,
|
||||
ArrayObject $fieldNodes,
|
||||
ResolveInfo $info,
|
||||
@@ -799,7 +798,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
/**
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function isPromise($value) : bool
|
||||
protected function isPromise($value) : bool
|
||||
{
|
||||
return $value instanceof Promise || $this->exeContext->promiseAdapter->isThenable($value);
|
||||
}
|
||||
@@ -810,7 +809,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @param mixed $value
|
||||
*/
|
||||
private function getPromise($value) : ?Promise
|
||||
protected function getPromise($value) : ?Promise
|
||||
{
|
||||
if ($value === null || $value instanceof Promise) {
|
||||
return $value;
|
||||
@@ -843,7 +842,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return Promise|mixed|null
|
||||
*/
|
||||
private function promiseReduce(array $values, callable $callback, $initialValue)
|
||||
protected function promiseReduce(array $values, callable $callback, $initialValue)
|
||||
{
|
||||
return array_reduce(
|
||||
$values,
|
||||
@@ -871,7 +870,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results)
|
||||
protected function completeListValue(ListOfType $returnType, ArrayObject $fieldNodes, ResolveInfo $info, array $path, &$results)
|
||||
{
|
||||
$itemType = $returnType->getWrappedType();
|
||||
Utils::invariant(
|
||||
@@ -906,7 +905,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function completeLeafValue(LeafType $returnType, &$result)
|
||||
protected function completeLeafValue(LeafType $returnType, &$result)
|
||||
{
|
||||
try {
|
||||
return $returnType->serialize($result);
|
||||
@@ -930,7 +929,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function completeAbstractValue(
|
||||
protected function completeAbstractValue(
|
||||
AbstractType $returnType,
|
||||
ArrayObject $fieldNodes,
|
||||
ResolveInfo $info,
|
||||
@@ -941,7 +940,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
$typeCandidate = $returnType->resolveType($result, $exeContext->contextValue, $info);
|
||||
|
||||
if ($typeCandidate === null) {
|
||||
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
|
||||
$runtimeType = static::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);
|
||||
} elseif (is_callable($typeCandidate)) {
|
||||
$runtimeType = Schema::resolveType($typeCandidate);
|
||||
} else {
|
||||
@@ -1001,7 +1000,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return Promise|Type|string|null
|
||||
*/
|
||||
private function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
|
||||
protected function defaultTypeResolver($value, $contextValue, ResolveInfo $info, AbstractType $abstractType)
|
||||
{
|
||||
// First, look for `__typename`.
|
||||
if ($value !== null &&
|
||||
@@ -1066,7 +1065,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function completeObjectValue(
|
||||
protected function completeObjectValue(
|
||||
ObjectType $returnType,
|
||||
ArrayObject $fieldNodes,
|
||||
ResolveInfo $info,
|
||||
@@ -1116,7 +1115,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return Error
|
||||
*/
|
||||
private function invalidReturnTypeError(
|
||||
protected function invalidReturnTypeError(
|
||||
ObjectType $returnType,
|
||||
$result,
|
||||
ArrayObject $fieldNodes
|
||||
@@ -1135,7 +1134,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @throws Error
|
||||
*/
|
||||
private function collectAndExecuteSubfields(
|
||||
protected function collectAndExecuteSubfields(
|
||||
ObjectType $returnType,
|
||||
ArrayObject $fieldNodes,
|
||||
array $path,
|
||||
@@ -1151,7 +1150,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
* type. Memoizing ensures the subfields are not repeatedly calculated, which
|
||||
* saves overhead when resolving lists of values.
|
||||
*/
|
||||
private function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject
|
||||
protected function collectSubFields(ObjectType $returnType, ArrayObject $fieldNodes) : ArrayObject
|
||||
{
|
||||
if (! isset($this->subFieldCache[$returnType])) {
|
||||
$this->subFieldCache[$returnType] = new SplObjectStorage();
|
||||
@@ -1186,7 +1185,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return Promise|stdClass|array<mixed>
|
||||
*/
|
||||
private function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
|
||||
protected function executeFields(ObjectType $parentType, $rootValue, array $path, ArrayObject $fields)
|
||||
{
|
||||
$containsPromise = false;
|
||||
$results = [];
|
||||
@@ -1194,7 +1193,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
$fieldPath = $path;
|
||||
$fieldPath[] = $responseName;
|
||||
$result = $this->resolveField($parentType, $rootValue, $fieldNodes, $fieldPath);
|
||||
if ($result === self::$UNDEFINED) {
|
||||
if ($result === static::$UNDEFINED) {
|
||||
continue;
|
||||
}
|
||||
if (! $containsPromise && $this->isPromise($result)) {
|
||||
@@ -1204,7 +1203,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
}
|
||||
// If there are no promises, we can just return the object
|
||||
if (! $containsPromise) {
|
||||
return self::fixResultsIfEmptyArray($results);
|
||||
return static::fixResultsIfEmptyArray($results);
|
||||
}
|
||||
|
||||
// Otherwise, results is a map from field name to the result of resolving that
|
||||
@@ -1222,7 +1221,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @return array<mixed>|stdClass|mixed
|
||||
*/
|
||||
private static function fixResultsIfEmptyArray($results)
|
||||
protected static function fixResultsIfEmptyArray($results)
|
||||
{
|
||||
if ($results === []) {
|
||||
return new stdClass();
|
||||
@@ -1237,7 +1236,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
*
|
||||
* @param array<string, Promise|mixed> $assoc
|
||||
*/
|
||||
private function promiseForAssocArray(array $assoc) : Promise
|
||||
protected function promiseForAssocArray(array $assoc) : Promise
|
||||
{
|
||||
$keys = array_keys($assoc);
|
||||
$valuesAndPromises = array_values($assoc);
|
||||
@@ -1249,7 +1248,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
$resolvedResults[$keys[$i]] = $value;
|
||||
}
|
||||
|
||||
return self::fixResultsIfEmptyArray($resolvedResults);
|
||||
return static::fixResultsIfEmptyArray($resolvedResults);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1258,7 +1257,7 @@ class ReferenceExecutor implements ExecutorImplementation
|
||||
* @param InterfaceType|UnionType $returnType
|
||||
* @param mixed $result
|
||||
*/
|
||||
private function ensureValidRuntimeType(
|
||||
protected function ensureValidRuntimeType(
|
||||
$runtimeTypeOrName,
|
||||
AbstractType $returnType,
|
||||
ResolveInfo $info,
|
||||
|
||||
@@ -9,6 +9,7 @@ use Countable;
|
||||
use GraphQL\Utils\AST;
|
||||
use InvalidArgumentException;
|
||||
use IteratorAggregate;
|
||||
use ReturnTypeWillChange;
|
||||
use Traversable;
|
||||
use function array_merge;
|
||||
use function array_splice;
|
||||
@@ -69,6 +70,7 @@ class NodeList implements ArrayAccess, IteratorAggregate, Countable
|
||||
*
|
||||
* @phpstan-return T
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetGet($offset)// : Node
|
||||
{
|
||||
$item = $this->nodes[$offset];
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace GraphQL\Language;
|
||||
|
||||
use JsonSerializable;
|
||||
use ReturnTypeWillChange;
|
||||
|
||||
class SourceLocation implements JsonSerializable
|
||||
{
|
||||
@@ -46,6 +47,7 @@ class SourceLocation implements JsonSerializable
|
||||
/**
|
||||
* @return int[]
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toSerializableArray();
|
||||
|
||||
+21
-3
@@ -559,10 +559,12 @@ class Helper
|
||||
);
|
||||
}
|
||||
} else {
|
||||
parse_str((string) $request->getBody(), $bodyParams);
|
||||
if ($request instanceof ServerRequestInterface) {
|
||||
$bodyParams = $request->getParsedBody();
|
||||
}
|
||||
|
||||
if (! is_array($bodyParams)) {
|
||||
throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType[0]));
|
||||
if (! isset($bodyParams)) {
|
||||
$bodyParams = $this->decodeContent((string) $request->getBody(), $contentType[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -576,6 +578,22 @@ class Helper
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*
|
||||
* @throws RequestError
|
||||
*/
|
||||
protected function decodeContent(string $rawBody, string $contentType) : array
|
||||
{
|
||||
parse_str($rawBody, $bodyParams);
|
||||
|
||||
if (! is_array($bodyParams)) {
|
||||
throw new RequestError('Unexpected content type: ' . Utils::printSafeJson($contentType));
|
||||
}
|
||||
|
||||
return $bodyParams;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts query execution result to PSR-7 response
|
||||
*
|
||||
|
||||
@@ -12,6 +12,7 @@ use GraphQL\Type\Schema;
|
||||
use GraphQL\Utils\Utils;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function is_iterable;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
@@ -95,9 +96,9 @@ class FieldDefinition
|
||||
if (is_callable($fields)) {
|
||||
$fields = $fields();
|
||||
}
|
||||
if (! is_array($fields)) {
|
||||
if (! is_iterable($fields)) {
|
||||
throw new InvariantViolation(
|
||||
sprintf('%s fields must be an array or a callable which returns such an array.', $type->name)
|
||||
sprintf('%s fields must be an iterable or a callable which returns such an iterable.', $type->name)
|
||||
);
|
||||
}
|
||||
$map = [];
|
||||
@@ -123,6 +124,17 @@ class FieldDefinition
|
||||
$fieldDef = self::create($field);
|
||||
} elseif ($field instanceof self) {
|
||||
$fieldDef = $field;
|
||||
} elseif (is_callable($field)) {
|
||||
if (! is_string($name)) {
|
||||
throw new InvariantViolation(
|
||||
sprintf(
|
||||
'%s lazy fields must be an associative array with field names as keys.',
|
||||
$type->name
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$fieldDef = new UnresolvedFieldDefinition($type, $name, $field);
|
||||
} else {
|
||||
if (! is_string($name) || ! $field) {
|
||||
throw new InvariantViolation(
|
||||
@@ -137,7 +149,8 @@ class FieldDefinition
|
||||
|
||||
$fieldDef = self::create(['name' => $name, 'type' => $field]);
|
||||
}
|
||||
$map[$fieldDef->name] = $fieldDef;
|
||||
|
||||
$map[$fieldDef->getName()] = $fieldDef;
|
||||
}
|
||||
|
||||
return $map;
|
||||
@@ -180,6 +193,11 @@ class FieldDefinition
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getType() : Type
|
||||
{
|
||||
if (! isset($this->type)) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
|
||||
interface HasFieldsType
|
||||
{
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function getField(string $name) : FieldDefinition;
|
||||
|
||||
public function hasField(string $name) : bool;
|
||||
|
||||
public function findField(string $name) : ?FieldDefinition;
|
||||
|
||||
/**
|
||||
* @return array<string, FieldDefinition>
|
||||
*
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function getFields() : array;
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function getFieldNames() : array;
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use GraphQL\Utils\Utils;
|
||||
use function count;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
use function is_iterable;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
@@ -80,14 +81,14 @@ class InputObjectType extends Type implements InputType, NullableType, NamedType
|
||||
$fields = $fields();
|
||||
}
|
||||
|
||||
if (! is_array($fields)) {
|
||||
if (! is_iterable($fields)) {
|
||||
throw new InvariantViolation(
|
||||
sprintf('%s fields must be an array or a callable which returns such an array.', $this->name)
|
||||
sprintf('%s fields must be an iterable or a callable which returns such an iterable.', $this->name)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($fields as $name => $field) {
|
||||
if ($field instanceof Type) {
|
||||
if ($field instanceof Type || is_callable($field)) {
|
||||
$field = ['type' => $field];
|
||||
}
|
||||
$field = new InputObjectField($field + ['name' => $name]);
|
||||
|
||||
@@ -24,8 +24,8 @@ class IntType extends ScalarType
|
||||
//
|
||||
// n.b. JavaScript's integers are safe between -(2^53 - 1) and 2^53 - 1 because
|
||||
// they are internally represented as IEEE 754 doubles.
|
||||
private const MAX_INT = 2147483647;
|
||||
private const MIN_INT = -2147483648;
|
||||
public const MAX_INT = 2147483647;
|
||||
public const MIN_INT = -2147483648;
|
||||
|
||||
/** @var string */
|
||||
public $name = Type::INT;
|
||||
|
||||
@@ -15,7 +15,7 @@ use function is_callable;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
class InterfaceType extends Type implements AbstractType, OutputType, CompositeType, NullableType, NamedType, ImplementingType
|
||||
class InterfaceType extends TypeWithFields implements AbstractType, OutputType, CompositeType, NullableType, NamedType, ImplementingType
|
||||
{
|
||||
/** @var InterfaceTypeDefinitionNode|null */
|
||||
public $astNode;
|
||||
@@ -23,13 +23,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
/** @var array<int, InterfaceTypeExtensionNode> */
|
||||
public $extensionASTNodes;
|
||||
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
* @var array<string, FieldDefinition>
|
||||
*/
|
||||
private $fields;
|
||||
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
@@ -79,43 +72,6 @@ class InterfaceType extends Type implements AbstractType, OutputType, CompositeT
|
||||
return $type;
|
||||
}
|
||||
|
||||
public function getField(string $name) : FieldDefinition
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
|
||||
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
public function hasField(string $name) : bool
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
|
||||
return isset($this->fields[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition[]
|
||||
*/
|
||||
public function getFields() : array
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
protected function initializeFields() : void
|
||||
{
|
||||
$fields = $this->config['fields'] ?? [];
|
||||
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
|
||||
}
|
||||
|
||||
public function implementsInterface(InterfaceType $interfaceType) : bool
|
||||
{
|
||||
if (! isset($this->interfaceMap)) {
|
||||
|
||||
+1
-50
@@ -55,7 +55,7 @@ use function sprintf;
|
||||
* }
|
||||
* ]);
|
||||
*/
|
||||
class ObjectType extends Type implements OutputType, CompositeType, NullableType, NamedType, ImplementingType
|
||||
class ObjectType extends TypeWithFields implements OutputType, CompositeType, NullableType, NamedType, ImplementingType
|
||||
{
|
||||
/** @var ObjectTypeDefinitionNode|null */
|
||||
public $astNode;
|
||||
@@ -66,13 +66,6 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType
|
||||
/** @var ?callable */
|
||||
public $resolveFieldFn;
|
||||
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
* @var FieldDefinition[]
|
||||
*/
|
||||
private $fields;
|
||||
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
@@ -123,48 +116,6 @@ class ObjectType extends Type implements OutputType, CompositeType, NullableType
|
||||
return $type;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function getField(string $name) : FieldDefinition
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
Utils::invariant(isset($this->fields[$name]), 'Field "%s" is not defined for type "%s"', $name, $this->name);
|
||||
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
public function hasField(string $name) : bool
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
|
||||
return isset($this->fields[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FieldDefinition[]
|
||||
*
|
||||
* @throws InvariantViolation
|
||||
*/
|
||||
public function getFields() : array
|
||||
{
|
||||
if (! isset($this->fields)) {
|
||||
$this->initializeFields();
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
protected function initializeFields() : void
|
||||
{
|
||||
$fields = $this->config['fields'] ?? [];
|
||||
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
|
||||
}
|
||||
|
||||
public function implementsInterface(InterfaceType $interfaceType) : bool
|
||||
{
|
||||
if (! isset($this->interfaceMap)) {
|
||||
|
||||
@@ -11,6 +11,7 @@ use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Type\Schema;
|
||||
use function array_diff_key;
|
||||
use function array_filter;
|
||||
@@ -167,7 +168,12 @@ class QueryPlan
|
||||
$implementors = [];
|
||||
foreach ($selectionSet->selections as $selectionNode) {
|
||||
if ($selectionNode instanceof FieldNode) {
|
||||
$fieldName = $selectionNode->name->value;
|
||||
$fieldName = $selectionNode->name->value;
|
||||
|
||||
if ($fieldName === Introspection::TYPE_NAME_FIELD_NAME) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = $parentType->getField($fieldName);
|
||||
$selectionType = $type->getType();
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils\Utils;
|
||||
use JsonSerializable;
|
||||
use ReflectionClass;
|
||||
use ReturnTypeWillChange;
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function assert;
|
||||
@@ -308,6 +309,7 @@ abstract class Type implements JsonSerializable
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function jsonSerialize()
|
||||
{
|
||||
return $this->toString();
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Utils\Utils;
|
||||
use function array_keys;
|
||||
|
||||
abstract class TypeWithFields extends Type implements HasFieldsType
|
||||
{
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
* @var array<string, FieldDefinition>
|
||||
*/
|
||||
private $fields;
|
||||
|
||||
private function initializeFields() : void
|
||||
{
|
||||
if (isset($this->fields)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$fields = $this->config['fields'] ?? [];
|
||||
$this->fields = FieldDefinition::defineFieldMap($this, $fields);
|
||||
}
|
||||
|
||||
public function getField(string $name) : FieldDefinition
|
||||
{
|
||||
Utils::invariant($this->hasField($name), 'Field "%s" is not defined for type "%s"', $name, $this->name);
|
||||
|
||||
return $this->findField($name);
|
||||
}
|
||||
|
||||
public function findField(string $name) : ?FieldDefinition
|
||||
{
|
||||
$this->initializeFields();
|
||||
|
||||
if (! isset($this->fields[$name])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->fields[$name] instanceof UnresolvedFieldDefinition) {
|
||||
$this->fields[$name] = $this->fields[$name]->resolve();
|
||||
}
|
||||
|
||||
return $this->fields[$name];
|
||||
}
|
||||
|
||||
public function hasField(string $name) : bool
|
||||
{
|
||||
$this->initializeFields();
|
||||
|
||||
return isset($this->fields[$name]);
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getFields() : array
|
||||
{
|
||||
$this->initializeFields();
|
||||
|
||||
foreach ($this->fields as $name => $field) {
|
||||
if (! ($field instanceof UnresolvedFieldDefinition)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->fields[$name] = $field->resolve();
|
||||
}
|
||||
|
||||
return $this->fields;
|
||||
}
|
||||
|
||||
/** @inheritDoc */
|
||||
public function getFieldNames() : array
|
||||
{
|
||||
$this->initializeFields();
|
||||
|
||||
return array_keys($this->fields);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace GraphQL\Type\Definition;
|
||||
|
||||
use GraphQL\Error\InvariantViolation;
|
||||
use function is_array;
|
||||
use function sprintf;
|
||||
|
||||
class UnresolvedFieldDefinition
|
||||
{
|
||||
/** @var Type $type */
|
||||
private $type;
|
||||
|
||||
/** @var string $name */
|
||||
private $name;
|
||||
|
||||
/** @var callable(): (FieldDefinition|array<string, mixed>|Type) $resolver */
|
||||
private $resolver;
|
||||
|
||||
/**
|
||||
* @param callable(): (FieldDefinition|array<string, mixed>|Type) $resolver
|
||||
*/
|
||||
public function __construct(Type $type, string $name, callable $resolver)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->name = $name;
|
||||
$this->resolver = $resolver;
|
||||
}
|
||||
|
||||
public function getName() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function resolve() : FieldDefinition
|
||||
{
|
||||
$field = ($this->resolver)();
|
||||
|
||||
if ($field instanceof FieldDefinition) {
|
||||
if ($field->name !== $this->name) {
|
||||
throw new InvariantViolation(
|
||||
sprintf('%s.%s should not dynamically change its name when resolved lazily.', $this->type->name, $this->name)
|
||||
);
|
||||
}
|
||||
|
||||
return $field;
|
||||
}
|
||||
|
||||
if (! is_array($field)) {
|
||||
return FieldDefinition::create(['name' => $this->name, 'type' => $field]);
|
||||
}
|
||||
|
||||
if (! isset($field['name'])) {
|
||||
$field['name'] = $this->name;
|
||||
} elseif ($field['name'] !== $this->name) {
|
||||
throw new InvariantViolation(
|
||||
sprintf('%s.%s should not dynamically change its name when resolved lazily.', $this->type->name, $this->name)
|
||||
);
|
||||
}
|
||||
|
||||
if (isset($field['args']) && ! is_array($field['args'])) {
|
||||
throw new InvariantViolation(
|
||||
sprintf('%s.%s args must be an array.', $this->type->name, $this->name)
|
||||
);
|
||||
}
|
||||
|
||||
return FieldDefinition::create($field);
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -293,7 +293,7 @@ EOD;
|
||||
'Object and Interface types provide the fields they describe. Abstract ' .
|
||||
'types, Union and Interface, provide the Object types possible ' .
|
||||
'at runtime. List and NonNull types compose other types.',
|
||||
'fields' => static function () {
|
||||
'fields' => static function () : array {
|
||||
return [
|
||||
'kind' => [
|
||||
'type' => Type::nonNull(self::_typeKind()),
|
||||
@@ -337,7 +337,7 @@ EOD;
|
||||
'args' => [
|
||||
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
|
||||
],
|
||||
'resolve' => static function (Type $type, $args) {
|
||||
'resolve' => static function (Type $type, $args) : ?array {
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$fields = $type->getFields();
|
||||
|
||||
@@ -381,7 +381,7 @@ EOD;
|
||||
'args' => [
|
||||
'includeDeprecated' => ['type' => Type::boolean(), 'defaultValue' => false],
|
||||
],
|
||||
'resolve' => static function ($type, $args) {
|
||||
'resolve' => static function ($type, $args) : ?array {
|
||||
if ($type instanceof EnumType) {
|
||||
$values = array_values($type->getValues());
|
||||
|
||||
@@ -484,7 +484,7 @@ EOD;
|
||||
'description' =>
|
||||
'Object and Interface types are described by a list of Fields, each of ' .
|
||||
'which has a name, potentially a list of arguments, and a return type.',
|
||||
'fields' => static function () {
|
||||
'fields' => static function () : array {
|
||||
return [
|
||||
'name' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
@@ -540,7 +540,7 @@ EOD;
|
||||
'Arguments provided to Fields or Directives and the input fields of an ' .
|
||||
'InputObject are represented as Input Values which describe their type ' .
|
||||
'and optionally a default value.',
|
||||
'fields' => static function () {
|
||||
'fields' => static function () : array {
|
||||
return [
|
||||
'name' => [
|
||||
'type' => Type::nonNull(Type::string()),
|
||||
@@ -661,7 +661,7 @@ EOD;
|
||||
],
|
||||
'args' => [
|
||||
'type' => Type::nonNull(Type::listOf(Type::nonNull(self::_inputValue()))),
|
||||
'resolve' => static function (Directive $directive) {
|
||||
'resolve' => static function (Directive $directive) : array {
|
||||
return $directive->args ?? [];
|
||||
},
|
||||
],
|
||||
|
||||
+9
-25
@@ -20,9 +20,10 @@ use GraphQL\Type\Definition\UnionType;
|
||||
use GraphQL\Utils\InterfaceImplementations;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Utils\Utils;
|
||||
use InvalidArgumentException;
|
||||
use Traversable;
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
use function get_class;
|
||||
use function implode;
|
||||
use function is_array;
|
||||
use function is_callable;
|
||||
@@ -60,13 +61,6 @@ class Schema
|
||||
*/
|
||||
private $resolvedTypes = [];
|
||||
|
||||
/**
|
||||
* Lazily initialized.
|
||||
*
|
||||
* @var array<string, array<string, ObjectType|UnionType>>
|
||||
*/
|
||||
private $subTypeMap;
|
||||
|
||||
/**
|
||||
* Lazily initialised.
|
||||
*
|
||||
@@ -507,25 +501,15 @@ class Schema
|
||||
*/
|
||||
public function isSubType(AbstractType $abstractType, ImplementingType $maybeSubType) : bool
|
||||
{
|
||||
if (! isset($this->subTypeMap[$abstractType->name])) {
|
||||
$this->subTypeMap[$abstractType->name] = [];
|
||||
|
||||
if ($abstractType instanceof UnionType) {
|
||||
foreach ($abstractType->getTypes() as $type) {
|
||||
$this->subTypeMap[$abstractType->name][$type->name] = true;
|
||||
}
|
||||
} else {
|
||||
$implementations = $this->getImplementations($abstractType);
|
||||
foreach ($implementations->objects() as $type) {
|
||||
$this->subTypeMap[$abstractType->name][$type->name] = true;
|
||||
}
|
||||
foreach ($implementations->interfaces() as $type) {
|
||||
$this->subTypeMap[$abstractType->name][$type->name] = true;
|
||||
}
|
||||
}
|
||||
if ($abstractType instanceof InterfaceType) {
|
||||
return $maybeSubType->implementsInterface($abstractType);
|
||||
}
|
||||
|
||||
return isset($this->subTypeMap[$abstractType->name][$maybeSubType->name]);
|
||||
if ($abstractType instanceof UnionType) {
|
||||
return $abstractType->isPossibleType($maybeSubType);
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException(sprintf('$abstractType must be of type UnionType|InterfaceType got: %s.', get_class($abstractType)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -372,8 +372,9 @@ class SchemaValidationContext
|
||||
*/
|
||||
private function validateDirectivesAtLocation($directives, string $location)
|
||||
{
|
||||
$directivesNamed = [];
|
||||
$schema = $this->schema;
|
||||
/** @var array<string, array<int, DirectiveNode>> $potentiallyDuplicateDirectives */
|
||||
$potentiallyDuplicateDirectives = [];
|
||||
$schema = $this->schema;
|
||||
foreach ($directives as $directive) {
|
||||
$directiveName = $directive->name->value;
|
||||
|
||||
@@ -386,6 +387,7 @@ class SchemaValidationContext
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
$includes = Utils::some(
|
||||
$schemaDirective->locations,
|
||||
static function ($schemaLocation) use ($location) : bool {
|
||||
@@ -402,17 +404,22 @@ class SchemaValidationContext
|
||||
);
|
||||
}
|
||||
|
||||
$existingNodes = $directivesNamed[$directiveName] ?? [];
|
||||
$existingNodes[] = $directive;
|
||||
$directivesNamed[$directiveName] = $existingNodes;
|
||||
if ($schemaDirective->isRepeatable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$existingNodes = $potentiallyDuplicateDirectives[$directiveName] ?? [];
|
||||
$existingNodes[] = $directive;
|
||||
$potentiallyDuplicateDirectives[$directiveName] = $existingNodes;
|
||||
}
|
||||
foreach ($directivesNamed as $directiveName => $directiveList) {
|
||||
|
||||
foreach ($potentiallyDuplicateDirectives as $directiveName => $directiveList) {
|
||||
if (count($directiveList) <= 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->reportError(
|
||||
sprintf('Directive @%s used twice at the same location.', $directiveName),
|
||||
sprintf('Non-repeatable directive @%s used more than once at the same location.', $directiveName),
|
||||
$directiveList
|
||||
);
|
||||
}
|
||||
@@ -426,7 +433,7 @@ class SchemaValidationContext
|
||||
$fieldMap = $type->getFields();
|
||||
|
||||
// Objects and Interfaces both must define one or more fields.
|
||||
if (! $fieldMap) {
|
||||
if ($fieldMap === []) {
|
||||
$this->reportError(
|
||||
sprintf('Type %s must define one or more fields.', $type->name),
|
||||
$this->getAllNodes($type)
|
||||
|
||||
+31
@@ -584,6 +584,8 @@ class AST
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use getOperationAST instead.
|
||||
*
|
||||
* Returns operation type ("query", "mutation" or "subscription") given a document and operation name
|
||||
*
|
||||
* @param string $operationName
|
||||
@@ -608,4 +610,33 @@ class AST
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the operation within a document by name.
|
||||
*
|
||||
* If a name is not provided, an operation is only returned if the document has exactly one.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
public static function getOperationAST(DocumentNode $document, ?string $operationName = null) : ?OperationDefinitionNode
|
||||
{
|
||||
$operation = null;
|
||||
foreach ($document->definitions->getIterator() as $node) {
|
||||
if (! $node instanceof OperationDefinitionNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($operationName === null) {
|
||||
// We found a second operation, so we bail instead of returning an ambiguous result.
|
||||
if ($operation !== null) {
|
||||
return null;
|
||||
}
|
||||
$operation = $node;
|
||||
} elseif ($node->name instanceof NameNode && $node->name->value === $operationName) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
return $operation;
|
||||
}
|
||||
}
|
||||
@@ -489,7 +489,7 @@ class BuildClientSchema
|
||||
'name' => $directive['name'],
|
||||
'description' => $directive['description'],
|
||||
'args' => $this->buildInputValueDefMap($directive['args']),
|
||||
'isRepeatable' => $directive['isRepeatable'],
|
||||
'isRepeatable' => $directive['isRepeatable'] ?? false,
|
||||
'locations' => $directive['locations'],
|
||||
]);
|
||||
}
|
||||
|
||||
+5
-7
@@ -7,6 +7,7 @@ namespace GraphQL\Utils;
|
||||
use ArrayAccess;
|
||||
use GraphQL\Type\Definition\EnumValueDefinition;
|
||||
use InvalidArgumentException;
|
||||
use ReturnTypeWillChange;
|
||||
use SplObjectStorage;
|
||||
use function array_key_exists;
|
||||
use function array_search;
|
||||
@@ -90,7 +91,7 @@ class MixedStore implements ArrayAccess
|
||||
* <p>
|
||||
* The return value will be casted to boolean if non-boolean was returned.
|
||||
*/
|
||||
public function offsetExists($offset)
|
||||
public function offsetExists($offset) : bool
|
||||
{
|
||||
if ($offset === false) {
|
||||
return $this->falseValueIsSet;
|
||||
@@ -135,6 +136,7 @@ class MixedStore implements ArrayAccess
|
||||
*
|
||||
* @return mixed Can return all value types.
|
||||
*/
|
||||
#[ReturnTypeWillChange]
|
||||
public function offsetGet($offset)
|
||||
{
|
||||
if ($offset === true) {
|
||||
@@ -181,10 +183,8 @@ class MixedStore implements ArrayAccess
|
||||
* @param mixed $value <p>
|
||||
* The value to set.
|
||||
* </p>
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function offsetSet($offset, $value)
|
||||
public function offsetSet($offset, $value) : void
|
||||
{
|
||||
if ($offset === false) {
|
||||
$this->falseValue = $value;
|
||||
@@ -217,10 +217,8 @@ class MixedStore implements ArrayAccess
|
||||
* @param mixed $offset <p>
|
||||
* The offset to unset.
|
||||
* </p>
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function offsetUnset($offset)
|
||||
public function offsetUnset($offset) : void
|
||||
{
|
||||
if ($offset === true) {
|
||||
$this->trueValue = null;
|
||||
|
||||
+9
-3
@@ -494,14 +494,19 @@ class SchemaExtender
|
||||
'locations' => $directive->locations,
|
||||
'args' => static::extendArgs($directive->args),
|
||||
'astNode' => $directive->astNode,
|
||||
'isRepeatable' => $directive->isRepeatable,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, bool> $options
|
||||
*/
|
||||
public static function extend(Schema $schema, DocumentNode $documentAST, array $options = []) : Schema
|
||||
{
|
||||
public static function extend(
|
||||
Schema $schema,
|
||||
DocumentNode $documentAST,
|
||||
array $options = [],
|
||||
?callable $typeConfigDecorator = null
|
||||
) : Schema {
|
||||
if (! (isset($options['assumeValid']) || isset($options['assumeValidSDL']))) {
|
||||
DocumentValidator::assertValidSDLExtension($documentAST, $schema);
|
||||
}
|
||||
@@ -579,7 +584,8 @@ class SchemaExtender
|
||||
}
|
||||
|
||||
throw new Error('Unknown type: "' . $typeName . '". Ensure that this type exists either in the original schema, or is added in a type definition.', [$typeName]);
|
||||
}
|
||||
},
|
||||
$typeConfigDecorator
|
||||
);
|
||||
|
||||
static::$extendTypeCache = [];
|
||||
|
||||
+10
-8
@@ -25,6 +25,7 @@ use GraphQL\Type\Definition\Directive;
|
||||
use GraphQL\Type\Definition\EnumType;
|
||||
use GraphQL\Type\Definition\FieldArgument;
|
||||
use GraphQL\Type\Definition\FieldDefinition;
|
||||
use GraphQL\Type\Definition\HasFieldsType;
|
||||
use GraphQL\Type\Definition\ImplementingType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\InputType;
|
||||
@@ -196,9 +197,10 @@ class TypeInfo
|
||||
if ($type instanceof ImplementingType) {
|
||||
$nestedTypes = array_merge($nestedTypes, $type->getInterfaces());
|
||||
}
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
foreach ($type->getFields() as $fieldName => $field) {
|
||||
if (count($field->args ?? []) > 0) {
|
||||
|
||||
if ($type instanceof HasFieldsType) {
|
||||
foreach ($type->getFields() as $field) {
|
||||
if (count($field->args) > 0) {
|
||||
$fieldArgTypes = array_map(
|
||||
static function (FieldArgument $arg) : Type {
|
||||
return $arg->getType();
|
||||
@@ -212,7 +214,7 @@ class TypeInfo
|
||||
}
|
||||
}
|
||||
if ($type instanceof InputObjectType) {
|
||||
foreach ($type->getFields() as $fieldName => $field) {
|
||||
foreach ($type->getFields() as $field) {
|
||||
$nestedTypes[] = $field->getType();
|
||||
}
|
||||
}
|
||||
@@ -414,11 +416,11 @@ class TypeInfo
|
||||
if ($name === $typeNameMeta->name && $parentType instanceof CompositeType) {
|
||||
return $typeNameMeta;
|
||||
}
|
||||
if ($parentType instanceof ObjectType ||
|
||||
$parentType instanceof InterfaceType) {
|
||||
$fields = $parentType->getFields();
|
||||
|
||||
return $fields[$name] ?? null;
|
||||
if ($parentType instanceof ObjectType ||
|
||||
$parentType instanceof InterfaceType
|
||||
) {
|
||||
return $parentType->findField($name);
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
@@ -85,15 +85,13 @@ class FieldsOnCorrectType extends ValidationRule
|
||||
$interfaceUsageCount = [];
|
||||
|
||||
foreach ($schema->getPossibleTypes($type) as $possibleType) {
|
||||
$fields = $possibleType->getFields();
|
||||
if (! isset($fields[$fieldName])) {
|
||||
if (! $possibleType->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
// This object type defines this field.
|
||||
$suggestedObjectTypes[] = $possibleType->name;
|
||||
foreach ($possibleType->getInterfaces() as $possibleInterface) {
|
||||
$fields = $possibleInterface->getFields();
|
||||
if (! isset($fields[$fieldName])) {
|
||||
if (! $possibleInterface->hasField($fieldName)) {
|
||||
continue;
|
||||
}
|
||||
// This interface type defines this field.
|
||||
@@ -128,7 +126,7 @@ class FieldsOnCorrectType extends ValidationRule
|
||||
private function getSuggestedFieldNames(Schema $schema, $type, $fieldName)
|
||||
{
|
||||
if ($type instanceof ObjectType || $type instanceof InterfaceType) {
|
||||
$possibleFieldNames = array_keys($type->getFields());
|
||||
$possibleFieldNames = $type->getFieldNames();
|
||||
|
||||
return Utils::suggestionList($fieldName, $possibleFieldNames);
|
||||
}
|
||||
|
||||
+4
-4
@@ -250,10 +250,10 @@ class OverlappingFieldsCanBeMerged extends ValidationRule
|
||||
$fieldName = $selection->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType instanceof ObjectType ||
|
||||
$parentType instanceof InterfaceType) {
|
||||
$tmp = $parentType->getFields();
|
||||
if (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
$parentType instanceof InterfaceType
|
||||
) {
|
||||
if ($parentType->hasField($fieldName)) {
|
||||
$fieldDef = $parentType->getField($fieldName);
|
||||
}
|
||||
}
|
||||
$responseName = $selection->alias ? $selection->alias->value : $fieldName;
|
||||
|
||||
@@ -10,13 +10,14 @@ use GraphQL\Language\AST\FragmentDefinitionNode;
|
||||
use GraphQL\Language\AST\FragmentSpreadNode;
|
||||
use GraphQL\Language\AST\InlineFragmentNode;
|
||||
use GraphQL\Language\AST\SelectionSetNode;
|
||||
use GraphQL\Type\Definition\HasFieldsType;
|
||||
use GraphQL\Type\Definition\InputObjectType;
|
||||
use GraphQL\Type\Definition\Type;
|
||||
use GraphQL\Type\Introspection;
|
||||
use GraphQL\Utils\TypeInfo;
|
||||
use GraphQL\Validator\ValidationContext;
|
||||
use InvalidArgumentException;
|
||||
use function class_alias;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
|
||||
abstract class QuerySecurityRule extends ValidationRule
|
||||
@@ -117,8 +118,7 @@ abstract class QuerySecurityRule extends ValidationRule
|
||||
case $selection instanceof FieldNode:
|
||||
$fieldName = $selection->name->value;
|
||||
$fieldDef = null;
|
||||
if ($parentType && method_exists($parentType, 'getFields')) {
|
||||
$tmp = $parentType->getFields();
|
||||
if ($parentType instanceof HasFieldsType || $parentType instanceof InputObjectType) {
|
||||
$schemaMetaFieldDef = Introspection::schemaMetaFieldDef();
|
||||
$typeMetaFieldDef = Introspection::typeMetaFieldDef();
|
||||
$typeNameMetaFieldDef = Introspection::typeNameMetaFieldDef();
|
||||
@@ -129,8 +129,8 @@ abstract class QuerySecurityRule extends ValidationRule
|
||||
$fieldDef = $typeMetaFieldDef;
|
||||
} elseif ($fieldName === $typeNameMetaFieldDef->name) {
|
||||
$fieldDef = $typeNameMetaFieldDef;
|
||||
} elseif (isset($tmp[$fieldName])) {
|
||||
$fieldDef = $tmp[$fieldName];
|
||||
} elseif ($parentType->hasField($fieldName)) {
|
||||
$fieldDef = $parentType->getField($fieldName);
|
||||
}
|
||||
}
|
||||
$responseName = $this->getFieldName($selection);
|
||||
|
||||
+10
-3
@@ -34,6 +34,7 @@ class UniqueDirectivesPerLocation extends ValidationRule
|
||||
|
||||
public function getASTVisitor(ASTValidationContext $context)
|
||||
{
|
||||
/** @var array<string, true> $uniqueDirectiveMap */
|
||||
$uniqueDirectiveMap = [];
|
||||
|
||||
$schema = $context->getSchema();
|
||||
@@ -41,16 +42,22 @@ class UniqueDirectivesPerLocation extends ValidationRule
|
||||
? $schema->getDirectives()
|
||||
: Directive::getInternalDirectives();
|
||||
foreach ($definedDirectives as $directive) {
|
||||
$uniqueDirectiveMap[$directive->name] = ! $directive->isRepeatable;
|
||||
if ($directive->isRepeatable) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uniqueDirectiveMap[$directive->name] = true;
|
||||
}
|
||||
|
||||
$astDefinitions = $context->getDocument()->definitions;
|
||||
foreach ($astDefinitions as $definition) {
|
||||
if (! ($definition instanceof DirectiveDefinitionNode)) {
|
||||
if (! ($definition instanceof DirectiveDefinitionNode)
|
||||
|| $definition->repeatable
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$uniqueDirectiveMap[$definition->name->value] = $definition->repeatable;
|
||||
$uniqueDirectiveMap[$definition->name->value] = true;
|
||||
}
|
||||
|
||||
return [
|
||||
|
||||
@@ -81,7 +81,7 @@ class ValuesOfCorrectType extends ValidationRule
|
||||
|
||||
return null;
|
||||
},
|
||||
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) {
|
||||
NodeKind::OBJECT => function (ObjectValueNode $node) use ($context, &$fieldName) : ?VisitorOperation {
|
||||
// Note: TypeInfo will traverse into a list's item type, so look to the
|
||||
// parent input type to check if it is a list.
|
||||
$type = Type::getNamedType($context->getInputType());
|
||||
@@ -116,6 +116,8 @@ class ValuesOfCorrectType extends ValidationRule
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
NodeKind::OBJECT_FIELD => static function (ObjectFieldNode $node) use ($context) : void {
|
||||
$parentType = Type::getNamedType($context->getParentInputType());
|
||||
|
||||
@@ -31,6 +31,7 @@ use GraphQL\Utils\TypeInfo;
|
||||
use SplObjectStorage;
|
||||
use function array_merge;
|
||||
use function array_pop;
|
||||
use function assert;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
@@ -180,16 +181,15 @@ class ValidationContext extends ASTValidationContext
|
||||
while (count($setsToVisit) > 0) {
|
||||
$set = array_pop($setsToVisit);
|
||||
|
||||
for ($i = 0, $selectionCount = count($set->selections); $i < $selectionCount; $i++) {
|
||||
$selection = $set->selections[$i];
|
||||
foreach ($set->selections as $selection) {
|
||||
if ($selection instanceof FragmentSpreadNode) {
|
||||
$spreads[] = $selection;
|
||||
} elseif ($selection instanceof FieldNode || $selection instanceof InlineFragmentNode) {
|
||||
if ($selection->selectionSet) {
|
||||
$setsToVisit[] = $selection->selectionSet;
|
||||
}
|
||||
} else {
|
||||
throw InvariantViolation::shouldNotHappen();
|
||||
assert($selection instanceof FieldNode || $selection instanceof InlineFragmentNode);
|
||||
$selectionSet = $selection->selectionSet;
|
||||
if ($selectionSet !== null) {
|
||||
$setsToVisit[] = $selectionSet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Base API Graph Controller
|
||||
* @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 1 Sept 2022
|
||||
*/
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Base API Controller
|
||||
*/
|
||||
class _GraphQL
|
||||
{
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
/* This space intentionally left blank */
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute
|
||||
*
|
||||
* @param string $query The query to execute
|
||||
* @param array $variables Variables to include in query
|
||||
* @return array GraphQL response
|
||||
*/
|
||||
public static function execute( string $query, array $variables = [], array $member = NULL )
|
||||
{
|
||||
/* Register our GraphQL library */
|
||||
\IPS\IPS::$PSR0Namespaces['GraphQL'] = \IPS\ROOT_PATH . "/system/3rd_party/graphql-php";
|
||||
|
||||
/* Execute! */
|
||||
$result = \GraphQL\GraphQL::executeQuery(
|
||||
new \GraphQL\Type\Schema([
|
||||
'query' => \IPS\Api\GraphQL\TypeRegistry::query(),
|
||||
'mutation' => \IPS\Api\GraphQL\TypeRegistry::mutation()
|
||||
]),
|
||||
$query,
|
||||
NULL, // $rootValue
|
||||
[
|
||||
'member' => \IPS\Member::loggedIn()
|
||||
], // $context
|
||||
$variables
|
||||
);
|
||||
|
||||
/* Convert result into JSON and send */
|
||||
$output = $result->toArray( ( \IPS\IN_DEV OR \IPS\DEBUG_GRAPHQL ) ? \GraphQL\Error\DebugFlag::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\DebugFlag::INCLUDE_TRACE : false );
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class _PaginatedResponse extends Response
|
||||
* @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
|
||||
* @param int|NULL $count Total Count
|
||||
* @param \IPS\Member|NULL $authorizedMember The member making the API request or NULL for API Key / client_credentials
|
||||
* @param int $perPage Number of results per page
|
||||
* @return void
|
||||
@@ -78,6 +78,17 @@ class _PaginatedResponse extends Response
|
||||
$this->count = (int) $count;
|
||||
$this->resultsPerPage = $perPage ? (int) $perPage : 25;
|
||||
$this->authorizedMember = $authorizedMember;
|
||||
|
||||
if( $this->iterator instanceof \IPS\Db\Select )
|
||||
{
|
||||
/* Limit the query before calling count(), as this runs the query so it cannot be modified after */
|
||||
$this->iterator->query .= \IPS\Db::i()->compileLimitClause( array( ( $this->page - 1 ) * $this->resultsPerPage, $this->resultsPerPage ) );
|
||||
|
||||
if ( $count === NULL )
|
||||
{
|
||||
$this->count = $iterator->count();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,11 +100,6 @@ class _PaginatedResponse extends Response
|
||||
{
|
||||
$results = array();
|
||||
|
||||
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 )
|
||||
{
|
||||
if( $this->iterator instanceof \IPS\Patterns\ActiveRecordIterator )
|
||||
|
||||
+29
-2
@@ -77,6 +77,7 @@ class _Webhook extends \IPS\Node\Model
|
||||
*/
|
||||
public function setDefaultValues()
|
||||
{
|
||||
$this->content_type = 'application/x-www-form-urlencoded';
|
||||
$this->_data['filters'] = '{}';
|
||||
$this->_data['url'] = '';
|
||||
$this->_data['events'] = '';
|
||||
@@ -91,6 +92,19 @@ class _Webhook extends \IPS\Node\Model
|
||||
public function form( &$form )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'url', $this->url ?? "", TRUE ) );
|
||||
|
||||
if( \in_array($this->content_type, ['application/x-www-form-urlencoded', 'application/json']))
|
||||
{
|
||||
$selectedHeader = $this->content_type;
|
||||
$customHeader = '';
|
||||
}
|
||||
else
|
||||
{
|
||||
$selectedHeader = 'custom';
|
||||
$customHeader = $this->content_type;
|
||||
}
|
||||
$form->add( new \IPS\Helpers\Form\Select('webhook_content_type', $selectedHeader, TRUE , ['options' => ['application/x-www-form-urlencoded' => 'x-www-form-urlencoded', 'application/json' => 'application/json', 'custom' => 'Other'] , 'toggles'=> ['custom' => ['webhook_content_type_other'] ] ] ) );
|
||||
$form->add( new \IPS\Helpers\Form\Text('webhook_content_type_other', $customHeader, FALSE , [], NULL, NULL, NULL, 'webhook_content_type_other' ) );
|
||||
$events = [];
|
||||
|
||||
foreach ( \IPS\Api\Webhook::getAvailableWebhooks() as $app => $hooks )
|
||||
@@ -116,8 +130,17 @@ class _Webhook extends \IPS\Node\Model
|
||||
public function formatFormValues( $values )
|
||||
{
|
||||
$values['events'] = $values['webhook_events'];
|
||||
unset( $values['webhook_events'] );
|
||||
|
||||
if( $values['webhook_content_type'] AND $values['webhook_content_type'] === 'custom' )
|
||||
{
|
||||
$values['content_type'] = $values['webhook_content_type_other'];
|
||||
}
|
||||
else
|
||||
{
|
||||
$values['content_type'] = $values['webhook_content_type'];
|
||||
}
|
||||
|
||||
unset( $values['webhook_events'], $values['webhook_content_type'], $values['webhook_content_type_other'] );
|
||||
|
||||
return $values;
|
||||
}
|
||||
@@ -147,6 +170,9 @@ class _Webhook extends \IPS\Node\Model
|
||||
*/
|
||||
public static function fire( $event, $data = NULL, $filters = array() )
|
||||
{
|
||||
/* This will intentionally not call $data::apiOutput to avoid unnecessary overhead here */
|
||||
\IPS\Log::debug( print_r( array_merge(['event' => $event], ['payload' => $data], ['filters' => $filters ]) , TRUE ), 'webhook_fire_call');
|
||||
|
||||
/* Get our webhooks from cache */
|
||||
if ( !isset( \IPS\Data\Store::i()->webhooks ) )
|
||||
{
|
||||
@@ -163,7 +189,8 @@ class _Webhook extends \IPS\Node\Model
|
||||
$webhooks[ $e ][ $row['id'] ] = array(
|
||||
'key' => $row['api_key'],
|
||||
'url' => $row['url'],
|
||||
'filters' => json_decode( $row['filters'], TRUE )
|
||||
'filters' => json_decode( $row['filters'], TRUE ),
|
||||
'content_type_header' => $row['content_type']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1089,7 +1089,7 @@ EOF;
|
||||
$buttons['export'] = array(
|
||||
'icon' => 'download',
|
||||
'title' => 'download',
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=downloadMafia")
|
||||
'link' => \IPS\Http\Url::internal("app=core&module=applications&controller=applications&appKey={$this->_id}&do=customDownload")
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1164,8 +1164,24 @@ EOF;
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
if ( $this->requires_manual_intervention )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Lang string for the tooltip when this is locked
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get__lockedLang()
|
||||
{
|
||||
return $this->requires_manual_intervention ? 'invalid_php8_customization' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get Node Description
|
||||
@@ -3148,6 +3164,9 @@ EOF;
|
||||
*/
|
||||
public function build()
|
||||
{
|
||||
/* Use full upgrader? */
|
||||
$forceFullUpgrade = FALSE;
|
||||
|
||||
/* Write the application data to the application.json file */
|
||||
$applicationData = array(
|
||||
'application_title' => \IPS\Member::loggedIn()->language()->get('__app_' . $this->directory ),
|
||||
@@ -3268,15 +3287,22 @@ EOF;
|
||||
{
|
||||
\unlink( $jsChangesFile );
|
||||
}
|
||||
|
||||
/* Force full upgrade if global JS has changed */
|
||||
foreach( new \RecursiveIteratorIterator( new \RecursiveArrayIterator( $jsChanges ) ) as $k => $v )
|
||||
{
|
||||
if( mb_substr( $v, 0, 6 ) === 'global' )
|
||||
{
|
||||
$forceFullUpgrade = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->installJavascript();
|
||||
|
||||
/* Take care of hooks for this app */
|
||||
$this->buildHooks();
|
||||
|
||||
/* Take care of features for this app */
|
||||
$this->buildNewFeatures();
|
||||
|
||||
/* And custom build routines */
|
||||
foreach( $this->extensions( 'core', 'Build' ) as $builder )
|
||||
{
|
||||
@@ -3343,11 +3369,10 @@ EOF;
|
||||
'tasks' => file_exists( $setupDir . "/tasks.json" ),
|
||||
'modules' => file_exists( $setupDir . "/modules.json" ),
|
||||
'widgets' => file_exists( $setupDir . "/widgets.json" ),
|
||||
'whatsnew' => file_exists( $setupDir . "/whatsnew.json" ),
|
||||
'customOptions' => file_exists( $setupDir . "/options.php" ),
|
||||
'customRoutines' => file_exists( $setupDir . "/upgrade.php" ),
|
||||
),
|
||||
'forceMainUpgrader' => FALSE,
|
||||
'forceMainUpgrader' => $forceFullUpgrade,
|
||||
'forceManualDownloadNoCiC' => FALSE,
|
||||
'forceManualDownloadCiC' => FALSE,
|
||||
), JSON_PRETTY_PRINT ) );
|
||||
@@ -3440,47 +3465,6 @@ EOF;
|
||||
|
||||
return $previousChanges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build New Feature Highlights
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function buildNewFeatures()
|
||||
{
|
||||
/* Assign any that are flagged is "current" to this version */
|
||||
\IPS\Db::i()->update( 'core_members_features', array( 'feature_version_added' => $this->long_version ), array( "feature_version_added=? AND feature_app=?", -1, $this->directory ) );
|
||||
|
||||
/* Write the JSON file */
|
||||
\IPS\Application\Feature::writeJsonFile( $this->directory, $this->long_version );
|
||||
}
|
||||
|
||||
/**
|
||||
* Install new feature highlights
|
||||
*
|
||||
* @param string $appKey The application key
|
||||
* @param int $versionId The version ID
|
||||
* @return void
|
||||
*/
|
||||
public static function installNewFeatures( string $appKey, int $versionId )
|
||||
{
|
||||
if ( !\in_array( $appKey, \IPS\IPS::$ipsApps ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if ( file_exists( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_{$versionId}/whatsnew.json" ) )
|
||||
{
|
||||
if ( $data = @json_decode( \file_get_contents( \IPS\Application::load( $appKey )->getApplicationPath() . "/setup/upg_{$versionId}/whatsnew.json" ), TRUE ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_members_features', $data );
|
||||
}
|
||||
catch ( \IPS\Db\Exception $e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build skin templates for an app
|
||||
|
||||
@@ -1,405 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Application Feature Highlight Class
|
||||
* @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 15 Nov 2013
|
||||
*/
|
||||
|
||||
namespace IPS\Application;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
class _Feature extends \IPS\Patterns\ActiveRecord implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @brief [ActiveRecord] Database Prefix
|
||||
*/
|
||||
public static $databasePrefix = 'feature_';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database table
|
||||
* @note This MUST be over-ridden
|
||||
*/
|
||||
public static $databaseTable = 'core_members_features';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
* @note This needs to be declared in any child classes as well, only declaring here for editor code-complete/error-check functionality
|
||||
*/
|
||||
protected static $multitons = array();
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Database ID Fields
|
||||
* @note If using this, declare a static $multitonMap = array(); in the child class to prevent duplicate loading queries
|
||||
*/
|
||||
protected static $databaseIdFields = array( 'feature_unique_key' );
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Map
|
||||
*/
|
||||
protected static $multitonMap = array();
|
||||
|
||||
/**
|
||||
* Developer Management Table
|
||||
*
|
||||
* @param \IPS\Http\Url $url URL of the page the table is shown on.
|
||||
* @param string $appKay Application Key
|
||||
* @return string
|
||||
*/
|
||||
public static function devTable( \IPS\Http\Url $url, string $appKey ): string
|
||||
{
|
||||
$application = \IPS\Application::load( $appKey );
|
||||
|
||||
if ( isset( \IPS\Request::i()->featureAction ) )
|
||||
{
|
||||
switch( \IPS\Request::i()->featureAction )
|
||||
{
|
||||
case 'form':
|
||||
$current = NULL;
|
||||
if ( isset( \IPS\Request::i()->uniqueKey ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$current = static::load( \IPS\Request::i()->uniqueKey, 'feature_unique_key' );
|
||||
}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
}
|
||||
$form = new \IPS\Helpers\Form;
|
||||
static::form( $form, $application, $current, isset( \IPS\Request::i()->version ) ? \IPS\Request::i()->version : NULL );
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
if ( $current === NULL )
|
||||
{
|
||||
$current = new static;
|
||||
}
|
||||
|
||||
$current->formatFormValues( $values );
|
||||
$current->save();
|
||||
|
||||
\IPS\Output::i()->redirect( $url, 'saved' );
|
||||
}
|
||||
|
||||
return $form;
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
|
||||
$feature = static::load( \IPS\Request::i()->uniqueKey, 'feature_unique_key' );
|
||||
$feature->delete();
|
||||
|
||||
\IPS\Output::i()->redirect( $url, 'deleted' );
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$tree = new \IPS\Helpers\Tree\Tree(
|
||||
$url,
|
||||
'dev_features',
|
||||
function() use ( $application, $url ) {
|
||||
$return = array();
|
||||
$return[] = static::_devTableRow( -1, FALSE, $application, $url );
|
||||
$versions = $application->getAllVersions();
|
||||
krsort( $versions );
|
||||
foreach( $versions AS $long => $human )
|
||||
{
|
||||
$return[] = static::_devTableRow( $long, FALSE, $application, $url );
|
||||
}
|
||||
return $return;
|
||||
},
|
||||
function( $long, $root=FALSE ) use ( $application, $url ) {
|
||||
return static::_devTableRow( $long, $root, $application, $url );
|
||||
},
|
||||
function( $long ) {
|
||||
return NULL;
|
||||
},
|
||||
function( $long ) use ( $application, $url ) {
|
||||
$return = array();
|
||||
|
||||
$file = \IPS\ROOT_PATH . "/applications/{$application->directory}/setup/upg_" . ( $long == -1 ? 'working' : $long ) . "/whatsnew.json";
|
||||
if ( file_exists( $file ) )
|
||||
{
|
||||
if ( $data = @json_decode( \file_get_contents( $file ), TRUE ) )
|
||||
{
|
||||
foreach( $data AS $key => $row )
|
||||
{
|
||||
$return[] = \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row( $url, $long . '.' . $key, $row['feature_title'], FALSE, array(
|
||||
'edit' => array(
|
||||
'icon' => 'pencil',
|
||||
'title' => 'edit',
|
||||
'link' => $url->setQueryString( array( 'featureAction' => 'form', 'uniqueKey' => $row['feature_unique_key'] ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('edit') )
|
||||
),
|
||||
'delete' => array(
|
||||
'icon' => 'times',
|
||||
'title' => 'delete',
|
||||
'link' => $url->setQueryString( array( 'featureAction' => 'delete', 'uniqueKey' => $row['feature_unique_key'] ) ),
|
||||
'data' => array( 'confirm' => '' )
|
||||
)
|
||||
), NULL, NULL, NULL, FALSE );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
);
|
||||
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* Developer Management Table row
|
||||
*
|
||||
* @param string|int $long Long version
|
||||
* @param bool $root Is this a root row
|
||||
* @param \IPS\Application $application The application this feature belongs to
|
||||
* @param \IPS\Http\Url $url Base URL
|
||||
* @return string
|
||||
*/
|
||||
public static function _devTableRow( $long, $root, $application, $url )
|
||||
{
|
||||
$file = \IPS\ROOT_PATH . "/applications/{$application->directory}/setup/upg_" . ( $long == -1 ? 'working' : $long ) . "/whatsnew.json";
|
||||
|
||||
$hasChildren = FALSE;
|
||||
if ( file_exists( $file ) )
|
||||
{
|
||||
if ( $data = @json_decode( \file_get_contents( $file ), TRUE ) )
|
||||
{
|
||||
if ( \count( $data ) )
|
||||
{
|
||||
$hasChildren = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( $root === FALSE )
|
||||
{
|
||||
$buttons = array(
|
||||
'add' => array(
|
||||
'icon' => 'plus-circle',
|
||||
'title' => 'add_feature_highlight',
|
||||
'link' => $url->setQueryString( array( 'featureAction' => 'form', 'version' => $long ) ),
|
||||
'data' => array( 'ipsDialog' => '', 'ipsDialog-title' => \IPS\Member::loggedIn()->language()->addToStack('add_feature_highlight') )
|
||||
)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
$buttons = array();
|
||||
}
|
||||
|
||||
if ( $long === -1 )
|
||||
{
|
||||
$name = \IPS\Member::loggedIn()->language()->addToStack( 'versions_working' );
|
||||
}
|
||||
else
|
||||
{
|
||||
$name = $application->getAllVersions()[ $long ];
|
||||
}
|
||||
|
||||
return \IPS\Theme::i()->getTemplate( 'trees', 'core' )->row( $url, $long, $name, $hasChildren, $buttons, ( $long === -1 ) ? NULL : $long, NULL, NULL, $root );
|
||||
}
|
||||
|
||||
/**
|
||||
* Form
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form The form, passed by reference
|
||||
* @param \IPS\Application $application Current application
|
||||
* @param NULL|\IPS\Application\Feature $current Currently editing, or NULL for new.
|
||||
* @return void
|
||||
*/
|
||||
public static function form( &$form, \IPS\Application $application, ?\IPS\Application\Feature $current, ?int $version )
|
||||
{
|
||||
$form->hiddenValues['feature_app'] = $application->directory;
|
||||
$form->hiddenValues['feature_version_added'] = ( $current ) ? $current->version_added : $version;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'feature_title', ( $current ) ? $current->title : NULL, TRUE ) );
|
||||
$form->add( new \IPS\Helpers\Form\TextArea( 'feature_text', ( $current ) ? $current->text : NULL, TRUE, array( 'maxLength' => 300 ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'feature_image', NULL, FALSE, array( 'image' => TRUE, 'temporary' => TRUE ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Url( 'feature_url', ( $current ) ? $current->url : NULL, FALSE ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Save
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function save()
|
||||
{
|
||||
parent::save();
|
||||
|
||||
\IPS\Db::i()->insert( 'core_members_feature_seen', array( 'member_id' => \IPS\Member::loggedIn()->member_id, 'feature_id' => $this->id ), FALSE, TRUE );
|
||||
|
||||
static::writeJsonFile( $this->app, $this->version_added );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Delete
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function delete()
|
||||
{
|
||||
if ( $this->image )
|
||||
{
|
||||
@\unlink( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/{$this->image}" );
|
||||
}
|
||||
|
||||
parent::delete();
|
||||
|
||||
/* If there's no more features for this version, make sure features folder is cleaned up */
|
||||
if ( \is_dir( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/" ) AND !\IPS\Db::i()->select( 'COUNT(*)', 'core_members_features', array( "feature_version_added=? AND feature_app=?", $this->version_added, $this->app ) )->first() )
|
||||
{
|
||||
@\rmdir( \IPS\ROOT_PATH . "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/" );
|
||||
}
|
||||
|
||||
static::writeJsonFile( $this->app, $this->version_added );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Application
|
||||
*
|
||||
* @return \IPS\Application
|
||||
*/
|
||||
public function get__app(): \IPS\Application
|
||||
{
|
||||
return \IPS\Application::load( $this->_data['app'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format Form Values
|
||||
*
|
||||
* @param array $values Form Values
|
||||
* @return void
|
||||
*/
|
||||
public function formatFormValues( array $values )
|
||||
{
|
||||
$this->title = $values['feature_title'];
|
||||
$this->text = $values['feature_text'];
|
||||
$this->url = (string) $values['feature_url'];
|
||||
$this->version_added = ( isset( $values['feature_version_added'] ) AND $values['feature_version_added'] ) ? $values['feature_version_added'] : -1;
|
||||
$this->app = $values['feature_app'];
|
||||
|
||||
if ( \array_key_exists( 'feature_image', $values ) AND $values['feature_image'] )
|
||||
{
|
||||
/* Make a friendly file name */
|
||||
$filename = \IPS\Http\Url\Friendly::seoTitle( $values['feature_title'] ) . image_type_to_extension( exif_imagetype( $values['feature_image'] ) );
|
||||
|
||||
if ( !\is_dir( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/" ) )
|
||||
{
|
||||
mkdir( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/" );
|
||||
chmod( \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/", \IPS\IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
|
||||
$dir = \IPS\ROOT_PATH . "/applications/{$values['feature_app']}/setup/upg_" . ( $values['feature_version_added'] == -1 ? "working" : $values['feature_version_added'] ) . "/features/";
|
||||
if ( !\is_dir( $dir ) )
|
||||
{
|
||||
mkdir( $dir );
|
||||
chmod( $dir, \IPS\IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
|
||||
/* If there is an existing file, remove it */
|
||||
if ( $this->image AND @\file_exists( $dir . '/' . $this->image ) )
|
||||
{
|
||||
@\unlink( $dir . '/' . $this->image );
|
||||
}
|
||||
|
||||
\move_uploaded_file( $values['feature_image'], $dir . '/' . $filename );
|
||||
$this->image = $filename;
|
||||
}
|
||||
|
||||
if ( !$this->unique_key )
|
||||
{
|
||||
$this->unique_key = md5( $values['feature_app'] . $values['feature_title'] . mt_rand( 0, 1000 ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark feature as read
|
||||
*
|
||||
* @param \IPS\Member|NULL $member The member, or NULL for currently logged in member.
|
||||
* @param \IPS\Member\Feature $features Variable length amount of features to mark as read.
|
||||
* @return void
|
||||
*/
|
||||
public static function markRead( ?\IPS\Member $member, \IPS\Application\Feature ...$features )
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if( !$member->member_id )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
$insert = array();
|
||||
foreach( $features AS $feature )
|
||||
{
|
||||
$insert[] = array( 'member_id' => $member->member_id, 'feature_id' => $feature->id );
|
||||
}
|
||||
|
||||
if ( \count( $insert ) )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_members_feature_seen', $insert, TRUE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON Serialize
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$versions = $this->_app->getAllVersions();
|
||||
return array(
|
||||
'id' => $this->id,
|
||||
'version' => ( $this->version_added AND isset( $versions[ $this->version_added ] ) ) ? $versions[$this->version_added] : $this->_app->version,
|
||||
'title' => $this->title,
|
||||
'description' => $this->text,
|
||||
'more_info' => $this->url,
|
||||
'image' => $this->image ? (string) \IPS\Http\Url::internal( "/applications/{$this->app}/setup/upg_" . ( $this->version_added == -1 ? 'working' : $this->version_added ) . "/features/{$this->image}", 'none' ) : NULL
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Write Json File
|
||||
*
|
||||
* @param string $appKey Application key to write.
|
||||
* @param int $versionId Long version
|
||||
* @return void
|
||||
*/
|
||||
public static function writeJsonFile( string $appKey, int $versionId )
|
||||
{
|
||||
$path = \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/whatsnew.json";
|
||||
@unlink( $path );
|
||||
|
||||
if ( !\is_dir( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/" ) )
|
||||
{
|
||||
mkdir( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/" );
|
||||
chmod( \IPS\ROOT_PATH . "/applications/{$appKey}/setup/upg_" . ( $versionId == -1 ? 'working' : $versionId ) . "/", \IPS\IPS_FOLDER_PERMISSION );
|
||||
}
|
||||
|
||||
$data = array();
|
||||
foreach( \IPS\Db::i()->select( '*', 'core_members_features', array( "feature_app=? AND feature_version_added=?", $appKey, $versionId ), "feature_version_added DESC" ) AS $row )
|
||||
{
|
||||
$data[ $row['feature_unique_key'] ] = $row;
|
||||
}
|
||||
|
||||
if ( \count( $data ) )
|
||||
{
|
||||
\file_put_contents( $path, json_encode( $data, JSON_PRETTY_PRINT ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
@unlink( $path );
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+104
-24
@@ -289,7 +289,15 @@ abstract class _Comment extends \IPS\Content
|
||||
$obj->postCreate();
|
||||
|
||||
/* This needs to happen before the index is added */
|
||||
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
|
||||
if( $item::$firstCommentRequired and $obj->isFirst() )
|
||||
{
|
||||
$container = $obj->container();
|
||||
$parameters = array_merge( array( 'newContentItem-' . $item::$application . '/' . $item::$module . '-' . ( $container ? $container->_id : 0 ) ), $obj->attachmentIds() );
|
||||
}
|
||||
else
|
||||
{
|
||||
$parameters = array_merge( array( 'reply-' . $item::$application . '/' . $item::$module . '-' . $item->$idColumn ), $obj->attachmentIds() );
|
||||
}
|
||||
\IPS\File::claimAttachments( ...$parameters );
|
||||
|
||||
/* Add to search index */
|
||||
@@ -418,7 +426,7 @@ abstract class _Comment extends \IPS\Content
|
||||
|
||||
$item->save();
|
||||
|
||||
if ( !$item->hidden() and ! $item->approvedButHidden() and $item->containerWrapper() and $item->container()->_comments !== NULL )
|
||||
if ( !$item->hidden() and ! $item->approvedButHidden() and $item->containerWrapper() and $item->container()->_comments !== NULL and !$item->isFutureDate() )
|
||||
{
|
||||
$item->container()->_comments = ( $item->container()->_comments + 1 );
|
||||
$item->container()->setLastComment( $this );
|
||||
@@ -459,7 +467,66 @@ abstract class _Comment extends \IPS\Content
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time(), $class . '__' . $item->$itemIdColumn );
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
catch( \RedisException $e ) { }
|
||||
}
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $item->container() AND $item->container()->contentHeldForApprovalByNode( 'comment', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
/* Finally if the item itself requires moderation, that's why */
|
||||
if ( $foundReason === FALSE AND \is_array( $item::supportedMetaDataTypes() ) AND \in_array('core_ItemModeration', $item::supportedMetaDataTypes() ) AND (bool) \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->enabled( $item, $this->author() ) )
|
||||
{
|
||||
$log->held_reason = 'item';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,7 +1075,10 @@ abstract class _Comment extends \IPS\Content
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $oldItem ), $oldItem->$idColumn ) );
|
||||
\IPS\Db::i()->delete( 'core_item_member_map', array( 'map_class=? and map_item_id=?', \get_class( $item ), $item->$idColumn ) );
|
||||
}
|
||||
|
||||
|
||||
/* Update reports */
|
||||
\IPS\Db::i()->update( 'core_rc_index', array( 'item_id' => $item->$idColumn, 'node_id' => ( $item->containerWrapper() ? $item->containerWrapper()->_id : 0 ) ), array( 'class=? and content_id=?', \get_class( $this ), $oldItem->$idColumn ) );
|
||||
|
||||
if( $skip === FALSE )
|
||||
{
|
||||
$oldItem->rebuildFirstAndLastCommentData();
|
||||
@@ -1096,7 +1166,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Remove featured comment associations */
|
||||
if( $this->isFeatured() AND $item )
|
||||
if( $item and $this->isFeatured() )
|
||||
{
|
||||
\IPS\Application::load('core')->extensions( 'core', 'MetaData' )['FeaturedComments']->unfeatureComment( $item, $this );
|
||||
}
|
||||
@@ -1154,7 +1224,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Solvable' ) and isset( $itemClass::$databaseColumnMap['solved_comment_id'] ) )
|
||||
if ( $item AND \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Solvable' ) and isset( $itemClass::$databaseColumnMap['solved_comment_id'] ) )
|
||||
{
|
||||
/* Reset item */
|
||||
if ( $item->mapped('solved_comment_id') and $item->mapped('solved_comment_id') == $this->$idColumn )
|
||||
@@ -1165,7 +1235,7 @@ abstract class _Comment extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
if ( $item AND \IPS\IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$item->clearCachedStatistics();
|
||||
}
|
||||
@@ -1806,28 +1876,38 @@ abstract class _Comment extends \IPS\Content
|
||||
public function webhookFilters()
|
||||
{
|
||||
$filters = parent::webhookFilters();
|
||||
$item = $this->item();
|
||||
try
|
||||
{
|
||||
$item = $this->item();
|
||||
}
|
||||
catch ( \OutOfRangeException $e )
|
||||
{
|
||||
$item = NULL;
|
||||
}
|
||||
|
||||
/* This is kept for legacy compatibility, but starting with 4.6.13 the Content and Item class handle this inside their own webhookFilter methods */
|
||||
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item->hidden() ) );
|
||||
$filters['hidden'] = ( ( (bool) $this->hidden() ) or ( (bool) $item and $item->hidden() ) );
|
||||
|
||||
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
|
||||
if( $item )
|
||||
{
|
||||
$filters['locked'] = $item->locked();
|
||||
if ( \in_array( 'IPS\Content\Lockable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['locked'] = $item->locked();
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['pinned'] = (bool) $item->mapped('pinned');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Featurable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['featured'] = (bool) $item->mapped('featured');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Polls', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['hasPoll'] = (bool) $item->mapped('poll');
|
||||
}
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Pinnable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['pinned'] = (bool) $item->mapped('pinned');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Featurable', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['featured'] = (bool) $item->mapped('featured');
|
||||
}
|
||||
if ( \in_array( 'IPS\Content\Polls', class_implements( $item ) ) )
|
||||
{
|
||||
$filters['hasPoll'] = (bool) $item->mapped('poll');
|
||||
}
|
||||
|
||||
|
||||
return $filters;
|
||||
}
|
||||
|
||||
|
||||
+234
-32
@@ -404,6 +404,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
\IPS\Db::i()->delete( 'core_content_featured', [ 'feature_content_id=? and feature_content_class=?', $this->$idColumn, \get_class( $this ) ] );
|
||||
|
||||
\IPS\Api\Webhook::fire( str_replace( '\\', '', \substr( \get_called_class(), 3 ) ) . '_delete', $this, $this->webhookFilters() );
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
|
||||
parent::delete();
|
||||
|
||||
@@ -455,9 +461,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @param string $autoSaveLocation The autosave location key of any editors to check, or NULL to use the default.
|
||||
* @param string|NULL $autoSaveKeys The autosave keys (for new content) or attach lookup ids (for an edit) of any editors to check, or NULL to use the default.
|
||||
* @param array $imageUploads Images that have been uploaded that may require moderation
|
||||
* @param array|NULL $filtersMatched What matched, passed by reference
|
||||
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
|
||||
*/
|
||||
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE )
|
||||
public function shouldTriggerProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[], &$hiddenByFilter = FALSE, &$itemHiddenByFilter = FALSE, array &$filtersMatched = array() )
|
||||
{
|
||||
/* Set our content */
|
||||
$content = $content ?: $this->content();
|
||||
@@ -528,11 +535,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$binds = array_merge( $binds, $_autoSaveKeys );
|
||||
}
|
||||
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
if ( $this instanceof \IPS\Content\Item OR ( $first AND $item::$firstCommentRequired ) )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
|
||||
$attachmentsMapWhere = array_merge( array( 'location_key=? AND ( ' . implode( ' OR ', $clauses ) . ' )', $autoSaveLocation ), $binds );
|
||||
@@ -559,11 +578,23 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
if ( $this instanceof \IPS\Content\Item OR ( $first AND $item::$firstCommentRequired ) )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
|
||||
if ( $title AND !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
|
||||
$attachmentsMapWhere = \IPS\Db::i()->in( 'temp', array_map( 'md5', $autoSaveKeys ) );
|
||||
@@ -602,6 +633,15 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $hiddenByFilter OR $itemHiddenByFilter )
|
||||
{
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
$log->held_reason = 'image';
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,22 +652,22 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( !$itemHiddenByFilter )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
}
|
||||
if ( !$itemHiddenByFilter and $title )
|
||||
{
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
|
||||
$itemHiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( !$hiddenByFilter )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content );
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $content, $member, $filtersMatched );
|
||||
}
|
||||
if ( !$hiddenByFilter and $title )
|
||||
{
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title );
|
||||
$hiddenByFilter = \IPS\core\Profanity::hiddenByFilters( $title, $member, $filtersMatched );
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -650,13 +690,20 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
* @return bool Whether to send unapproved notifications (i.e. true if the content was hidden)
|
||||
*/
|
||||
public function checkProfanityFilters( $first=FALSE, $edit=TRUE, $content=NULL, $title=NULL, $autoSaveLocation=NULL, $autoSaveKeys=NULL, $imageUploads=[] )
|
||||
{
|
||||
{
|
||||
/* Check this content type is hideable */
|
||||
if ( !( $this instanceof \IPS\Content\Hideable ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/* Check it */
|
||||
$hiddenByFilter = FALSE;
|
||||
$itemHiddenByFilter = FALSE;
|
||||
$sendNotifications = FALSE;
|
||||
$filtersMatched = array();
|
||||
$item = ( $this instanceof \IPS\Content\Item ) ? $this : $this->item();
|
||||
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter );
|
||||
$this->shouldTriggerProfanityFilters( $first, $edit, $content, $title, $autoSaveLocation, $autoSaveKeys, $imageUploads, $hiddenByFilter, $itemHiddenByFilter, $filtersMatched );
|
||||
|
||||
/* If we need to hide the item, then do that */
|
||||
if ( $itemHiddenByFilter )
|
||||
@@ -698,6 +745,54 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->$column = ( $itemHiddenByFilter and $this instanceof \IPS\Content\Comment ) ? 2 : 1; # We use the special 2 flag to note it is only hidden because the parent is
|
||||
$this->save();
|
||||
}
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$itemColumnId = $item::$databaseColumnId;
|
||||
|
||||
try
|
||||
{
|
||||
if ( $this instanceof \IPS\Content\Comment AND $item::$firstCommentRequired AND $this->isFirst() )
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_class( $item ), $item->$itemColumnId );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
if ( isset( $filtersMatched['type'] ) AND isset( $filtersMatched['match'] ) )
|
||||
{
|
||||
$log = new \IPS\core\Approval;
|
||||
if ( $this instanceof \IPS\Content\Comment AND $item::$firstCommentRequired AND $this->isFirst() )
|
||||
{
|
||||
$log->content_class = \get_class( $item );
|
||||
$log->content_id = $item->$itemColumnId;
|
||||
}
|
||||
else
|
||||
{
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
}
|
||||
$log->held_reason = $filtersMatched['type'];
|
||||
switch( $filtersMatched['type'] )
|
||||
{
|
||||
case 'profanity':
|
||||
$log->held_data = array( 'word' => $filtersMatched['match'] );
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
$log->held_data = array( 'url' => $filtersMatched['match'] );
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
$log->held_data = array( 'email' => $filtersMatched['match'] );
|
||||
break;
|
||||
}
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* If we did either, then recount number of comments */
|
||||
@@ -706,8 +801,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$item->resyncCommentCounts();
|
||||
$item->resyncLastComment();
|
||||
$item->save();
|
||||
|
||||
if ( $container = $item->containerWrapper() )
|
||||
|
||||
if ( $first AND $container = $item->containerWrapper() )
|
||||
{
|
||||
$container->resetCommentCounts();
|
||||
$container->save();
|
||||
@@ -959,7 +1054,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->$column = $val;
|
||||
$this->save();
|
||||
|
||||
if ( $action == 'feature' )
|
||||
if ( $action == 'feature' and $this->author()->member_id )
|
||||
{
|
||||
\IPS\Db::i()->insert( 'core_content_featured', [
|
||||
'feature_content_id' => $this->$idColumn,
|
||||
@@ -1092,6 +1187,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
if ( $this->container() AND !$this->skipContainerRebuild )
|
||||
{
|
||||
$this->container()->resetCommentCounts();
|
||||
@@ -1109,10 +1206,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
else
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->container()->rebuildPostedIn( array($this->$idColumn) );
|
||||
}
|
||||
}
|
||||
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \BadMethodCallException $e ) {}
|
||||
catch( \OutOfRangeException $e ) {}
|
||||
@@ -1256,9 +1354,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$this->save();
|
||||
$this->onHide( $member );
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete( 'core_soft_delete_log', array( 'sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey ) );
|
||||
\IPS\Db::i()->insert( 'core_soft_delete_log', array(
|
||||
'sdl_obj_id' => $this->$idColumn,
|
||||
@@ -1283,6 +1381,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1346,10 +1450,10 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
$this->save();
|
||||
$this->onUnhide( ( $approving OR ( $pbr AND $this->hidden() === 0 ) ), $member );
|
||||
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( static::$hideLogKey )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
\IPS\Db::i()->delete('core_soft_delete_log', array('sdl_obj_id=? AND sdl_obj_key=?', $this->$idColumn, static::$hideLogKey));
|
||||
}
|
||||
|
||||
@@ -1405,6 +1509,12 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
$this->expireWidgetCaches();
|
||||
$this->adjustSessions();
|
||||
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn )->delete();
|
||||
}
|
||||
catch( \OutOfRangeException $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1443,7 +1553,42 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
catch ( \UnderflowException $e )
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
/* If this is requiring approval and has a logged reason */
|
||||
$hidden = $this->hidden();
|
||||
$item = NULL;
|
||||
if ( ( $this instanceof \IPS\Content\Comment ) )
|
||||
{
|
||||
$item = $this->item();
|
||||
/* If this is the first comment, and it's required, then we need to check the items hidden status instead */
|
||||
if ( $item::$firstCommentRequired AND $this->isFirst() AND $hidden !== 1 )
|
||||
{
|
||||
$hidden = $this->item()->hidden();
|
||||
}
|
||||
}
|
||||
|
||||
if ( $hidden === 1 )
|
||||
{
|
||||
/* If moderator, show the reason */
|
||||
$reason = NULL;
|
||||
if( $this->modPermission( 'unhide' ) )
|
||||
{
|
||||
$reason = ( $item and $item::$firstCommentRequired AND $this->isFirst() ) ? $item->approvalQueueReason() : $this->approvalQueueReason();
|
||||
}
|
||||
|
||||
if ( $reason )
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack( 'hidden_with_reason', FALSE, array( 'sprintf' => array( $reason ) ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Otherwise show a message that the content requires approval before it can be edited. */
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack( 'hidden_awaiting_approval' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->hiddenBlurb = \IPS\Member::loggedIn()->language()->addToStack('hidden');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1482,6 +1627,33 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Reason for requiring approval
|
||||
*/
|
||||
protected $_approvalQueueReason = NULL;
|
||||
|
||||
/**
|
||||
* Approval Queue Reason
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function approvalQueueReason()
|
||||
{
|
||||
if ( $this->_approvalQueueReason === NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$this->_approvalQueueReason = \IPS\core\Approval::loadFromContent( \get_class( $this ), $this->$idColumn )->reason();
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
$this->_approvalQueueReason = NULL;
|
||||
}
|
||||
}
|
||||
return $this->_approvalQueueReason;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can promote this comment/item?
|
||||
*
|
||||
@@ -1615,6 +1787,9 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Find or create an index */
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$item = ( $this instanceof \IPS\Content\Comment ) ? $this->item() : $this;
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
|
||||
try
|
||||
{
|
||||
$index = \IPS\core\Reports\Report::load( $this->$idColumn, 'content_id', array( 'class=?', \get_called_class() ) );
|
||||
@@ -1633,6 +1808,8 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$index->num_reports = 1;
|
||||
$index->num_comments = 0;
|
||||
$index->auto_moderation_exempt = 0;
|
||||
$index->item_id = $item->$itemIdColumn;
|
||||
$index->node_id = $item->containerWrapper() ? $item->containerWrapper()->_id : 0;
|
||||
}
|
||||
|
||||
/* Only set this to a new report if it is not already under review */
|
||||
@@ -1658,7 +1835,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
|
||||
/* Run automatic moderation */
|
||||
$index->runAutomaticModeration();
|
||||
|
||||
|
||||
/* Send notification to mods */
|
||||
$moderators = array( 'm' => array(), 'g' => array() );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_moderators' ) as $mod )
|
||||
@@ -1671,21 +1848,38 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
if ( $canView === FALSE )
|
||||
{
|
||||
$perms = json_decode( $mod['perms'], TRUE );
|
||||
|
||||
|
||||
if ( isset( $perms['can_view_reports'] ) AND $perms['can_view_reports'] === TRUE )
|
||||
{
|
||||
$canView = TRUE;
|
||||
}
|
||||
|
||||
/* Got nodes? */
|
||||
if ( $canView === TRUE and $container = $item->containerWrapper() and isset( $container::$modPerm ) )
|
||||
{
|
||||
if ( isset( $perms[ $container::$modPerm ] ) and $perms[ $container::$modPerm ] != '*' and $perms[ $container::$modPerm ] != -1 )
|
||||
{
|
||||
if ( empty( $perms[ $container::$modPerm ] ) or ! in_array( $item->mapped('container'), $perms[ $container::$modPerm ] ) )
|
||||
{
|
||||
$canView = FALSE;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if ( $canView === TRUE )
|
||||
{
|
||||
$moderators[ $mod['type'] ][] = $mod['id'];
|
||||
}
|
||||
}
|
||||
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'report_center', $index, array( $index, $reportInsert, $this ) );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( \count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( \count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $mem )
|
||||
{
|
||||
$notification->recipients->attach( \IPS\Member::constructFromData( $member ) );
|
||||
$memberObj = \IPS\Member::constructFromData( $mem );
|
||||
if( $this->canView( $memberObj ) )
|
||||
{
|
||||
$notification->recipients->attach( $memberObj );
|
||||
}
|
||||
}
|
||||
$notification->send();
|
||||
|
||||
@@ -1696,7 +1890,7 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
/* Return */
|
||||
return $index;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Change IP Address
|
||||
* @param string $ip The new IP address
|
||||
@@ -2433,10 +2627,11 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
$notification = new \IPS\Notification( \IPS\Application::load('core'), 'unapproved_content', $this, array( $this, $this->author() ) );
|
||||
foreach ( \IPS\Db::i()->select( '*', 'core_members', ( \count( $moderators['m'] ) ? \IPS\Db::i()->in( 'member_id', $moderators['m'] ) . ' OR ' : '' ) . \IPS\Db::i()->in( 'member_group_id', $moderators['g'] ) . ' OR ' . \IPS\Db::i()->findInSet( 'mgroup_others', $moderators['g'] ) ) as $member )
|
||||
{
|
||||
/* We don't need to notify the author of the content */
|
||||
if( $this->author()->member_id != $member['member_id'] )
|
||||
$member = \IPS\Member::constructFromData( $member );
|
||||
/* We don't need to notify the author of the content or when member cannot view this content*/
|
||||
if( $this->author()->member_id != $member->member_id AND $this->canView( $member ) )
|
||||
{
|
||||
$notification->recipients->attach(\IPS\Member::constructFromData($member));
|
||||
$notification->recipients->attach( $member );
|
||||
}
|
||||
}
|
||||
$notification->send();
|
||||
@@ -2607,7 +2802,14 @@ abstract class _Content extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if( $this instanceof \IPS\Content\Comment )
|
||||
{
|
||||
$item = $this->item();
|
||||
try
|
||||
{
|
||||
$item = $this->item();
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
+166
-18
@@ -252,7 +252,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
/* Score by timestamp for trending */
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.1, $class .'__' . $item->$idColumn );
|
||||
}
|
||||
catch( \Exception $e ) {}
|
||||
catch( \RedisException $e ) {}
|
||||
}
|
||||
|
||||
if ( ! $countUpdated )
|
||||
@@ -648,7 +648,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
catch ( \BadMethodCallException $e ) {}
|
||||
|
||||
/* Make sure we can edit and tag */
|
||||
if ( ( !$item->canEdit() OR !$item::canTag( NULL, $container ) ) )
|
||||
if ( !$item->canEdit() OR ( !$item::canTag( NULL, $container ) AND !\count( $item->tags() ) ) )
|
||||
{
|
||||
throw new \OutOfRangeException;
|
||||
}
|
||||
@@ -953,9 +953,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'hide_reason' ) );
|
||||
$this->moderationAlertField($form, $item);
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->modAction( \IPS\Request::i()->action, NULL, $values['hide_reason'] );
|
||||
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -969,6 +975,30 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
else
|
||||
{
|
||||
if( \IPS\Request::i()->action === 'lock' )
|
||||
{
|
||||
if ( \IPS\Member::loggedIn()->modPermission('can_manage_alerts') AND $item->author()->member_id )
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$this->moderationAlertField($form, $item);
|
||||
$this->_setBreadcrumbAndTitle( $item );
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->output = $form->customTemplate( array( \IPS\Theme::i()->getTemplate( 'forms', 'core' ), 'popupTemplate' ) );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$item->modAction( \IPS\Request::i()->action );
|
||||
}
|
||||
|
||||
@@ -1076,6 +1106,8 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
},
|
||||
'clubs' => TRUE
|
||||
) ) );
|
||||
|
||||
$this->moderationAlertField( $form, $item);
|
||||
|
||||
if ( isset( $class::$databaseColumnMap['moved_to'] ) )
|
||||
{
|
||||
@@ -1107,6 +1139,10 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$item->markRead( NULL, NULL, NULL, TRUE );
|
||||
}
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'] )
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
|
||||
\IPS\Session::i()->modLog( 'modlog__action_move', array( $item::$title => TRUE, $item->url()->__toString() => FALSE, $item->mapped( 'title' ) ?: ( method_exists( $item, 'item' ) ? $item->item()->mapped( 'title' ) : NULL ) => FALSE ), $item );
|
||||
|
||||
@@ -1369,19 +1405,32 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
$class = static::$contentModel;
|
||||
$item = $class::loadAndCheckPerms( \IPS\Request::i()->id );
|
||||
$item->react( \IPS\Content\Reaction::load( \IPS\Request::i()->reaction ) );
|
||||
|
||||
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->reaction );
|
||||
$item->react( $reaction );
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->json( array(
|
||||
$output = array(
|
||||
'status' => 'ok',
|
||||
'count' => \count( $item->reactions() ),
|
||||
'score' => $item->reactionCount(),
|
||||
'blurb' => ( \IPS\Settings::i()->reaction_count_display == 'count' ) ? '' : \IPS\Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $item )
|
||||
));
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$output['datalayer'] = array_replace( $item->getDataLayerProperties(), ['reaction_type' => $reaction->_title] );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( $output );
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
\IPS\core\DataLayer::i()->addEvent( 'content_react', array_replace( $item->getDataLayerProperties(), [ 'reaction_type' => $reaction->_title ] ) );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
}
|
||||
@@ -2218,7 +2267,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$comment = NULL;
|
||||
if ( mb_substr( $method, -7 ) === 'Comment' )
|
||||
{
|
||||
if ( isset( $class::$commentClass ) )
|
||||
if ( isset( $class::$commentClass ) and ( isset( \IPS\Request::i()->comment ) OR mb_substr( $method, 0, 8 ) === 'multimod' ) )
|
||||
{
|
||||
$class = $class::$commentClass;
|
||||
$method = '_' . mb_substr( $method, 0, mb_strlen( $method ) - 7 );
|
||||
@@ -2228,7 +2277,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
elseif ( mb_substr( $method, -6 ) === 'Review' )
|
||||
{
|
||||
if ( isset( $class::$reviewClass ) )
|
||||
if ( isset( $class::$reviewClass ) and ( isset( \IPS\Request::i()->review ) OR mb_substr( $method, 0, 8 ) === 'multimod' ) )
|
||||
{
|
||||
$class = $class::$reviewClass;
|
||||
$method = '_' . mb_substr( $method, 0, mb_strlen( $method ) - 6 );
|
||||
@@ -2243,7 +2292,15 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
|
||||
if ( !$comment or !method_exists( $this, $method ) )
|
||||
{
|
||||
\IPS\Output::i()->error( 'page_not_found', '2S136/B', 404, '' );
|
||||
if ( mb_substr( $method, 0, 4 ) === 'find' )
|
||||
{
|
||||
/* Nothing found, redirect to main URL */
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
}
|
||||
else
|
||||
{
|
||||
\IPS\Output::i()->error( 'page_not_found', '2S136/B', 404, '' );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2381,9 +2438,16 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
{
|
||||
$form = new \IPS\Helpers\Form;
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'hide_reason' ) );
|
||||
$this->moderationAlertField( $form, $comment);
|
||||
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$comment->modAction( 'hide', NULL, $values['hide_reason'] );
|
||||
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $comment);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2662,6 +2726,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$currentPageCount = $item->commentPageCount();
|
||||
|
||||
$valueField = $commentClass::$databaseColumnMap['content'];
|
||||
$idField = $commentClass::$databaseColumnId;
|
||||
|
||||
if ( $item::$firstCommentRequired and $comment->mapped( 'first' ) )
|
||||
{
|
||||
@@ -2700,6 +2765,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
\IPS\Output::i()->error( 'node_noperm_delete', '3S136/1K', 403, '' );
|
||||
}
|
||||
}
|
||||
|
||||
/* Reset best answer */
|
||||
if( $item->topic_answered_pid and $item->topic_answered_pid == $comment->$idField )
|
||||
{
|
||||
$item->topic_answered_pid = 0;
|
||||
$item->save();
|
||||
}
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
@@ -2759,7 +2831,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$oldItem = $item;
|
||||
|
||||
/* Construct a form */
|
||||
$form = $this->_splitForm( $item );
|
||||
$form = $this->_splitForm( $item, $comment );
|
||||
|
||||
/* Handle submissions */
|
||||
if ( $values = $form->values() )
|
||||
@@ -2855,6 +2927,11 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$oldItem->url()->__toString() => FALSE,
|
||||
$oldItem->mapped( 'title' ) => FALSE
|
||||
), $item );
|
||||
|
||||
if( isset( $values['moderation_alert_content'] ) AND $values['moderation_alert_content'] )
|
||||
{
|
||||
$this->sendModerationAlert( $values, $comment );
|
||||
}
|
||||
|
||||
/* Redirect to it */
|
||||
\IPS\Output::i()->redirect( $item->url() );
|
||||
@@ -3007,20 +3084,38 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
try
|
||||
{
|
||||
\IPS\Session::i()->csrfCheck();
|
||||
|
||||
$comment->react( \IPS\Content\Reaction::load( \IPS\Request::i()->reaction ) );
|
||||
|
||||
|
||||
$reaction = \IPS\Content\Reaction::load( \IPS\Request::i()->reaction );
|
||||
$comment->react( $reaction );
|
||||
|
||||
if ( \IPS\Request::i()->isAjax() )
|
||||
{
|
||||
\IPS\Output::i()->json( array(
|
||||
$output = array(
|
||||
'status' => 'ok',
|
||||
'count' => \count( $comment->reactions() ),
|
||||
'score' => $comment->reactionCount(),
|
||||
'blurb' => ( \IPS\Settings::i()->reaction_count_display == 'count' ) ? '' : \IPS\Theme::i()->getTemplate( 'global', 'core' )->reactionBlurb( $comment )
|
||||
));
|
||||
);
|
||||
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$item = $item ?? $comment->item();
|
||||
$object = ( !isset( $item::$databaseColumnMap['content'] ) AND ( $item::$commentClass ) AND $comment->isFirst() ) ? $item : $comment;
|
||||
$output['datalayer'] = array_replace( $object->getDataLayerProperties(), ['reaction_type' => $reaction->_title] );
|
||||
}
|
||||
|
||||
\IPS\Output::i()->json( $output );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Data Layer Event */
|
||||
if ( \IPS\Settings::i()->core_datalayer_enabled )
|
||||
{
|
||||
$properties = $comment->getDataLayerProperties();
|
||||
$properties['reaction_type'] = $reaction->_title;
|
||||
|
||||
\IPS\core\DataLayer::i()->addEvent( 'content_react', $properties );
|
||||
}
|
||||
\IPS\Output::i()->redirect( $comment->url() );
|
||||
}
|
||||
}
|
||||
@@ -3058,9 +3153,14 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$form = new \IPS\Helpers\Form( 'form', 'add_recommend_content' );
|
||||
$form->class = 'ipsForm_vertical ipsForm_fullWidth';
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'feature_mod_note', NULL, FALSE ) );
|
||||
$this->moderationAlertField($form, $comment);
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
$item->featureComment( $comment, $values['feature_mod_note'] );
|
||||
if( isset( $values['moderation_alert_content']) AND $values['moderation_alert_content'])
|
||||
{
|
||||
$this->sendModerationAlert($values, $item);
|
||||
}
|
||||
|
||||
\IPS\Session::i()->modLog( 'modlog__featured_comment', array(
|
||||
(string) $comment->url() => FALSE,
|
||||
@@ -3291,7 +3391,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
$classToCheck = $item;
|
||||
$checkAgainst = 'split_merge';
|
||||
}
|
||||
|
||||
|
||||
if ( !$classToCheck::modPermission( $checkAgainst, NULL, $item->containerWrapper() ) )
|
||||
{
|
||||
throw new \DomainException;
|
||||
@@ -3693,7 +3793,7 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
* @param \IPS\Content\Item $item The item
|
||||
* @return \IPS\Helpers\Form
|
||||
*/
|
||||
protected function _splitForm( \IPS\Content\Item $item )
|
||||
protected function _splitForm( \IPS\Content\Item $item, ?\IPS\Content\Comment $comment = NULL )
|
||||
{
|
||||
try
|
||||
{
|
||||
@@ -3808,7 +3908,13 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
}
|
||||
}
|
||||
}, NULL, NULL, 'split_into_url' ) );
|
||||
|
||||
|
||||
/* multi mod doesn't support moderation alerts , so if $comment isn't set, we can't use it */
|
||||
if( $comment )
|
||||
{
|
||||
$this->moderationAlertField($form, $comment );
|
||||
}
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
@@ -4062,4 +4168,46 @@ class _Controller extends \IPS\Helpers\CoverPhoto\Controller
|
||||
protected function finishManage( $item )
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the moderation alert fields
|
||||
*
|
||||
* @param \IPS\Helpers\Form $form
|
||||
* @return void
|
||||
*/
|
||||
protected function moderationAlertField( \IPS\Helpers\Form $form, \IPS\Content $item )
|
||||
{
|
||||
if( \IPS\Member::loggedIn()->modPermission('can_manage_alerts') AND $item->author()->member_id )
|
||||
{
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'moderation_alert', FALSE, FALSE, array( 'togglesOn' => array( 'moderation_alert_title', 'moderation_alert_content','moderation_alert_anonymous','moderation_alert_reply' ) ) ) );
|
||||
\IPS\Member::loggedIn()->language()->words[ 'moderation_alert'] = \IPS\Member::loggedIn()->language()->addToStack('moderation_alert_name', FALSE, ['sprintf' => [ $item->author()->name ] ] );
|
||||
$form->add( new \IPS\Helpers\Form\Text( 'moderation_alert_title', NULL, TRUE, array( 'maxLength' => 255 ), NULL, NULL, NULL, 'moderation_alert_title' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Editor( 'moderation_alert_content', NULL, TRUE, array( 'app' => 'core', 'key' => 'Alert', 'autoSaveKey' => 'createAlert', 'attachIds' => NULL ), NULL, NULL, NULL, 'moderation_alert_content' ) );
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'moderation_alert_anonymous', FALSE, TRUE, array( 'togglesOff' => array( 'moderation_alert_reply') ), NULL, NULL, NULL, 'moderation_alert_anonymous' ) );
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'moderation_alert_reply', 0, TRUE, array( 'disabled' => (bool) \IPS\Member::loggedIn()->members_disable_pm, 'options' => array( '0' => 'alert_no_reply', '1' => 'alert_can_reply', '2' => 'alert_must_reply' ) ), NULL, NULL, NULL, 'moderation_alert_reply' ) );
|
||||
|
||||
if ( \IPS\Member::loggedIn()->members_disable_pm )
|
||||
{
|
||||
\IPS\Member::loggedIn()->language()->words['alert_reply_desc'] = \IPS\Member::loggedIn()->language()->get('alert_reply__nopm_desc');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the alert
|
||||
*
|
||||
* @param array $values
|
||||
* @param Item|Comment $content
|
||||
* @return \IPS\core\Alerts\Alert
|
||||
*/
|
||||
protected function sendModerationAlert( array $values, \IPS\Content\Item|\IPS\Content\Comment $content ) : \IPS\core\Alerts\Alert
|
||||
{
|
||||
$values['alert_title'] = $values['moderation_alert_title'];
|
||||
$values['alert_content'] = $values['moderation_alert_content'];
|
||||
$values['alert_recipient_type'] = 'user';
|
||||
$values['alert_recipient_user'] = $content->author()->member_id;
|
||||
$values['alert_anonymous'] = $values['moderation_alert_anonymous'];
|
||||
$values['alert_reply'] = $values['moderation_alert_reply'];
|
||||
return \IPS\core\Alerts\Alert::_createFromForm( $values, NULL );
|
||||
}
|
||||
}
|
||||
+151
-22
@@ -223,14 +223,15 @@ abstract class _Item extends \IPS\Content
|
||||
* @param \IPS\DateTime $time The time
|
||||
* @param \IPS\Node\Model|NULL $container Container (e.g. forum), if appropriate
|
||||
* @param bool|NULL $hidden Hidden? (NULL to work our automatically)
|
||||
* @param \IPS\DateTime|NULL $futureDate Publish date for future items
|
||||
* @return static
|
||||
*/
|
||||
public static function createItem( \IPS\Member $author, $ipAddress, \IPS\DateTime $time, \IPS\Node\Model $container = NULL, $hidden=NULL )
|
||||
public static function createItem(\IPS\Member $author, $ipAddress, \IPS\DateTime $time, \IPS\Node\Model $container = NULL, $hidden=NULL)
|
||||
{
|
||||
/* Create the object */
|
||||
$obj = new static;
|
||||
|
||||
foreach ( array( 'date', 'updated', 'author', 'author_name', 'ip_address', 'last_comment', 'last_comment_by', 'last_comment_name', 'last_review', 'container', 'approved', 'hidden', 'locked', 'status', 'views', 'pinned', 'featured', 'is_future_entry', 'num_comments', 'num_reviews', 'unapproved_comments', 'hidden_comments', 'unapproved_reviews', 'hidden_reviews' ) as $k )
|
||||
foreach ( array( 'date', 'updated', 'author', 'author_name', 'ip_address', 'last_comment', 'last_comment_by', 'last_comment_name', 'last_review', 'container', 'approved', 'hidden', 'locked', 'status', 'views', 'pinned', 'featured', 'is_future_entry', 'num_comments', 'num_reviews', 'unapproved_comments', 'hidden_comments', 'unapproved_reviews', 'hidden_reviews', 'future_date' ) as $k )
|
||||
{
|
||||
if ( isset( static::$databaseColumnMap[ $k ] ) )
|
||||
{
|
||||
@@ -244,6 +245,8 @@ abstract class _Item extends \IPS\Content
|
||||
case 'last_comment':
|
||||
case 'last_review':
|
||||
case 'date':
|
||||
$val = $time->getTimestamp();
|
||||
break;
|
||||
case 'updated':
|
||||
$val = $time->getTimestamp();
|
||||
break;
|
||||
@@ -321,6 +324,9 @@ abstract class _Item extends \IPS\Content
|
||||
case 'is_future_entry':
|
||||
$val = ( $time->getTimestamp() > time() ) ? 1 : 0;
|
||||
break;
|
||||
case 'future_date':
|
||||
$val = ( $time->getTimestamp() > time() ) ? $time->getTimestamp() : time();
|
||||
break;
|
||||
}
|
||||
|
||||
foreach ( \is_array( static::$databaseColumnMap[ $k ] ) ? static::$databaseColumnMap[ $k ] : array( static::$databaseColumnMap[ $k ] ) as $column )
|
||||
@@ -329,7 +335,7 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Update the container */
|
||||
if ( $container )
|
||||
{
|
||||
@@ -399,10 +405,10 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Create the item */
|
||||
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
|
||||
|
||||
$time = ( static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) and isset( $values[ static::$formLangPrefix . 'date' ] ) and $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime ) ? $values[ static::$formLangPrefix . 'date' ] : new \IPS\DateTime;
|
||||
|
||||
/* Create the item */
|
||||
$obj = static::createItem( $member, \IPS\Request::i()->ipAddress(), $time, $container );
|
||||
$obj = static::createItem( $member, \IPS\Request::i()->ipAddress(), $time, $container, NULL );
|
||||
$obj->processBeforeCreate( $values );
|
||||
$obj->processForm( $values );
|
||||
$obj->save();
|
||||
@@ -423,9 +429,6 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
$idColumn = static::$databaseColumnId;
|
||||
$commentIdColumn = $commentClass::$databaseColumnId;
|
||||
|
||||
$parameters = array_merge( array( 'newContentItem-' . static::$application . '/' . static::$module . '-' . ( $container ? $container->_id : 0 ) ), $comment->attachmentIds() );
|
||||
\IPS\File::claimAttachments( ...$parameters );
|
||||
|
||||
if ( isset( static::$databaseColumnMap['first_comment_id'] ) )
|
||||
{
|
||||
@@ -858,9 +861,9 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
|
||||
/* Future date */
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) and static::canFuturePublish( NULL, $container ) and isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$column = static::$databaseColumnMap['date'];
|
||||
$column = static::$databaseColumnMap['future_date'];
|
||||
$return['date'] = new \IPS\Helpers\Form\Date( static::$formLangPrefix . 'date', ( $item and $item->$column ) ? \IPS\DateTime::ts( $item->$column ) : 0, FALSE, array( 'time' => TRUE, 'unlimited' => 0, 'unlimitedLang' => 'immediately'), NULL, NULL, NULL, static::$formLangPrefix . 'date' );
|
||||
}
|
||||
|
||||
@@ -1071,6 +1074,58 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
\IPS\core\IndexNow::addUrlToQueue( $this->url() );
|
||||
}
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $this->container() AND $this->container()->contentHeldForApprovalByNode( 'item', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1179,7 +1234,6 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
if ( $this instanceof \IPS\Content\FuturePublishing AND isset( $values[ static::$formLangPrefix . 'date' ] ) )
|
||||
{
|
||||
|
||||
$column = static::$databaseColumnMap['is_future_entry'];
|
||||
|
||||
if ( $container AND $this->$column )
|
||||
@@ -1195,6 +1249,8 @@ abstract class _Item extends \IPS\Content
|
||||
if ( $values[ static::$formLangPrefix . 'date' ] instanceof \IPS\DateTime AND $values[ static::$formLangPrefix . 'date' ]->getTimestamp() > time() )
|
||||
{
|
||||
/* Was not future, but now is */
|
||||
$dateColumn = static::$databaseColumnMap['future_date'];
|
||||
$this->$dateColumn = $values[ static::$formLangPrefix . 'date' ]->getTimestamp();
|
||||
$this->unPublish();
|
||||
}
|
||||
}
|
||||
@@ -1980,6 +2036,9 @@ abstract class _Item extends \IPS\Content
|
||||
}
|
||||
}
|
||||
|
||||
/* Update reports */
|
||||
\IPS\Db::i()->update( 'core_rc_index', array( 'node_id' => $container->_id ), array( 'class=? and content_id=?', \get_class( $this ), $oldContainer->_id ) );
|
||||
|
||||
/* Update caches */
|
||||
$this->expireWidgetCaches();
|
||||
|
||||
@@ -2153,6 +2212,13 @@ abstract class _Item extends \IPS\Content
|
||||
/* Update notifications */
|
||||
\IPS\Db::i()->update( 'core_notifications', array( 'item_id' => $this->$idColumn ), array( 'item_class=? and item_id=?', (string) \get_class( $item ), $item->$idColumn ) );
|
||||
|
||||
/* Follows */
|
||||
if ( $this instanceof \IPS\Content\Followable )
|
||||
{
|
||||
\IPS\Db::i()->update( 'core_follow', "`follow_id` = MD5( CONCAT( `follow_app`, ';', `follow_area`, ';', {$this->$idColumn}, ';', `follow_member_id` ) ), `follow_rel_id` = {$this->$idColumn}", array( "follow_id=MD5( CONCAT( `follow_app`, ';', `follow_area`, ';', {$item->$idColumn}, ';', `follow_member_id` ) )" ), array(), NULL, \IPS\Db::IGNORE );
|
||||
\IPS\Db::i()->delete( 'core_follow_count_cache', array( 'class=? AND id=?', \get_called_class(), (int) $this->$idColumn ) );
|
||||
}
|
||||
|
||||
/* Update moderation history */
|
||||
\IPS\Db::i()->update( 'core_moderator_logs', array( 'item_id' => $this->$idColumn ), array( 'item_id=? AND class=?', $item->$idColumn, (string) \get_class( $this ) ) );
|
||||
\IPS\Session::i()->modLog( 'modlog__action_merge', array( $item->mapped('title') => FALSE, $this->url()->__toString() => FALSE, $this->mapped('title') => FALSE ), $this );
|
||||
@@ -2927,13 +2993,26 @@ abstract class _Item extends \IPS\Content
|
||||
* @param bool $joinAuthor If true, will join the members table for the author
|
||||
* @param bool $joinLastCommenter If true, will join the members table for the last commenter
|
||||
* @param bool $showMovedLinks If true, moved item links are included in the results
|
||||
* @param array|null $location Array of item lat and long
|
||||
* @return \IPS\Patterns\ActiveRecordIterator|int
|
||||
*/
|
||||
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE )
|
||||
public static function getItemsWithPermission( $where=array(), $order=NULL, $limit=10, $permissionKey='read', $includeHiddenItems=\IPS\Content\Hideable::FILTER_AUTOMATIC, $queryFlags=0, \IPS\Member $member=NULL, $joinContainer=FALSE, $joinComments=FALSE, $joinReviews=FALSE, $countOnly=FALSE, $joins=NULL, $skipPermission=FALSE, $joinTags=TRUE, $joinAuthor=TRUE, $joinLastCommenter=TRUE, $showMovedLinks=FALSE, $location=NULL )
|
||||
{
|
||||
/* Are we trying to improve count performance? */
|
||||
$countShortcut = FALSE;
|
||||
|
||||
$having = NULL;
|
||||
if ( isset( $location['lat'] ) and isset( $location['lon'] ) )
|
||||
{
|
||||
/* Make sure co-ordinates are in a valid format regardless of locale */
|
||||
$location['lat'] = number_format( $location['lat'], 6, '.', '' );
|
||||
$location['lon'] = number_format( $location['lon'], 6, '.', '' );
|
||||
|
||||
$where[] = array( static::$databasePrefix . 'latitude' . ' IS NOT NULL AND ' . static::$databasePrefix . 'longitude' . ' IS NOT NULL' );
|
||||
$having = array( 'distance < 500' );
|
||||
$order = 'distance ASC';
|
||||
}
|
||||
|
||||
/* Do we really need tags? */
|
||||
if ( $joinTags and ! \IPS\Settings::i()->tags_enabled )
|
||||
{
|
||||
@@ -3135,6 +3214,12 @@ abstract class _Item extends \IPS\Content
|
||||
{
|
||||
static::$permissionSelect[ $lookupKey ] = array();
|
||||
$permQuery = \IPS\Db::i()->select( 'perm_type_id', 'core_permission_index', array( "core_permission_index.app='" . $containerClass::$permApp . "' AND core_permission_index.perm_type='" . $containerClass::$permType . "' AND (" . \IPS\Db::i()->findInSet( 'perm_' . $containerClass::$permissionMap[ $permissionKey ], $member->permissionArray() ) . ' OR ' . 'perm_' . $containerClass::$permissionMap[ $permissionKey ] . "='*' )" ) );
|
||||
|
||||
/* If we cannot access clubs, skip them */
|
||||
if ( \IPS\IPS::classUsesTrait( $containerClass, 'IPS\Content\ClubContainer' ) AND !$member->canAccessModule( \IPS\Application\Module::get( 'core', 'clubs', 'front' ) ) )
|
||||
{
|
||||
$containerWhere[] = array( $containerClass::$databaseTable . '.' . $containerClass::$databasePrefix . $containerClass::clubIdColumn() . ' IS NULL' );
|
||||
}
|
||||
|
||||
if ( \count( $containerWhere ) )
|
||||
{
|
||||
@@ -3287,6 +3372,11 @@ abstract class _Item extends \IPS\Content
|
||||
|
||||
$selectClause = static::$databaseTable . '.*';
|
||||
|
||||
if ( isset( $location['lat'] ) and isset( $location['lon'] ) and is_numeric( $location['lat'] ) and is_numeric( $location['lon'] ) )
|
||||
{
|
||||
$selectClause .= ', ( 3959 * acos( cos( radians(' . $location['lat'] . ') ) * cos( radians( ' . static::$databasePrefix . 'latitude' . ' ) ) * cos( radians( ' . static::$databasePrefix . 'longitude' . ' ) - radians( ' . $location['lon'] . ' ) ) + sin( radians( ' . $location['lat'] . ' ) ) * sin( radians( ' . static::$databasePrefix . 'latitude' . ') ) ) ) AS distance';
|
||||
}
|
||||
|
||||
if( $joinAuthor and isset( static::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
$selectClause .= ', author.*';
|
||||
@@ -3318,7 +3408,7 @@ abstract class _Item extends \IPS\Content
|
||||
$selectClause .= ', core_tags_cache.tag_cache_text';
|
||||
}
|
||||
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, $groupBy, NULL, $queryFlags );
|
||||
$select = \IPS\Db::i()->select( $selectClause, static::$databaseTable, $where, $order, $limit, $groupBy, $having, $queryFlags );
|
||||
}
|
||||
|
||||
/* Join stuff */
|
||||
@@ -3410,15 +3500,15 @@ abstract class _Item extends \IPS\Content
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
$where = array( array( static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
$where = array( array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['featured'] . '=?', 1 ) );
|
||||
if ( $container )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['container'] . '=?', $container->_id );
|
||||
}
|
||||
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( \get_called_class() ) ) )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
$where[] = array( static::$databaseTable . '.' . static::$databasePrefix . static::$databaseColumnMap['is_future_entry'] . '=?', 0 );
|
||||
}
|
||||
|
||||
return static::getItemsWithPermission( $where, $order, $limit );
|
||||
@@ -4876,7 +4966,9 @@ abstract class _Item extends \IPS\Content
|
||||
'app' => static::$application,
|
||||
'key' => mb_ucfirst( static::$module ),
|
||||
'autoSaveKey' => 'reply-' . static::$application . '/' . static::$module . '-' . $this->$idColumn,
|
||||
'minimize' => isset( \IPS\Request::i()->$submitted ) ? NULL : static::$formLangPrefix . '_comment_placeholder'
|
||||
'minimize' => isset( \IPS\Request::i()->$submitted ) ? NULL : static::$formLangPrefix . '_comment_placeholder',
|
||||
'contentClass' => \get_called_class(),
|
||||
'contentId' => $this->$idColumn
|
||||
), function() use( $self ) {
|
||||
if ( !$self->mergeConcurrentComment() )
|
||||
{
|
||||
@@ -8405,7 +8497,7 @@ abstract class _Item extends \IPS\Content
|
||||
public function unpublish( $member=NULL )
|
||||
{
|
||||
/* Now do the actual stuff */
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$future = static::$databaseColumnMap['is_future_entry'];
|
||||
|
||||
@@ -8440,15 +8532,21 @@ abstract class _Item extends \IPS\Content
|
||||
public function publish( $member=NULL )
|
||||
{
|
||||
/* Now do the actual stuff */
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['date'] ) )
|
||||
if ( isset( static::$databaseColumnMap['is_future_entry'] ) AND isset( static::$databaseColumnMap['future_date'] ) )
|
||||
{
|
||||
$date = static::$databaseColumnMap['date'];
|
||||
$date = static::$databaseColumnMap['future_date'];
|
||||
$future = static::$databaseColumnMap['is_future_entry'];
|
||||
|
||||
$this->$date = time();
|
||||
$this->$future = 0;
|
||||
}
|
||||
|
||||
if ( isset( static::$databaseColumnMap['date'] ) )
|
||||
{
|
||||
$dateField = \is_array( static::$databaseColumnMap['date'] ) ? static::$databaseColumnMap['date'][0] : static::$databaseColumnMap['date'];
|
||||
$this->$dateField = time();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
$this->onPublish( $member );
|
||||
|
||||
@@ -8458,10 +8556,41 @@ abstract class _Item extends \IPS\Content
|
||||
\IPS\Db::i()->update( 'core_tags_perms', array( 'tag_perm_visible' => 1 ), array( 'tag_perm_aai_lookup=?', $this->tagAAIKey() ) );
|
||||
}
|
||||
|
||||
/* Update the first comment */
|
||||
if ( static::$firstCommentRequired )
|
||||
{
|
||||
$comment = $this->firstComment();
|
||||
$date = $comment::$databaseColumnMap['date'];
|
||||
|
||||
$comment->$date = time();
|
||||
$comment->save();
|
||||
|
||||
if ( isset( static::$databaseColumnMap['last_comment'] ) )
|
||||
{
|
||||
$lastCommentField = static::$databaseColumnMap['last_comment'];
|
||||
if ( \is_array( $lastCommentField ) )
|
||||
{
|
||||
foreach ( $lastCommentField as $column )
|
||||
{
|
||||
$this->$column = time();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->$lastCommentField = time();
|
||||
}
|
||||
|
||||
$this->save();
|
||||
}
|
||||
}
|
||||
|
||||
/* Mark this item as read for the author */
|
||||
$this->markRead( $this->author() );
|
||||
|
||||
/* Update search index */
|
||||
if ( $this instanceof \IPS\Content\Searchable )
|
||||
{
|
||||
\IPS\Content\Search\Index::i()->index( $this );
|
||||
\IPS\Content\Search\Index::i()->index( ( static::$firstCommentRequired ) ? $this->firstComment() : $this );
|
||||
}
|
||||
|
||||
/* Send notifications if necessary */
|
||||
|
||||
@@ -1,471 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Facebook Promotion
|
||||
* @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 10 Feb 2017
|
||||
*/
|
||||
|
||||
namespace IPS\Content\Promote;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Facebook Promotion
|
||||
*/
|
||||
class _Facebook extends PromoteAbstract
|
||||
{
|
||||
/**
|
||||
* @brief Icon
|
||||
*/
|
||||
public static $icon = 'facebook';
|
||||
|
||||
/**
|
||||
* @brief Default settings
|
||||
*/
|
||||
public $defaultSettings = array(
|
||||
'token' => NULL,
|
||||
'owner' => NULL,
|
||||
'page_name' => NULL, //
|
||||
'permissions' => NULL,
|
||||
'members' => NULL,
|
||||
'page' => NULL, //
|
||||
'tags' => NULL,
|
||||
'image' => NULL,
|
||||
'last_sync' => 0,
|
||||
'shareable' => array(),
|
||||
'member_token' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Get image
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getPhoto()
|
||||
{
|
||||
if ( ! $this->settings['image'] or $this->settings['last_sync'] < time() - 86400 )
|
||||
{
|
||||
/* Fetch again */
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $this->settings['page'] . "/picture?type=large&appsecret_proof=" . static::appSecretProof( $this->settings['token'] ) )->request()->get();
|
||||
|
||||
$extension = str_replace( 'image/', '', $response->httpHeaders['Content-Type'] );
|
||||
$newFile = \IPS\File::create( 'core_Promote', 'facebook_' . $this->settings['page'] . '.' . $extension, (string) $response, NULL, FALSE, NULL, FALSE );
|
||||
|
||||
$this->saveSettings( array( 'image' => (string) $newFile->url, 'last_sync' => time() ) );
|
||||
}
|
||||
|
||||
return $this->settings['image'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get name
|
||||
*
|
||||
* @param string|NULL $serviceId Specific page/group ID
|
||||
* @return string
|
||||
*/
|
||||
public function getName( $serviceId=NULL )
|
||||
{
|
||||
if ( $serviceId )
|
||||
{
|
||||
$realId = mb_substr( $serviceId, 2 );
|
||||
if ( mb_substr( $serviceId, 0, 2 ) == 'p_' )
|
||||
{
|
||||
return isset( $this->settings['shareable']['pages'][ $realId ]['name'] ) ? $this->settings['shareable']['pages'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
else
|
||||
{
|
||||
return isset( $this->settings['shareable']['groups'][ $realId ]['name'] ) ? $this->settings['shareable']['groups'][ $realId ]['name'] : \IPS\Member::loggedIn()->language()->addToStack( 'promote_service_name_missing' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check publish permissions
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function canPostToPage()
|
||||
{
|
||||
$facebook = static::getLoginHandler();
|
||||
|
||||
if ( $facebook === NULL )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$authorizedScopes = $facebook->authorizedScopes( $this->member );
|
||||
|
||||
if( $authorizedScopes === NULL )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
foreach( array( 'manage_pages', 'publish_pages' ) as $perm )
|
||||
{
|
||||
if ( ! \in_array( $perm, $authorizedScopes ) )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the appSecretProof key
|
||||
*
|
||||
* @param string $token The Token
|
||||
* @return string
|
||||
*/
|
||||
protected static function appSecretProof( $token )
|
||||
{
|
||||
$facebook = static::getLoginHandler();
|
||||
return hash_hmac( 'sha256', $token, $facebook->settings['client_secret'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get form elements for this share service
|
||||
*
|
||||
* @param string $text Text for the text entry
|
||||
* @param string $link Short or full link (short when available)
|
||||
* @param string $content Additional text content (usually a comment, or the item content)
|
||||
*
|
||||
* @return array of form elements
|
||||
*/
|
||||
public function form( $text, $link=null, $content=null )
|
||||
{
|
||||
$shareable = array();
|
||||
$_text = '';
|
||||
|
||||
if ( $this->promote and $this->promote->id )
|
||||
{
|
||||
$_text = $text;
|
||||
|
||||
if ( isset( $this->promote->form_data['facebook'] ) AND $settings = $this->promote->form_data['facebook'] )
|
||||
{
|
||||
$shareable = $settings['shareable'];
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( \count( $this->settings['tags'] ) )
|
||||
{
|
||||
$_text .= '#' . implode( ' #', $this->settings['tags'] );
|
||||
}
|
||||
}
|
||||
|
||||
$names = array();
|
||||
$options = array();
|
||||
if ( isset( $this->settings['shareable'] ) )
|
||||
{
|
||||
if( isset( $this->settings['shareable']['pages'] ) )
|
||||
{
|
||||
foreach( $this->settings['shareable']['pages'] as $id => $data )
|
||||
{
|
||||
$options[ 'p_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_page_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
}
|
||||
|
||||
if( isset( $this->settings['shareable']['groups'] ) )
|
||||
{
|
||||
foreach( $this->settings['shareable']['groups'] as $id => $data )
|
||||
{
|
||||
$options[ 'g_' . $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'promote_facebook_group_prefix', FALSE, array( 'sprintf' => array( $data['name'] ) ) );
|
||||
$names[] = $data['name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
new \IPS\Helpers\Form\TextArea( 'promote_social_content_facebook', $_text, FALSE, array( 'maxLength' => 2000, 'rows' => 10 ) ),
|
||||
new \IPS\Helpers\Form\CheckboxSet( 'promote_facebook_shareable', $shareable, FALSE, array( 'options' => $options ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow for any extra processing
|
||||
*
|
||||
* @param array $values Values from the form isn't it though
|
||||
* @return mixed
|
||||
*/
|
||||
public function processPromoteForm( $values )
|
||||
{
|
||||
if ( isset( $values['promote_facebook_shareable'] ) )
|
||||
{
|
||||
return array( 'shareable' => $values['promote_facebook_shareable'] );
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Post to Facebook
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @return void
|
||||
*/
|
||||
public function post( $promote )
|
||||
{
|
||||
$photos = $promote->imageObjects();
|
||||
|
||||
$settings = $promote->form_data['facebook'];
|
||||
$shareable = $this->settings['shareable'];
|
||||
$responses = array();
|
||||
|
||||
foreach( $settings['shareable'] as $id )
|
||||
{
|
||||
$feedId = mb_substr( $id, 2 );
|
||||
|
||||
if ( mb_substr( $id, 0, 2 ) == 'p_' )
|
||||
{
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, $shareable['pages'][ $feedId ]['token'] );
|
||||
}
|
||||
else
|
||||
{
|
||||
$responses[ $id ][] = $this->_post( $promote, $photos, $feedId, isset( $this->settings['member_token']['access_token'] ) ? $this->settings['member_token']['access_token'] : $this->settings['token'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $responses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Actually post the post, but not post post as that would be too late
|
||||
*
|
||||
* @param \IPS\core\Promote $promote Promote Object
|
||||
* @param array $photos The photos
|
||||
* @param int $feedId The feed ID to post to. Honestly, it's not rocket science.
|
||||
* @param string $token I'll give you one guess. Does anyone actually read this?
|
||||
* @return stuff
|
||||
*/
|
||||
protected function _post( $promote, $photos, $feedId, $token=NULL )
|
||||
{
|
||||
/* Get the last 20 items to see if we have a duplicate */
|
||||
try
|
||||
{
|
||||
$items = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->setQueryString( $this->_addToken( array(
|
||||
'limit' => 20
|
||||
), $token ) )->request()->get()->decodeJson();
|
||||
|
||||
if ( isset( $items['data'] ) )
|
||||
{
|
||||
foreach( $items['data'] as $item )
|
||||
{
|
||||
$time = new \IPS\DateTime( ( isset( $item['created_time'] ) ? $item['created_time'] : $item['updated_time' ] ) );
|
||||
$now = new \IPS\DateTime();
|
||||
|
||||
/* Only look back past the last 30 mins */
|
||||
if ( \intval( $now->diff( $time )->format('%i') ) > 30 )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_replace( '#\s{1,}#', " ", $promote->text['facebook'] ) == preg_replace( '#\s{1,}#', " ", $item['message'] ) )
|
||||
{
|
||||
/* Duplicate */
|
||||
return $item['id'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
}
|
||||
|
||||
if ( ! $photos or ( \is_array( $photos ) and \count( $photos ) === 1 ) )
|
||||
{
|
||||
/* Simple message */
|
||||
try
|
||||
{
|
||||
if ( $photos )
|
||||
{
|
||||
$thePhoto = array_pop( $photos );
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $thePhoto->url ),
|
||||
'link' => (string) $promote->short_link
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
else
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/feed" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'link' => (string) $promote->short_link
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
\IPS\Log::log( $response['error']['message'], 'facebook' );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
/* We have multiple photos so we first need to create an album, and then upload into that */
|
||||
try
|
||||
{
|
||||
/* The item may be a comment, which uses a language string for objectTitle - make sure that is parsed before sending */
|
||||
$title = $promote->objectTitle;
|
||||
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $title );
|
||||
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $feedId . "/albums" )->request()->post( $this->_addToken( array(
|
||||
'name' => $title,
|
||||
), $token ) )->decodeJson();
|
||||
|
||||
if ( ! isset( $response['id'] ) )
|
||||
{
|
||||
throw new \InvalidArgumentException('Could not create album');
|
||||
}
|
||||
|
||||
$newAlbumId = $response['id'];
|
||||
|
||||
foreach( $photos as $photo )
|
||||
{
|
||||
$response = \IPS\Http\Url::external( "https://graph.facebook.com/" . $newAlbumId . "/photos" )->request()->post( $this->_addToken( array(
|
||||
'message' => $promote->text['facebook'],
|
||||
'url' => (string) $this->returnUrlWithProtocol( $photo->url ),
|
||||
), $token ) )->decodeJson();
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
\IPS\Log::log( $e, 'facebook' );
|
||||
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->get('facebook_publish_exception') );
|
||||
}
|
||||
|
||||
if ( isset( $response['id'] ) )
|
||||
{
|
||||
return $response['id'];
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \InvalidArgumentException( \IPS\Member::loggedIn()->language()->addToStack('facebook_publish_exception') );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds 'access_token' to query string if token defined (it is for pages, but not for groups
|
||||
*
|
||||
* @param array $values The values of the array
|
||||
* @param string|NULL $token The token obvs
|
||||
* @return array
|
||||
*/
|
||||
protected function _addToken( $values, $token )
|
||||
{
|
||||
return ( $token ) ? array_merge( $values, array( 'access_token' => $token, 'appsecret_proof' => static::appSecretProof( $token ) ) ) : $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the published URL
|
||||
*
|
||||
* @param array $data Data returned from a successful POST
|
||||
* @return \IPS\Http\Url
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getUrl( $data )
|
||||
{
|
||||
if ( \is_array( $data ) and \count( $data ) )
|
||||
{
|
||||
/* Get a random URL from the many it was shared to */
|
||||
$array = $data[ array_rand( $data ) ];
|
||||
$data = array_pop( $array );
|
||||
}
|
||||
|
||||
if ( $data and preg_match( '#^[0-9_]*$#', $data ) )
|
||||
{
|
||||
return \IPS\Http\Url::external( 'https://facebook.com/' . $data );
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this member connected to the Facebook app?
|
||||
*
|
||||
* @param \IPS\Member $member A member object
|
||||
* @return boolean
|
||||
*/
|
||||
public function isConnected( $member )
|
||||
{
|
||||
if ( \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
|
||||
{
|
||||
/* Standard handler */
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
return $facebook->canProcess( $member );
|
||||
}
|
||||
|
||||
/* Custom */
|
||||
if ( isset( $this->settings['member_token']['access_token'] ) )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the URL is not protocol relative
|
||||
*
|
||||
* @return \IPS\Http\Url
|
||||
*/
|
||||
protected function returnUrlWithProtocol( \IPS\Http\Url $url )
|
||||
{
|
||||
if ( ! $url->data['scheme'] )
|
||||
{
|
||||
$url = $url->setScheme( ( \substr( \IPS\Settings::i()->base_url, 0, 5 ) == 'https' ) ? 'https' : 'http' );
|
||||
}
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to Facebook log in handler with the correct settings
|
||||
*
|
||||
* @return \IPS\Login object
|
||||
*/
|
||||
public static function getLoginHandler()
|
||||
{
|
||||
$facebook = \IPS\Login\Handler::findMethod('IPS\Login\Handler\Oauth2\Facebook');
|
||||
|
||||
if ( $facebook === NULL )
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* We are using the default app here */
|
||||
if ( ! \is_numeric( \IPS\Settings::i()->promote_facebook_auth ) )
|
||||
{
|
||||
$facebook->settings = json_decode( \IPS\Settings::i()->promote_facebook_auth, TRUE );
|
||||
}
|
||||
|
||||
return $facebook;
|
||||
}
|
||||
}
|
||||
@@ -166,7 +166,11 @@ trait Reactable
|
||||
$itemIdColumn = $item::$databaseColumnId;
|
||||
$itemId = $item->$itemIdColumn;
|
||||
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
|
||||
try
|
||||
{
|
||||
\IPS\Redis::i()->zIncrBy( 'trending', time() * 0.4, \get_class( $item ) .'__' . $itemId );
|
||||
}
|
||||
catch( \RedisException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,65 +523,89 @@ trait Reactable
|
||||
/**
|
||||
* React Blurb
|
||||
*
|
||||
* @return string
|
||||
* @return array
|
||||
*/
|
||||
public function reactBlurb()
|
||||
public function reactBlurb(): array
|
||||
{
|
||||
if ( $this->reactBlurb === NULL )
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
|
||||
if ( \count( $this->reactions() ) )
|
||||
|
||||
/*
|
||||
If we have lots of rows, then use a more efficient way of getting the data (but does increase query count and uses a group by).
|
||||
I know this is an artibrary number, but it should mean that most communities never need to run this code, and just those with topics with more than 4,000 pages
|
||||
*/
|
||||
if ( ( $this instanceof \IPS\Content\Item ) and isset( $this::$databaseColumnMap['num_comments'] ) and $this->mapped( 'num_comments' ) >= 100000 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( \is_array( $this->_reactions ) )
|
||||
$enabledReactions = [];
|
||||
foreach( \IPS\Content\Reaction::enabledReactions() as $reaction )
|
||||
{
|
||||
foreach( $this->_reactions AS $memberId => $reactions )
|
||||
{
|
||||
foreach( $reactions as $reaction )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $reaction ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $reaction ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $reaction ]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) AS $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[ $rep ] ) )
|
||||
{
|
||||
$this->reactBlurb[ $rep ] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[ $rep ]++;
|
||||
}
|
||||
$enabledReactions[] = $reaction->id;
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
|
||||
@uksort( $this->reactBlurb, function( $a, $b ) use( $enabledReactions ) {
|
||||
$positionA = $enabledReactions[ $a ]->position;
|
||||
$positionB = $enabledReactions[ $b ]->position;
|
||||
|
||||
if ( $positionA == $positionB )
|
||||
foreach( \IPS\Db::i()->select( 'COUNT(*) as count, reaction', 'core_reputation_index', $this->getReactionWhereClause( NULL , FALSE ), 'count DESC', NULL, 'reaction' ) as $row )
|
||||
{
|
||||
if( in_array( needle: $row['reaction'], haystack: $enabledReactions ) )
|
||||
{
|
||||
return 0;
|
||||
$this->reactBlurb[ $row['reaction'] ] = $row['count'];
|
||||
}
|
||||
|
||||
return ( $positionA < $positionB ) ? -1 : 1;
|
||||
} );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
if ( \count( $this->reactions() ) )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
if ( \is_array( $this->_reactions ) )
|
||||
{
|
||||
foreach ( $this->_reactions as $memberId => $reactions )
|
||||
{
|
||||
foreach ( $reactions as $reaction )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[$reaction] ) )
|
||||
{
|
||||
$this->reactBlurb[$reaction] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[$reaction]++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach ( \IPS\Db::i()->select( 'reaction', 'core_reputation_index', $this->getReactionWhereClause() )->join( 'core_reactions', 'reaction=reaction_id' ) as $rep )
|
||||
{
|
||||
if ( !isset( $this->reactBlurb[$rep] ) )
|
||||
{
|
||||
$this->reactBlurb[$rep] = 0;
|
||||
}
|
||||
|
||||
$this->reactBlurb[$rep]++;
|
||||
}
|
||||
}
|
||||
|
||||
/* Error suppressor for https://bugs.php.net/bug.php?id=50688 */
|
||||
$enabledReactions = \IPS\Content\Reaction::enabledReactions();
|
||||
|
||||
@uksort( $this->reactBlurb, function ( $a, $b ) use ( $enabledReactions ) {
|
||||
$positionA = $enabledReactions[$a]->position;
|
||||
$positionB = $enabledReactions[$b]->position;
|
||||
|
||||
if ( $positionA == $positionB )
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ( $positionA < $positionB ) ? -1 : 1;
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
$this->reactBlurb = array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $this->reactBlurb;
|
||||
}
|
||||
|
||||
|
||||
@@ -172,6 +172,65 @@ abstract class _Review extends \IPS\Content\Comment
|
||||
}
|
||||
|
||||
$item->save();
|
||||
|
||||
/* Was it moderated? Let's see why. */
|
||||
if ( $this->hidden() === 1 )
|
||||
{
|
||||
$idColumn = static::$databaseColumnId;
|
||||
|
||||
/* Check we don't already have a reason from profanity / url / email filters */
|
||||
try
|
||||
{
|
||||
\IPS\core\Approval::loadFromContent( \get_called_class(), $this->$idColumn );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* If the user is mod-queued - that's why. These will cascade, so check in that order. */
|
||||
$foundReason = FALSE;
|
||||
$log = new \IPS\core\Approval;
|
||||
$log->content_class = \get_called_class();
|
||||
$log->content_id = $this->$idColumn;
|
||||
if ( $this->author()->mod_posts )
|
||||
{
|
||||
|
||||
$log->held_reason = 'user';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't mod queued, but is in a group that is, that's why. */
|
||||
if ( $foundReason === FALSE AND $this->author()->group['g_mod_preview'] )
|
||||
{
|
||||
$log->held_reason = 'group';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
/* If the user isn't on mod queue, but the container requires approval, that's why. */
|
||||
if ( $foundReason === FALSE )
|
||||
{
|
||||
try
|
||||
{
|
||||
if ( $item->container() AND $item->container()->contentHeldForApprovalByNode( 'review', $this->author() ) === TRUE )
|
||||
{
|
||||
$log->held_reason = 'node';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
}
|
||||
catch( \BadMethodCallException $e ) { }
|
||||
}
|
||||
|
||||
/* Finally if the item itself requires moderation, that's why */
|
||||
if ( $foundReason === FALSE AND \is_array( $item::supportedMetaDataTypes() ) AND \in_array('core_ItemModeration', $item::supportedMetaDataTypes() ) AND (bool) \IPS\Application::load('core')->extensions( 'core', 'MetaData' )['ItemModeration']->enabled( $item, $this->author() ) )
|
||||
{
|
||||
$log->held_reason = 'item';
|
||||
$foundReason = TRUE;
|
||||
}
|
||||
|
||||
if ( $foundReason )
|
||||
{
|
||||
$log->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -51,80 +51,87 @@ class _Index extends \IPS\Content\Search\Index
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
|
||||
$settings = array(
|
||||
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
|
||||
);
|
||||
if ( $analyzer === 'custom' )
|
||||
{
|
||||
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
|
||||
$analyzer = key( $settings['analysis']['analyzer'] );
|
||||
}
|
||||
|
||||
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
|
||||
|
||||
$definition = array(
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'_doc' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
$analyzer = \IPS\Settings::i()->search_elastic_analyzer;
|
||||
$settings = array(
|
||||
'max_result_window' => \IPS\Settings::i()->search_index_maxresults
|
||||
);
|
||||
if ( $analyzer === 'custom' )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
$settings['analysis'] = json_decode( '{' . \IPS\Settings::i()->search_elastic_custom_analyzer . '}', TRUE );
|
||||
$analyzer = key( $settings['analysis']['analyzer'] );
|
||||
}
|
||||
|
||||
\IPS\Content\Search\Elastic\Index::request( $this->url )->delete();
|
||||
|
||||
$definition = array(
|
||||
'settings' => $settings,
|
||||
'mappings' => array(
|
||||
'_doc' => array(
|
||||
'properties' => array(
|
||||
'index_class' => array( 'type' => 'keyword' ),
|
||||
'index_object_id' => array( 'type' => 'long' ),
|
||||
'index_item_id' => array( 'type' => 'long' ),
|
||||
'index_container_class' => array( 'type' => 'keyword' ),
|
||||
'index_container_id' => array( 'type' => 'long' ),
|
||||
'index_title' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_content' => array(
|
||||
'type' => 'text',
|
||||
'analyzer' => $analyzer,
|
||||
),
|
||||
'index_permissions' => array( 'type' => 'keyword' ),
|
||||
'index_date_created' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_updated' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_date_commented' => array(
|
||||
'type' => 'date',
|
||||
'format' => 'epoch_second',
|
||||
),
|
||||
'index_author' => array( 'type' => 'long' ),
|
||||
'index_tags' => array( 'type' => 'keyword' ),
|
||||
'index_prefix' => array( 'type' => 'keyword' ),
|
||||
'index_hidden' => array( 'type' => 'byte' ),
|
||||
'index_item_index_id' => array( 'type' => 'keyword' ),
|
||||
'index_item_author' => array( 'type' => 'long' ),
|
||||
'index_is_last_comment' => array( 'type' => 'boolean' ),
|
||||
'index_club_id' => array( 'type' => 'long' ),
|
||||
'index_class_type_id_hash' => array( 'type' => 'keyword' ),
|
||||
'index_comments' => array( 'type' => 'long' ),
|
||||
'index_reviews' => array( 'type' => 'long' ),
|
||||
'index_participants' => array( 'type' => 'long' ),
|
||||
'index_is_anon' => array( 'type' => 'byte' ),
|
||||
'index_item_solved' => array( 'type' => 'byte' )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
try
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url->setQueryString( 'include_type_name', 'true' ) )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
|
||||
if( $response->httpResponseCode != 200 )
|
||||
{
|
||||
throw new \RuntimeException;
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
}
|
||||
}
|
||||
catch( \Exception $e )
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
$response = \IPS\Content\Search\Elastic\Index::request( $this->url )->setHeaders( array( 'Content-Type' => 'application/json' ) )->put( json_encode( $definition ) );
|
||||
\IPS\Log::log( $e, 'elasticsearch' );
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,13 +321,20 @@ class _Index extends \IPS\Content\Search\Index
|
||||
$participants += iterator_to_array( \IPS\Db::i()->select( 'DISTINCT ' . $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['author'], $reviewClass::$databaseTable, array( $reviewClass::$databasePrefix . $reviewClass::$databaseColumnMap['item'] . '=?', $item->$databaseColumnId ) ) );
|
||||
}
|
||||
$participants = array_values( array_unique( $participants ) );
|
||||
|
||||
|
||||
$isSolved = 0;
|
||||
if ( \IPS\IPS::classUsesTrait( $item, 'IPS\Content\Solvable' ) and $item->isSolved() )
|
||||
{
|
||||
$isSolved = 1;
|
||||
}
|
||||
|
||||
return array(
|
||||
'index_comments' => $item->mapped('num_comments'),
|
||||
'index_reviews' => $item->mapped('num_reviews'),
|
||||
'index_participants' => $participants,
|
||||
'index_tags' => $item->tags(),
|
||||
'index_prefix' => $item->prefix(),
|
||||
'index_item_solved' => $isSolved
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -390,10 +390,24 @@ class _Query extends \IPS\Content\Search\Query
|
||||
/* Some container types cannot be cached */
|
||||
$classes = [];
|
||||
$conditions = [];
|
||||
$noSimilarContentClasses = [];
|
||||
|
||||
foreach ( \IPS\Application::allExtensions( 'core', 'ContentRouter', FALSE ) as $object )
|
||||
{
|
||||
$classes = array_merge( $object->classes, $classes );
|
||||
|
||||
if ( ! $object->similarContent )
|
||||
{
|
||||
foreach( $object->classes as $class )
|
||||
{
|
||||
$noSimilarContentClasses[] = $class;
|
||||
|
||||
if ( is_subclass_of( $class, 'IPS\Content\Item' ) and isset( $class::$commentClass ) )
|
||||
{
|
||||
$noSimilarContentClasses[] = $class::$commentClass;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Some nodes have complex permissions, so avoid any permission issues by not selecting these */
|
||||
@@ -440,6 +454,20 @@ class _Query extends \IPS\Content\Search\Query
|
||||
}
|
||||
}
|
||||
|
||||
/* Prevent some things from being in similar content widget */
|
||||
if ( \count( $noSimilarContentClasses ) )
|
||||
{
|
||||
$this->mustNot[] = array(
|
||||
'bool' => array(
|
||||
'filter' => array(
|
||||
array(
|
||||
'terms' => array( 'index_class' => $noSimilarContentClasses )
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/* Only show non-hidden and approved items */
|
||||
$this->setHiddenFilter( static::HIDDEN_VISIBLE );
|
||||
}
|
||||
|
||||
@@ -565,8 +565,6 @@ class _Index extends \IPS\Content\Search\Index
|
||||
{
|
||||
if ( $cutoff )
|
||||
{
|
||||
$prefix = \IPS\Db::i()->prefix;
|
||||
|
||||
\IPS\Db::i()->delete( array( 'core_search_index_item_map', 'core_search_index' ), array( "core_search_index_item_map.index_item_id=core_search_index.index_item_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_item_map' );
|
||||
\IPS\Db::i()->delete( array( 'core_search_index_tags', 'core_search_index' ), array( "core_search_index_tags.index_id=core_search_index.index_id AND (core_search_index.index_id=core_search_index.index_item_index_id AND index_date_updated < ?)", $cutoff->getTimestamp() ), NULL, NULL, NULL, 'core_search_index_tags' );
|
||||
\IPS\Db::i()->delete( 'core_search_index', array( 'index_date_updated < ?', $cutoff->getTimestamp() ) );
|
||||
|
||||
@@ -1263,15 +1263,18 @@ class _Query extends \IPS\Content\Search\Query
|
||||
$order = $this->order;
|
||||
|
||||
/* This forces MySQL to strongly hint a better index to the optimiser for MySQL 5.7+, even though sorting will not be affected */
|
||||
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
|
||||
if ( !\IPS\CIC )
|
||||
{
|
||||
$order .= ", index_date_updated";
|
||||
}
|
||||
|
||||
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
|
||||
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
|
||||
{
|
||||
$this->forceIndex = 'index_date_updated';
|
||||
if ( $this->filterByUnread !== FALSE and ( ! mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) and \IPS\Db::i()->server_version >= 50700 ) )
|
||||
{
|
||||
$order .= ", index_date_updated";
|
||||
}
|
||||
|
||||
/* If we're filtering by unread, and not using MariaDB, then force an index because MySQL tends to use the incorrect one in this scenario. */
|
||||
if( !mb_stristr( \IPS\Db::i()->server_info, '-MariaDB' ) AND $this->filterByUnread === TRUE )
|
||||
{
|
||||
$this->forceIndex = 'index_date_updated';
|
||||
}
|
||||
}
|
||||
|
||||
/* But we're sorting by relevancy, we need to actually select that value with our fancy algorithm */
|
||||
|
||||
@@ -114,6 +114,13 @@ trait Solvable
|
||||
$notification->recipients->attach( $comment->author() );
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'item' => $this,
|
||||
'comment' => $comment,
|
||||
'markedBy' => $member
|
||||
];
|
||||
\IPS\Api\Webhook::fire( 'content_marked_solved', $payload );
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -182,12 +189,30 @@ trait Solvable
|
||||
{
|
||||
return ( ( $this->containerAllowsMemberSolvable() OR $this->containerAllowsSolvable() ) and $this->mapped('solved_comment_id') );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Is this a non-admin/mod but can solve this item?
|
||||
*
|
||||
* @param \IPS\Member|null $member The member (null for currently logged in member)
|
||||
* @return boolean
|
||||
*/
|
||||
public function isNotModeratorButCanSolve( \IPS\Member $member = NULL ): bool
|
||||
{
|
||||
$member = $member ?: \IPS\Member::loggedIn();
|
||||
|
||||
if ( $this->canSolve( $member ) and $member === $this->author() and $this->containerAllowsMemberSolvable() and ! $member->modPermissions() )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can user solve this item?
|
||||
*
|
||||
* @param \IPS\Member $member The member (null for currently logged in member)
|
||||
* @return bool
|
||||
* @param \IPS\Member|null $member The member (null for currently logged in member)
|
||||
* @return bool
|
||||
*/
|
||||
public function canSolve( \IPS\Member $member = NULL )
|
||||
{
|
||||
@@ -200,7 +225,7 @@ trait Solvable
|
||||
|
||||
if ( $this->containerAllowsSolvable() )
|
||||
{
|
||||
if ( $member == $this->author() and $this->containerAllowsMemberSolvable() )
|
||||
if ( $member === $this->author() and $this->containerAllowsMemberSolvable() )
|
||||
{
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
@@ -116,6 +116,58 @@ trait Statistics
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Most Recent Participans
|
||||
*
|
||||
* @param string|NULL $name If we are looking for a specific user.
|
||||
* @param int $limit The amount of results to return.
|
||||
* @return \IPS\Patterns\ActiveRecordIterator
|
||||
*/
|
||||
public function mostRecent( ?string $name = NULL, int $limit = 10 )
|
||||
{
|
||||
$commentClass = static::$commentClass;
|
||||
|
||||
if ( !isset( $commentClass::$databaseColumnMap['author'] ) )
|
||||
{
|
||||
throw new \BadMethodCallException;
|
||||
}
|
||||
|
||||
if ( $name )
|
||||
{
|
||||
$cacheKey = 'mostRecent_' . $limit . '_' . $name;
|
||||
}
|
||||
else
|
||||
{
|
||||
$cacheKey = 'mostRecent_' . $limit;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$members = $this->_getCached( $cacheKey );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
/* Get the ten most recent posters in this content that match input. */
|
||||
$where = $this->_getVisibleWhere();
|
||||
if ( $name )
|
||||
{
|
||||
$subQuery = \IPS\Db::i()->select( 'core_members.member_id', 'core_members', \IPS\Db::i()->like( 'core_members.name', $name ) );
|
||||
$where[] = [ $commentClass::$databaseTable . '.' . $commentClass::$databaseColumnMap['author'] . ' IN(?)', $subQuery ];
|
||||
}
|
||||
|
||||
$members = iterator_to_array( \IPS\Db::i()->select(
|
||||
$commentClass::$databaseColumnMap['author'],
|
||||
$commentClass::$databaseTable,
|
||||
$where,
|
||||
NULL,
|
||||
$limit ) );
|
||||
|
||||
$this->_storeCached( $cacheKey, $members );
|
||||
}
|
||||
|
||||
return new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members', array( \IPS\Db::i()->in( 'member_id', $members ) ) ), 'IPS\Member' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Members with most posts
|
||||
|
||||
@@ -182,7 +182,17 @@ abstract class _Widget extends \IPS\Widget\PermissionCache
|
||||
/* Rating */
|
||||
if ( \in_array( 'IPS\Content\Ratings', class_implements( $class ) ) and isset( $class::$databaseColumnMap['rating_average'] ) )
|
||||
{
|
||||
$return['rating'] = new \IPS\Helpers\Form\Rating( 'widget_feed_min_rating', isset( $this->configuration['widget_feed_min_rating'] ) ? $this->configuration['widget_feed_min_rating'] : 0, FALSE, array() );
|
||||
$ratingOptions = array(
|
||||
0 => 'any'
|
||||
);
|
||||
for( $i=1; $i<=\IPS\Settings::i()->reviews_rating_out_of; $i++ )
|
||||
{
|
||||
$ratingOptions[$i] = $i;
|
||||
}
|
||||
|
||||
$return['rating'] = new \IPS\Helpers\Form\Select( 'widget_feed_min_rating', isset( $this->configuration['widget_feed_min_rating'] ) ? $this->configuration['widget_feed_min_rating'] : 0, FALSE, array(
|
||||
'options' => $ratingOptions
|
||||
) );
|
||||
}
|
||||
|
||||
/* Number to show */
|
||||
|
||||
@@ -405,6 +405,12 @@ abstract class _WidgetComment extends \IPS\Widget\PermissionCache
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['rating_average'] . '>?', (int) $this->configuration['widget_feed_item_min_rating'] );
|
||||
}
|
||||
|
||||
/* Future */
|
||||
if ( \in_array( 'IPS\Content\FuturePublishing', class_implements( $itemClass ) ) )
|
||||
{
|
||||
$where['item'][] = array( $itemClass::$databasePrefix . $itemClass::$databaseColumnMap['is_future_entry'] . '=0' );
|
||||
}
|
||||
|
||||
/* Start date */
|
||||
if ( isset( $this->configuration['widget_feed_comment_date'] ) and isset( $this->configuration['widget_feed_comment_date'] ) and $this->configuration['widget_feed_comment_date'] > 0 )
|
||||
|
||||
@@ -588,7 +588,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
$oldId = $this->id;
|
||||
$oldColumn = isset( $this->column ) ? $this->column : "field_{$oldId}";
|
||||
parent::__clone();
|
||||
$column = isset( $this->column ) ? $this->column : "field_{$this->id}";
|
||||
$column = "field_{$this->id}";
|
||||
|
||||
if ( isset( static::$contentDatabaseTable ) )
|
||||
{
|
||||
@@ -899,7 +899,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
if( $this->type == 'Select' )
|
||||
{
|
||||
$requiredColumn = ( isset( $this::$databaseColumnMap['not_null'] ) and $this::$databaseColumnMap['not_null'] ) ? $this::$databaseColumnMap['not_null'] : NULL;
|
||||
if ( !$this->$requiredColumn AND !$this->multiple )
|
||||
if ( !$this->$requiredColumn AND !$this->multiple AND \is_array( $options ) )
|
||||
{
|
||||
array_unshift( $options, '' );
|
||||
}
|
||||
@@ -1016,7 +1016,7 @@ abstract class _CustomField extends \IPS\Node\Model
|
||||
if( $this->type == 'Select' )
|
||||
{
|
||||
$requiredColumn = ( isset( $this::$databaseColumnMap['not_null'] ) and $this::$databaseColumnMap['not_null'] ) ? $this::$databaseColumnMap['not_null'] : NULL;
|
||||
if ( !$this->$requiredColumn AND !$this->multiple )
|
||||
if ( !$this->$requiredColumn AND !$this->multiple AND \is_array( $options ) )
|
||||
{
|
||||
array_unshift( $options, '' );
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ class _Select implements \Iterator, \Countable
|
||||
{
|
||||
$query .= $this->db->prefix ? " `{$this->db->prefix}{$table}` AS `{$table}`" : " `{$table}`";
|
||||
}
|
||||
|
||||
|
||||
if ( $on )
|
||||
{
|
||||
if ( $using )
|
||||
|
||||
@@ -239,7 +239,7 @@ class _Developer
|
||||
throw $e;
|
||||
}
|
||||
/* If the error isn't important we should ignore it */
|
||||
else if( !\in_array( $e->getCode(), array( 1007, 1008, 1050, 1060, 1061, 1062, 1091 ) ) )
|
||||
else if( !\in_array( $e->getCode(), array( 1007, 1008, 1050, 1051, 1060, 1061, 1062, 1091 ) ) )
|
||||
{
|
||||
throw $e;
|
||||
}
|
||||
@@ -295,39 +295,6 @@ class _Developer
|
||||
{
|
||||
$queries = json_decode( file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$latestVersion}/queries.json" ), TRUE );
|
||||
}
|
||||
|
||||
/* Sync latest features */
|
||||
if ( \file_exists( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$latestVersion}/whatsnew.json" ) )
|
||||
{
|
||||
$data = json_decode( \file_get_contents( \IPS\ROOT_PATH . "/applications/{$this->app->directory}/setup/upg_{$latestVersion}/whatsnew.json" ), TRUE );
|
||||
if ( \count( $data ) )
|
||||
{
|
||||
foreach( $data AS $uid => $row )
|
||||
{
|
||||
try
|
||||
{
|
||||
$feature = \IPS\Application\Feature::load( $uid, 'feature_unique_key' );
|
||||
}
|
||||
catch( \OutOfRangeException $e )
|
||||
{
|
||||
$feature = new \IPS\Application\Feature;
|
||||
}
|
||||
|
||||
$feature->title = $row['feature_title'];
|
||||
$feature->text = $row['feature_text'];
|
||||
$feature->image = $row['feature_image'];
|
||||
$feature->url = $row['feature_url'];
|
||||
$feature->version_added = $row['feature_version_added'];
|
||||
$feature->app = $row['feature_app'];
|
||||
$feature->unique_key = $row['feature_unique_key'];
|
||||
$feature->save();
|
||||
|
||||
\IPS\Application\Feature::markRead( \IPS\Member::loggedIn(), $feature );
|
||||
}
|
||||
|
||||
$updated = TRUE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Check for missing tables or columns */
|
||||
|
||||
@@ -96,14 +96,9 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'core.css', 'core', 'admin' ) );
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'front' ) );
|
||||
\IPS\Output::i()->cssFiles = array_merge( \IPS\Output::i()->cssFiles, \IPS\Theme::i()->css( 'responsive.css', 'core', 'admin' ) );
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js('admin_newFeatures.js', 'core') );
|
||||
|
||||
/* JS */
|
||||
\IPS\Output::i()->globalControllers[] = 'core.admin.core.app';
|
||||
if ( \IPS\Member::loggedIn()->getFeatureHighlights( FALSE ) )
|
||||
{
|
||||
\IPS\Output::i()->globalControllers[] = 'core.admin.core.newFeatures';
|
||||
}
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'admin.js' ) );
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-ui.js', 'core', 'interface' ) );
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'jquery/jquery-touchpunch.js', 'core', 'interface' ) );
|
||||
@@ -529,6 +524,15 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
*/
|
||||
public function acpMenuCustom( $url ): ?string
|
||||
{
|
||||
if ( !\IPS\Application::appIsEnabled( 'cloud' ) AND $url == 'app=core&module=smartcommunity&controller=cloud' )
|
||||
{
|
||||
$url = \IPS\Http\Url::internal( $url, 'admin' );
|
||||
$title = \IPS\Member::loggedIn()->language()->addToStack( "menu__core_smartcommunity_features" );
|
||||
return <<<EOF
|
||||
<a href='{$url}' style="opacity:30%">{$title} <i class="fa fa-lock"></i></a>
|
||||
EOF;
|
||||
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -584,7 +588,7 @@ class _Admin extends \IPS\Dispatcher\Standard
|
||||
}
|
||||
|
||||
/* Don't show if installation was less than a month ago */
|
||||
if ( time() < \IPS\Settings::i()->board_start + ( 86400 * 30 ) )
|
||||
if ( time() < ( (int) \IPS\Settings::i()->board_start + ( 86400 * 30 ) ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ class _Api extends \IPS\Dispatcher
|
||||
* @brief Language
|
||||
*/
|
||||
public $language = NULL;
|
||||
|
||||
/**
|
||||
* Can the Response be cached?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public bool $cacheResponse = TRUE;
|
||||
|
||||
/**
|
||||
* Init
|
||||
@@ -94,10 +101,17 @@ class _Api extends \IPS\Dispatcher
|
||||
|
||||
/* Set other data */
|
||||
$this->_setLanguage();
|
||||
|
||||
/* We don't want to cache any output for Zapier Requests */
|
||||
if( \IPS\Request::i()->isZapier() )
|
||||
{
|
||||
$this->cacheResponse = FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
catch ( \IPS\Api\Exception $e )
|
||||
{
|
||||
/* Build resonse */
|
||||
/* Build response */
|
||||
$response = json_encode( array( 'errorCode' => $e->exceptionCode, 'errorMessage' => $e->getMessage() ), JSON_PRETTY_PRINT );
|
||||
|
||||
/* Do we need to log this? */
|
||||
@@ -408,7 +422,7 @@ class _Api extends \IPS\Dispatcher
|
||||
*/
|
||||
protected function _respond( $response, $httpResponseCode, $oauthError=NULL, $log=FALSE )
|
||||
{
|
||||
$headers = ( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !$this->rawAccessToken ) ? \IPS\Output::getCacheHeaders( time(), 60 ) : \IPS\Output::getNoCacheHeaders();
|
||||
$headers = $this->canBeCached() ? \IPS\Output::getCacheHeaders( time(), 60 ) : \IPS\Output::getNoCacheHeaders();
|
||||
|
||||
if ( $this->rawAccessToken and $oauthError )
|
||||
{
|
||||
@@ -422,6 +436,16 @@ class _Api extends \IPS\Dispatcher
|
||||
|
||||
\IPS\Output::i()->sendOutput( $response, $httpResponseCode, 'application/json', $headers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Can this response be cached?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function canBeCached() : bool
|
||||
{
|
||||
return $this->cacheResponse AND mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !$this->rawAccessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destructor
|
||||
|
||||
+13
-87
@@ -35,9 +35,6 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
*/
|
||||
public function init()
|
||||
{
|
||||
/* Get cached page if available */
|
||||
$this->checkCached();
|
||||
|
||||
/* Set up in progress? */
|
||||
if ( isset( \IPS\Settings::i()->setup_in_progress ) AND \IPS\Settings::i()->setup_in_progress )
|
||||
{
|
||||
@@ -81,6 +78,9 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
/* Base JS */
|
||||
static::baseJs();
|
||||
|
||||
/* Perform some legacy URL conversions - Need to do this before checking furl in case app name has changed */
|
||||
static::convertLegacyParameters();
|
||||
|
||||
/* Check friendly URL and whether it is correct */
|
||||
try
|
||||
{
|
||||
@@ -99,9 +99,6 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'app.js' ) );
|
||||
\IPS\Output::i()->error( 'requested_route_404', '1S160/2', 404, '' );
|
||||
}
|
||||
|
||||
/* Perform some legacy URL conversions*/
|
||||
static::convertLegacyParameters();
|
||||
|
||||
/* Run global init */
|
||||
try
|
||||
@@ -245,7 +242,7 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
{
|
||||
\IPS\Output::i()->breadcrumb['module'] = array( \IPS\Http\Url::internal( 'app=' . $this->application->directory . '&module=' . $this->module->key . '&controller=' . $this->module->default_controller, 'front', array_key_exists( $this->module->key, \IPS\Http\Url::furlDefinition() ) ? $this->module->key : NULL ), $this->module->_title );
|
||||
}
|
||||
|
||||
|
||||
/* Figure out what the global search is */
|
||||
foreach ( $this->application->extensions( 'core', 'ContentRouter' ) as $object )
|
||||
{
|
||||
@@ -257,7 +254,12 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
if ( is_subclass_of( $class, 'IPS\Content\Searchable' ) and $class::includeInSiteSearch() and $this->module->key == $class::$module )
|
||||
{
|
||||
$type = mb_strtolower( str_replace( '\\', '_', mb_substr( array_pop( $classes ), 4 ) ) );
|
||||
\IPS\Output::i()->defaultSearchOption = array( $type, "{$type}_pl" );
|
||||
|
||||
/* If not the default app, set default search option to current app */
|
||||
if ( ! mb_stristr( $type, $defaultApplication ) )
|
||||
{
|
||||
\IPS\Output::i()->defaultSearchOption = array( $type, "{$type}_pl" );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -388,75 +390,6 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
return !\IPS\Member::loggedIn()->group['g_view_board'] and !$this->application->allowGuestAccess( $this->module, $this->controller, \IPS\Request::i()->do );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check cache for this page
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function checkCached()
|
||||
{
|
||||
/* If this is a guest and there's a full cached page, we can serve that */
|
||||
if( mb_strtolower( $_SERVER['REQUEST_METHOD'] ) == 'get' AND !isset( \IPS\Request::i()->csrfKey ) AND !isset( \IPS\Request::i()->_mfaLogin ) AND !\IPS\Session\Front::loggedIn() AND !isset( \IPS\Request::i()->cookie['noCache'] ) AND \IPS\CACHE_PAGE_TIMEOUT )
|
||||
{
|
||||
/* Check whether a 304 Not modified response is appropriate */
|
||||
try
|
||||
{
|
||||
if( !empty( $_SERVER['HTTP_IF_MODIFIED_SINCE'] ) AND ( new \IPS\DateTime( \rtrim( $_SERVER['HTTP_IF_MODIFIED_SINCE'], ';' ) ) )->getTimestamp() > ( time() - \IPS\CACHE_PAGE_TIMEOUT ) )
|
||||
{
|
||||
/* This is intentionally using PHP methods to avoid database connections */
|
||||
$this->destruct = false;
|
||||
header('HTTP/1.1 304 Not Modified' );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
catch ( \Exception $e ) { }
|
||||
|
||||
/* Only check the output cache if it is enabled */
|
||||
if( empty( \IPS\OUTPUT_CACHE_METHOD ) OR ( !empty( \IPS\OUTPUT_CACHE_METHOD ) AND \IPS\OUTPUT_CACHE_METHOD == 'None' ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* Which language? */
|
||||
if ( isset( \IPS\Request::i()->cookie['language'] ) )
|
||||
{
|
||||
$language = \IPS\Request::i()->cookie['language'];
|
||||
}
|
||||
else
|
||||
{
|
||||
/* HTTP_ACCEPT_LANGUAGE is only available via http, thus trying to enable FURLs fails as we send out a socket request to test .htaccess */
|
||||
$language = ( isset( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) ) ? \IPS\Lang::autoDetectLanguage( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ) : NULL;
|
||||
|
||||
if ( $language === NULL )
|
||||
{
|
||||
$language = \IPS\Lang::defaultLanguage();
|
||||
}
|
||||
}
|
||||
|
||||
/* Which theme? */
|
||||
$theme = isset( \IPS\Request::i()->cookie['theme'] ) ? \IPS\Request::i()->cookie['theme'] : '';
|
||||
|
||||
/* Make sure a session is initiated */
|
||||
\IPS\Session\Front::i();
|
||||
|
||||
/* Get cache */
|
||||
try
|
||||
{
|
||||
$cache = \IPS\Output\Cache::i()->get( 'page_' . md5( (string) \IPS\Request::i()->url() ) . "_{$language}_{$theme}", TRUE );
|
||||
$sessionId = isset( \IPS\Request::i()->cookie['IPSSessionFront'] ) ? \IPS\Request::i()->cookie['IPSSessionFront'] : \IPS\Session::i()->id;
|
||||
|
||||
/* RSS feeds can be shown cached, all other pages contain a cache buster once logged in */
|
||||
$cache['meta']['httpHeaders'] += \IPS\Output::getCacheHeaders( $cache['meta']['lastUpdated'], \IPS\CACHE_PAGE_TIMEOUT );
|
||||
|
||||
$this->_setReferralCookie();
|
||||
|
||||
\IPS\Output::i()->sendOutput( str_replace( '{{csrfKey}}', md5( \IPS\SUITE_UNIQUE_KEY . '&& 0&' . $sessionId ), $cache['output'] ), $cache['meta']['code'], $cache['meta']['contentType'], $cache['meta']['httpHeaders'], FALSE, TRUE );
|
||||
|
||||
}
|
||||
catch ( \OutOfRangeException $e ) {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform some legacy URL parameter conversions
|
||||
*
|
||||
@@ -738,7 +671,7 @@ class _Front extends \IPS\Dispatcher\Standard
|
||||
public static function robotsTxtRules(): string
|
||||
{
|
||||
$path = str_replace( '//', '/', '/' . trim( str_replace( 'robots.txt', '', \IPS\Http\Url::createFromString( \IPS\Http\Url::baseUrl() )->data[ \IPS\Http\Url::COMPONENT_PATH ] ), '/' ) . '/' );
|
||||
|
||||
$sitemapUrl = ( new \IPS\Sitemap )->sitemapUrl;
|
||||
$content = <<<FILE
|
||||
# Rules for Invision Community (https://invisioncommunity.com)
|
||||
User-Agent: *
|
||||
@@ -751,6 +684,7 @@ Disallow: {$path}online/
|
||||
Disallow: {$path}discover/
|
||||
Disallow: {$path}leaderboard/
|
||||
Disallow: {$path}search/
|
||||
Disallow: {$path}tags/
|
||||
Disallow: {$path}*?advancedSearchForm=
|
||||
Disallow: {$path}register/
|
||||
Disallow: {$path}lostpassword/
|
||||
@@ -767,17 +701,9 @@ Disallow: {$path}*?forumId*
|
||||
# Block profile pages as these have little unique value, consume a lot of crawl time and contain hundreds of 301 links
|
||||
Disallow: {$path}profile/
|
||||
|
||||
FILE;
|
||||
|
||||
if ( \IPS\Settings::i()->sitemap_url )
|
||||
{
|
||||
$siteMapUrl = ( \IPS\Settings::i()->sitemap_url ) ? rtrim( \IPS\Settings::i()->sitemap_url, '/' ) : rtrim( \IPS\Settings::i()->base_url, '/' ) . '/sitemap.php';
|
||||
$content .= <<<FILE
|
||||
|
||||
# Sitemap URL
|
||||
Sitemap: {$siteMapUrl}
|
||||
Sitemap: {$sitemapUrl}
|
||||
FILE;
|
||||
}
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
@@ -120,23 +120,9 @@ class _GraphQL extends Api
|
||||
$variables = $json['variables'];
|
||||
}
|
||||
}
|
||||
|
||||
/* Execute! */
|
||||
$result = \GraphQL\GraphQL::executeQuery(
|
||||
new \GraphQL\Type\Schema([
|
||||
'query' => \IPS\Api\GraphQL\TypeRegistry::query(),
|
||||
'mutation' => \IPS\Api\GraphQL\TypeRegistry::mutation()
|
||||
]),
|
||||
$query,
|
||||
NULL, // $rootValue
|
||||
[
|
||||
'member' => \IPS\Member::loggedIn()
|
||||
], // $context
|
||||
$variables
|
||||
);
|
||||
|
||||
/* Convert result into JSON and send */
|
||||
$output = $result->toArray( ( \IPS\IN_DEV OR \IPS\DEBUG_GRAPHQL ) ? \GraphQL\Error\DebugFlag::INCLUDE_DEBUG_MESSAGE | \GraphQL\Error\DebugFlag::INCLUDE_TRACE : false );
|
||||
|
||||
$output = \IPS\Api\GraphQL::execute( $query, $variables );
|
||||
|
||||
\IPS\Member::loggedIn()->language()->parseOutputForDisplay( $output );
|
||||
$this->_respond( json_encode( $output, JSON_PRETTY_PRINT ), 200 );
|
||||
|
||||
|
||||
@@ -484,12 +484,19 @@ abstract class _Email
|
||||
*/
|
||||
protected static function _parseElementForClickTracking( $element, $templateKey=NULL )
|
||||
{
|
||||
/* Ignore any links that are mailto links */
|
||||
$href = (string)$element->getAttribute( 'href' );
|
||||
if( \str_starts_with( $href, 'mailto' ) )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* If we are working with mergeAndSend() this may not be a full URL (which may be passed as a user param)
|
||||
so we need to ignore any exceptions and simply not adjust the link, which should be done manually instead */
|
||||
try
|
||||
{
|
||||
$url = \IPS\Http\Url::internal( "app=core&module=system&controller=redirect", 'front' )->setQueryString( array(
|
||||
'url' => (string) $element->getAttribute('href'),
|
||||
'url' => $href,
|
||||
'resource' => ( \IPS\Request::i()->resource ) ? 1 : NULL,
|
||||
'key' => hash_hmac( "sha256", (string) $element->getAttribute('href'), \IPS\Settings::i()->site_secret_key . 'r' ),
|
||||
'email' => 1,
|
||||
@@ -760,7 +767,7 @@ abstract class _Email
|
||||
'mlog_subject' => $subject,
|
||||
'mlog_content' => $html ?: $plaintext,
|
||||
'mlog_resend_data' => json_encode( array( 'type' => $this->type, 'headers' => $this->_compileHeaders( $subject, $to, $cc, $bcc, $fromEmail, $fromName, $additionalHeaders, $boundary ), 'body' => array( 'html' => $html, 'plain' => $plaintext ), 'boundary' => $boundary ) ),
|
||||
'mlog_msg' => json_encode( array( 'message' => $e->getMessage(), 'details' => $e->extraDetails ) ),
|
||||
'mlog_msg' => json_encode( array( 'message' => $e->messageKey, 'details' => $e->extraDetails ) ),
|
||||
'mlog_smtp_log' => $this->getLog(),
|
||||
'mlog_notification_sent' => FALSE
|
||||
) );
|
||||
|
||||
@@ -713,19 +713,19 @@ class _Email
|
||||
switch ( $row['rule_criteria_field'] )
|
||||
{
|
||||
case 'to':
|
||||
$analyse = implode( ',', $this->to );
|
||||
$analyze = implode( ',', $this->to );
|
||||
break;
|
||||
|
||||
case 'from':
|
||||
$analyse = $this->from;
|
||||
$analyze = $this->from;
|
||||
break;
|
||||
|
||||
case 'sbjt':
|
||||
$analyse = $this->subject;
|
||||
$analyze = $this->subject;
|
||||
break;
|
||||
|
||||
case 'body':
|
||||
$analyse = $this->message;
|
||||
$analyze = $this->message;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -733,22 +733,22 @@ class _Email
|
||||
switch ( $row['rule_criteria_type'] )
|
||||
{
|
||||
case 'ctns':
|
||||
$match = (bool) ( mb_strpos( $analyse, $row['rule_criteria_value'] ) !== FALSE );
|
||||
$match = (bool) ( mb_strpos( $analyze, $row['rule_criteria_value'] ) !== FALSE );
|
||||
break;
|
||||
|
||||
case 'eqls':
|
||||
if ( mb_strpos( $analyse, ',' ) !== FALSE )
|
||||
if ( mb_strpos( $analyze, ',' ) !== FALSE )
|
||||
{
|
||||
$match = (bool) \in_array( $analyse, explode( ',', $analyse ) );
|
||||
$match = (bool) \in_array( $analyze, explode( ',', $analyze ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$match = (bool) ( $analyse == $row['rule_criteria_value'] );
|
||||
$match = (bool) ( $analyze == $row['rule_criteria_value'] );
|
||||
}
|
||||
break;
|
||||
|
||||
case 'regx':
|
||||
$match = (bool) ( preg_match( "/{$row['rule_criteria_value']}/", $analyse ) == 1 );
|
||||
$match = (bool) ( preg_match( "/{$row['rule_criteria_value']}/", $analyze ) == 1 );
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class _Exception extends \RuntimeException
|
||||
* @brief Extra details for log
|
||||
*/
|
||||
public $extraDetails = array();
|
||||
public $messageKey = NULL;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -40,6 +41,7 @@ class _Exception extends \RuntimeException
|
||||
{
|
||||
/* Store these for the extraLogData() method */
|
||||
$this->extraDetails = $extra;
|
||||
$this->messageKey = $message;
|
||||
|
||||
switch ( $code )
|
||||
{
|
||||
@@ -47,7 +49,7 @@ class _Exception extends \RuntimeException
|
||||
$message = \IPS\Member::loggedIn()->language()->addToStack( $message, FALSE, array( 'sprintf' => $extra ) );
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
return parent::__construct( $message, $code, $previous );
|
||||
}
|
||||
}
|
||||
@@ -60,7 +60,7 @@ class _Exception extends \RuntimeException
|
||||
/**
|
||||
* @brief File path
|
||||
*/
|
||||
public $file = NULL;
|
||||
public $filepath = NULL;
|
||||
|
||||
/**
|
||||
* @brief Original filename (used for friendlier errors)
|
||||
@@ -90,7 +90,7 @@ class _Exception extends \RuntimeException
|
||||
public function __construct( $file, $error, $originalFilename = NULL, $errorMessage = NULL, $extraLog = NULL )
|
||||
{
|
||||
/* Store the file */
|
||||
$this->file = $file;
|
||||
$this->filepath = $file;
|
||||
$this->originalFilename = $originalFilename;
|
||||
|
||||
/* Store additional debug info */
|
||||
|
||||
@@ -51,7 +51,7 @@ abstract class _File
|
||||
/**
|
||||
* @brief File extensions considered safe. Not an exhaustive list but these are the files we're most interested in being recognised.
|
||||
*/
|
||||
public static $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v', 'webp', 'm4a' );
|
||||
public static $safeFileExtensions = array( 'js', 'css', 'txt', 'ico', 'gif', 'jpg', 'jpe', 'jpeg', 'png', 'mp4', '3gp', 'mov', 'ogg', 'ogv', 'mp3', 'mpg', 'mpeg', 'ico', 'flv', 'webm', 'wmv', 'avi', 'm4v', 'webp', 'm4a', 'wav' );
|
||||
|
||||
/**
|
||||
* @brief File extensions for HTML5 compatible videos
|
||||
|
||||
@@ -22,6 +22,87 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Google extends \IPS\GeoLocation\GeoCoder
|
||||
{
|
||||
/**
|
||||
* Get by location string
|
||||
*
|
||||
* @param string $location
|
||||
* @return \IPS\GeoLocation
|
||||
* @throws \BadFunctionCallException
|
||||
*/
|
||||
public static function decodeLocation( string $location )
|
||||
{
|
||||
if ( \IPS\Settings::i()->googlemaps AND \IPS\Settings::i()->google_maps_api_key )
|
||||
{
|
||||
$data = \IPS\Http\Url::external( "https://maps.googleapis.com/maps/api/geocode/json" )->setQueryString( array(
|
||||
'address' => $location,
|
||||
'sensor' => 'false',
|
||||
'key' => \IPS\Settings::i()->google_maps_api_key_secret ?: \IPS\Settings::i()->google_maps_api_key
|
||||
) )->request()->get()->decodeJson();
|
||||
|
||||
$obj = new \IPS\GeoLocation;
|
||||
|
||||
$_address = '';
|
||||
|
||||
/* Make sure the response from Google is valid */
|
||||
if( isset( $data['results'] ) AND \is_array( $data['results'] ) AND \count( $data['results'] ) )
|
||||
{
|
||||
if( isset( $data['results'][0]['geometry'] ) AND $data['results'][0]['geometry']['location'] )
|
||||
{
|
||||
$obj->lat = $data['results'][0]['geometry']['location']['lat'];
|
||||
$obj->long = $data['results'][0]['geometry']['location']['lng'];
|
||||
}
|
||||
|
||||
if( isset( $data['results'][0]['name'] ) )
|
||||
{
|
||||
$obj->placeName = $data['results'][0]['name'];
|
||||
}
|
||||
|
||||
foreach( $data['results'][0]['address_components'] as $component )
|
||||
{
|
||||
if( $component['types'][0] == 'street_number' )
|
||||
{
|
||||
$_address = $component['long_name'];
|
||||
}
|
||||
elseif( $component['types'][0] == 'route' )
|
||||
{
|
||||
$_address .= " " . $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'postal_code' )
|
||||
{
|
||||
$obj->postalCode = $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'country' )
|
||||
{
|
||||
$obj->country = $component['short_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'administrative_area_level_1' )
|
||||
{
|
||||
$obj->region = $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'locality' )
|
||||
{
|
||||
$obj->city = $component['long_name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( $_address )
|
||||
{
|
||||
$obj->addressLines = array( $_address );
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \BadFunctionCallException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get by latitude and longitude
|
||||
*
|
||||
|
||||
@@ -22,6 +22,82 @@ if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
*/
|
||||
class _Mapbox extends \IPS\GeoLocation\GeoCoder
|
||||
{
|
||||
/**
|
||||
* Get by location string
|
||||
*
|
||||
* @param string $location
|
||||
* @return \IPS\GeoLocation
|
||||
* @throws \BadFunctionCallException
|
||||
*/
|
||||
public static function decodeLocation( string $location )
|
||||
{
|
||||
if ( \IPS\Settings::i()->mapbox AND \IPS\Settings::i()->mapbox_api_key )
|
||||
{
|
||||
$data = \IPS\Http\Url::external( "https://api.mapbox.com/geocoding/v5/mapbox.places/{$location}.json" )->setQueryString( array(
|
||||
'access_token' => \IPS\Settings::i()->mapbox_api_key,
|
||||
) )->request()->get()->decodeJson();
|
||||
|
||||
$obj = new \IPS\GeoLocation;
|
||||
|
||||
$_address = '';
|
||||
|
||||
/* Make sure the response from Mapbox is valid */
|
||||
if( isset( $data['results'] ) AND \is_array( $data['results'] ) AND \count( $data['results'] ) )
|
||||
{
|
||||
if( isset( $data['results'][0]['geometry'] ) AND isset( $data['results'][0]['geometry']['coordinates'] ) )
|
||||
{
|
||||
$obj->long = $data['results'][0]['geometry']['coordinates'][0];
|
||||
$obj->lat = $data['results'][0]['geometry']['coordinates'][1];
|
||||
}
|
||||
|
||||
$obj->placeName = $data['results'][0]['place_name'];
|
||||
|
||||
foreach( $data['results'][0]['address_components'] as $component )
|
||||
{
|
||||
if( $component['types'][0] == 'street_number' )
|
||||
{
|
||||
$_address = $component['long_name'];
|
||||
}
|
||||
elseif( $component['types'][0] == 'route' )
|
||||
{
|
||||
$_address .= " " . $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'postal_code' )
|
||||
{
|
||||
$obj->postalCode = $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'country' )
|
||||
{
|
||||
$obj->country = $component['short_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'administrative_area_level_1' )
|
||||
{
|
||||
$obj->region = $component['long_name'];
|
||||
}
|
||||
|
||||
if( $component['types'][0] == 'locality' )
|
||||
{
|
||||
$obj->city = $component['long_name'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if( $_address )
|
||||
{
|
||||
$obj->addressLines = array( $_address );
|
||||
}
|
||||
|
||||
return $obj;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \BadFunctionCallException;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get by latitude and longitude
|
||||
*
|
||||
|
||||
@@ -457,6 +457,11 @@ class _GeoLocation
|
||||
* @brief Postal Code
|
||||
*/
|
||||
public $postalCode;
|
||||
|
||||
/**
|
||||
* @brief Place Name
|
||||
*/
|
||||
public $placeName;
|
||||
|
||||
/**
|
||||
* @brief Map
|
||||
@@ -622,6 +627,17 @@ class _GeoLocation
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Geocode Location
|
||||
*
|
||||
* @param string $input Location search term
|
||||
* @return static
|
||||
*/
|
||||
public static function geocodeLocation( $input = NULL )
|
||||
{
|
||||
return \IPS\GeoLocation\GeoCoder::i()->decodeLocation( $input );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached lookups to prevent duplicate lookups
|
||||
*/
|
||||
|
||||
@@ -50,7 +50,19 @@ class _Google
|
||||
*/
|
||||
public function render( $width, $height, $zoom=NULL, $scale=1, $maptype='roadmap' )
|
||||
{
|
||||
return \IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->staticMap( \IPS\Http\Url::external( 'https://maps.google.com/' )->setQueryString( 'q', $this->_getLocation() ), $this->mapUrl( $width, $height, $zoom, $scale, $maptype ), $this->geolocation->lat, $this->geolocation->long, $width, $height );
|
||||
\IPS\Output::i()->jsFiles = array_merge( \IPS\Output::i()->jsFiles, \IPS\Output::i()->js( 'global_core.js', 'core', 'global' ) );
|
||||
|
||||
return \IPS\Settings::i()->google_maps_static_use_embed ?
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->googleMap( array(
|
||||
'lat' => $this->geolocation->lat,
|
||||
'long' => $this->geolocation->long,
|
||||
'key' => \IPS\Settings::i()->google_maps_api_key,
|
||||
'zoom' => $zoom,
|
||||
'scale' => $scale,
|
||||
'maptype' => $maptype,
|
||||
'width' => $width,
|
||||
'height' => $height )) :
|
||||
\IPS\Theme::i()->getTemplate( 'global', 'core', 'global' )->staticMap( \IPS\Http\Url::external( 'https://maps.google.com/' )->setQueryString( 'q', $this->_getLocation() ), $this->mapUrl( $width, $height, $zoom, $scale, $maptype ), $this->geolocation->lat, $this->geolocation->long, $width, $height );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -56,7 +56,12 @@ abstract class _Dynamic extends \IPS\Helpers\Chart
|
||||
* @brief Title
|
||||
*/
|
||||
public $title;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Description
|
||||
*/
|
||||
public $description;
|
||||
|
||||
/**
|
||||
* @brief Google Chart Options
|
||||
*/
|
||||
|
||||
@@ -37,7 +37,7 @@ abstract class _Controller extends \IPS\Dispatcher\Controller
|
||||
|
||||
$form = new \IPS\Helpers\Form( 'coverPhoto' );
|
||||
$form->class = 'ipsForm_vertical ipsForm_noLabels';
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'cover_photo', NULL, TRUE, array( 'image' => TRUE, 'allowStockPhotos' => TRUE, 'minimize' => FALSE, 'maxFileSize' => ( $photo->maxSize and $photo->maxSize != -1 ) ? $photo->maxSize / 1024 : NULL, 'storageExtension' => $this->_coverPhotoStorageExtension() ) ) );
|
||||
$form->add( new \IPS\Helpers\Form\Upload( 'cover_photo', NULL, TRUE, array( 'image' => [ 'maxWidth' => NULL, 'maxHeight' => NULL ], 'allowStockPhotos' => TRUE, 'minimize' => FALSE, 'maxFileSize' => ( $photo->maxSize and $photo->maxSize != -1 ) ? $photo->maxSize / 1024 : NULL, 'storageExtension' => $this->_coverPhotoStorageExtension() ) ) );
|
||||
if ( $values = $form->values() )
|
||||
{
|
||||
try
|
||||
|
||||
@@ -39,19 +39,8 @@ class _Hcaptcha implements CaptchaInterface
|
||||
*/
|
||||
public function getHtml()
|
||||
{
|
||||
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Display
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function rowHtml()
|
||||
{
|
||||
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js?hl=" . \IPS\Member::loggedIn()->language()->short ;
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey, preg_replace( '/^(.+?)\..*$/', '$1', \IPS\Member::loggedIn()->language()->short ) );
|
||||
\IPS\Output::i()->jsFilesAsync[] = "https://js.hcaptcha.com/1/api.js";
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->hCaptcha( \IPS\Settings::i()->hcaptcha_sitekey );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -46,6 +46,8 @@ class _Editor extends FormAbstract
|
||||
'maxLength' => FALSE, // The maximum length. Note that the content is HTML, so this isn't the maximum number of visible characters, so should only be used for database limits. The database's max allowed packet will override if smaller
|
||||
'editorId' => NULL, // Passed to editorAttachments. Only necessary if the name may be changed
|
||||
'allowAttachments => TRUE, // Should the editor show upload options?
|
||||
'contentClass' => NULL, // If set to a string, will check this class for prioritizing mentions to participants.
|
||||
'contentId' => NULL, // If set, will check this particular item ID for prioritizing mentions to participants.
|
||||
);
|
||||
* @endcode
|
||||
*/
|
||||
@@ -68,6 +70,8 @@ class _Editor extends FormAbstract
|
||||
'allowAttachments' => TRUE,
|
||||
'ipsPlugins' => "ipsautolink,ipsautosave,ipsctrlenter,ipscode,ipscontextmenu,ipsemoticon,ipsimage,ipslink,ipsmentions,ipspage,ipspaste,ipsquote,ipsspoiler,ipsautogrow,ipssource,removeformat",
|
||||
'profanityBlock' => TRUE,
|
||||
'contentClass' => NULL,
|
||||
'contentId' => NULL
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -356,7 +360,7 @@ class _Editor extends FormAbstract
|
||||
|
||||
/* Display */
|
||||
$template = $raw ? 'editorRaw' : 'editor';
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->$template( $this->name, $value, $this->options, $allowed, md5( $this->options['autoSaveKey'] . ':' . session_id() ), $attachmentArea, json_encode( array() ), $this->options['tags'] );
|
||||
return \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->$template( $this->name, $value, $this->options, $allowed, md5( $this->options['autoSaveKey'] . ':' . session_id() ), $attachmentArea, json_encode( array() ), $this->options['tags'], $this->options['contentClass'], $this->options['contentId'] );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -463,7 +467,7 @@ class _Editor extends FormAbstract
|
||||
|
||||
$altText = NULL;
|
||||
|
||||
if ( ! empty( $attachment['attach_labels'] ) )
|
||||
if ( \IPS\Settings::i()->ips_imagescanner_enable_discovery and ! empty( $attachment['attach_labels'] ) )
|
||||
{
|
||||
if ( $this->_altLabelWord === NULL )
|
||||
{
|
||||
|
||||
@@ -110,7 +110,7 @@ class _Number extends FormAbstract
|
||||
/* Display */
|
||||
if ( ! isset( static::$generatedHtml[ $this->_name ] ) )
|
||||
{
|
||||
static::$generatedHtml[ $this->_name ] = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( $this->name, $this->value, $this->required, $this->options['unlimited'], $this->options['range'], $this->options['min'], $this->options['max'], $this->options['step'], $this->options['decimals'], $this->options['unlimitedLang'], $this->options['disabled'], $this->suffix, $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $this->options['valueToggles'] );
|
||||
static::$generatedHtml[ $this->_name ] = \IPS\Theme::i()->getTemplate( 'forms', 'core', 'global' )->number( $this->name, $this->value, $this->required, $this->options['unlimited'], $this->options['range'], $this->options['min'], $this->options['max'], $this->options['step'], $this->options['decimals'], $this->options['unlimitedLang'], $this->options['disabled'], $this->suffix, $this->options['unlimitedToggles'], $this->options['unlimitedToggleOn'], $this->options['valueToggles'], NULL, $this->prefix );
|
||||
}
|
||||
|
||||
/* Because we mess around with suffixes in the Number field, we store the HTML here otherwise when it is regenerated, the suffix is missing */
|
||||
|
||||
@@ -246,7 +246,7 @@ class _Text extends TextArea
|
||||
}
|
||||
$return['prefix'] = \IPS\Request::i()->$freechoicePrefix;
|
||||
}
|
||||
elseif ( isset( \IPS\Request::i()->$firstAsPrefix ) and \IPS\Request::i()->$firstAsPrefix )
|
||||
elseif ( isset( \IPS\Request::i()->$firstAsPrefix ) and \IPS\Request::i()->$firstAsPrefix and !empty( $return ) )
|
||||
{
|
||||
$return = array_merge( array( 'prefix' => array_shift( $return ) ), $return );
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ class _Upload extends FormAbstract
|
||||
/* If this has to be an image, set the allowed file types */
|
||||
if ( isset( $options['image'] ) and !isset( $options['image']['optional'] ) and !isset( $options['allowedFileTypes'] ) )
|
||||
{
|
||||
$options['allowedFileTypes'] = \IPS\Image::$imageExtensions;
|
||||
$options['allowedFileTypes'] = \IPS\Image::supportedExtensions();
|
||||
}
|
||||
|
||||
/* Call parent constructor */
|
||||
@@ -707,7 +707,7 @@ class _Upload extends FormAbstract
|
||||
$ext = mb_strtolower( mb_substr( $file['name'], ( mb_strrpos( $file['name'], '.' ) + 1 ) ) );
|
||||
|
||||
/* Don't allow "XSS" in images */
|
||||
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::$imageExtensions ) )
|
||||
if( \in_array( $ext, \IPS\File::$safeFileExtensions ) AND \in_array( $ext, \IPS\Image::supportedExtensions() ) )
|
||||
{
|
||||
if( \IPS\File::checkXssInFile( file_get_contents( $file['tmp_name'] ) ) )
|
||||
{
|
||||
@@ -731,7 +731,7 @@ class _Upload extends FormAbstract
|
||||
$ext = mb_strtolower( mb_substr( $filename, mb_strrpos( $filename, '.' ) + 1 ) );
|
||||
|
||||
/* Do image-specific stuff */
|
||||
if ( \in_array( $ext, \IPS\Image::$imageExtensions ) )
|
||||
if ( \in_array( $ext, \IPS\Image::supportedExtensions() ) )
|
||||
{
|
||||
/* Resize */
|
||||
$image = NULL;
|
||||
|
||||
@@ -765,14 +765,15 @@ class _Content extends Table
|
||||
if( \in_array( \IPS\Request::i()->modaction, array( 'hide', 'move', 'merge' ) ) )
|
||||
{
|
||||
$options = array();
|
||||
$ids = array_keys( \IPS\Request::i()->moderate );
|
||||
foreach ( $ids as $id )
|
||||
$ids = array();
|
||||
foreach ( \IPS\Request::i()->moderate as $id => $empty )
|
||||
{
|
||||
$item = $class::load( $id );
|
||||
|
||||
$action = 'can' . ucwords( \IPS\Request::i()->modaction );
|
||||
if( $item->$action() )
|
||||
{
|
||||
$ids[] = $id;
|
||||
$options[ $id ] = $item->mapped('title');
|
||||
$descriptions[ $id ] = \IPS\Member::loggedIn()->language()->addToStack( 'byline_merge', FALSE, array( 'htmlsprintf' => array( $item->author()->name, \IPS\DateTime::ts( $item->mapped('date') )->html() ) ) );
|
||||
}
|
||||
@@ -879,7 +880,11 @@ class _Content extends Table
|
||||
{
|
||||
if ( $id != $values['merge_master'] )
|
||||
{
|
||||
$otherItems[] = $class::load( $id );
|
||||
$otherItem = $class::load( $id );
|
||||
if( $otherItem->canMerge() )
|
||||
{
|
||||
$otherItems[] = $otherItem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -949,16 +954,8 @@ class _Content extends Table
|
||||
/* Now update the containers in one go */
|
||||
foreach( $containers as $container )
|
||||
{
|
||||
if ( $container->_comments !== NULL )
|
||||
{
|
||||
$container->setLastComment();
|
||||
}
|
||||
|
||||
if ( $container->_reviews !== NULL )
|
||||
{
|
||||
$container->setLastReview();
|
||||
}
|
||||
|
||||
$container->setLastComment();
|
||||
$container->setLastReview();
|
||||
$container->resetCommentCounts();
|
||||
$container->save();
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
*/
|
||||
|
||||
namespace IPS\Http\Request;
|
||||
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
if ( !\defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
@@ -41,7 +41,7 @@ class _Sockets
|
||||
* @brief Timeout
|
||||
*/
|
||||
protected $timeout = 5;
|
||||
|
||||
|
||||
/**
|
||||
* @brief Headers
|
||||
*/
|
||||
@@ -56,7 +56,7 @@ class _Sockets
|
||||
* @brief Allowed protocols
|
||||
*/
|
||||
protected $allowedProtocols = array();
|
||||
|
||||
|
||||
/**
|
||||
* Contructor
|
||||
*
|
||||
@@ -75,7 +75,7 @@ class _Sockets
|
||||
$this->timeout = $timeout;
|
||||
$this->followRedirects = $followRedirects;
|
||||
$this->allowedProtocols = $allowedProtocols ?: array( 'http', 'https', 'ftp', 'scp', 'sftp', 'ftps' );
|
||||
|
||||
|
||||
/* Set our basic settings */
|
||||
stream_context_set_option( $this->context, array(
|
||||
'http' => array(
|
||||
@@ -103,7 +103,7 @@ class _Sockets
|
||||
$this->setHeaders( array( 'Authorization' => 'Basic ' . base64_encode( "{$username}:{$password}" ) ) );
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Set Headers
|
||||
*
|
||||
@@ -112,10 +112,10 @@ class _Sockets
|
||||
*/
|
||||
public function setHeaders( $headers )
|
||||
{
|
||||
$this->headers = array_merge( $this->headers, $headers );
|
||||
$this->headers = array_merge( $this->headers, $headers );
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Toggle SSL checks
|
||||
*
|
||||
@@ -133,7 +133,7 @@ class _Sockets
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Force TLS
|
||||
*
|
||||
@@ -142,7 +142,7 @@ class _Sockets
|
||||
public function forceTls()
|
||||
{
|
||||
if ( \defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT') )
|
||||
{
|
||||
{
|
||||
stream_context_set_option( $this->context, array(
|
||||
'ssl' => array(
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
|
||||
@@ -150,14 +150,14 @@ class _Sockets
|
||||
) );
|
||||
}
|
||||
elseif ( \defined('STREAM_CRYPTO_METHOD_TLS_CLIENT') )
|
||||
{
|
||||
{
|
||||
stream_context_set_option( $this->context, array(
|
||||
'ssl' => array(
|
||||
'crypto_method' => STREAM_CRYPTO_METHOD_TLS_CLIENT
|
||||
)
|
||||
) );
|
||||
}
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -212,7 +212,7 @@ class _Sockets
|
||||
{
|
||||
$resource = stream_socket_client( $hostname, $errno, $errstr, $this->timeout, \STREAM_CLIENT_CONNECT, $this->context );
|
||||
}
|
||||
/* Catch issues that may arise, such as DNS failure */
|
||||
/* Catch issues that may arise, such as DNS failure */
|
||||
catch( \ErrorException $e )
|
||||
{
|
||||
throw new SocketsException( $e->getMessage(), $e->getCode() );
|
||||
@@ -248,7 +248,7 @@ class _Sockets
|
||||
}
|
||||
|
||||
\fwrite( $resource, $request );
|
||||
|
||||
|
||||
/* Read response */
|
||||
stream_set_timeout( $resource, $this->timeout );
|
||||
$status = stream_get_meta_data( $resource );
|
||||
@@ -259,10 +259,10 @@ class _Sockets
|
||||
$response .= \fgets( $resource, 8192 );
|
||||
$status = stream_get_meta_data( $resource );
|
||||
}
|
||||
|
||||
|
||||
/* Close connection */
|
||||
\fclose( $resource );
|
||||
|
||||
|
||||
/* Log - but because the output can be large, only do this if we explicitly have debug logging enabled */
|
||||
if ( \defined('\IPS\DEBUG_LOG') and \IPS\DEBUG_LOG )
|
||||
{
|
||||
|
||||
+30
-2
@@ -597,11 +597,11 @@ class _Url
|
||||
$requestObj = new \IPS\Http\Request\Curl( $this, $timeout, $httpVersion, $followRedirects, $allowedProtocols );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Fallback to Sockets if we can't use cURL */
|
||||
if( !isset( $requestObj ) )
|
||||
{
|
||||
$requestObj = new \IPS\Http\Request\Sockets( $this, $timeout, $httpVersion, $followRedirects, $allowedProtocols );
|
||||
$requestObj = new \IPS\Http\Request\Sockets($this, $timeout, $httpVersion, $followRedirects, $allowedProtocols);
|
||||
}
|
||||
|
||||
/* Set a default user-agent (some services, e.g. spotify, block requests without one but it's good to do so anyway) */
|
||||
@@ -1280,4 +1280,32 @@ class _Url
|
||||
{
|
||||
return \IPS\Http\Url\Friendly::seoTitle( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a partial query string based on any filters or sort options
|
||||
*
|
||||
* @param array $additionalParams
|
||||
* @return self
|
||||
*/
|
||||
public function getSafeUrlFromFilters( array $additionalParams = [] ): self
|
||||
{
|
||||
$extraUrlParams = [];
|
||||
|
||||
foreach( array_merge( ['filter', 'sortby', 'sortdirection'], $additionalParams ) as $param )
|
||||
{
|
||||
if ( isset( \IPS\Request::i()->$param ) )
|
||||
{
|
||||
$extraUrlParams[ $param ] = \IPS\Request::i()->$param;
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $extraUrlParams ) )
|
||||
{
|
||||
return $this->setQueryString( $extraUrlParams );
|
||||
}
|
||||
else
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
}
|
||||
+7
-2
@@ -218,8 +218,10 @@ class _Gd extends \IPS\Image
|
||||
{
|
||||
case 'gif':
|
||||
imagealphablending( $newImage, FALSE );
|
||||
// PHP8 fix, see https://stackoverflow.com/questions/3874533/what-could-cause-a-color-index-out-of-range-error-for-imagecolorsforindex
|
||||
$transindex = imagecolortransparent( $this->image );
|
||||
if( $transindex >= 0 )
|
||||
$palletsize = imagecolorstotal($this->image);
|
||||
if( $transindex >= 0 AND $transindex < $palletsize )
|
||||
{
|
||||
$transcol = @imagecolorsforindex( $this->image, $transindex );
|
||||
$transindex = imagecolorallocatealpha( $newImage, $transcol['red'], $transcol['green'], $transcol['blue'], 127 );
|
||||
@@ -292,8 +294,11 @@ class _Gd extends \IPS\Image
|
||||
{
|
||||
case 'gif':
|
||||
imagealphablending( $newImage, FALSE );
|
||||
|
||||
// PHP8 fix, see https://stackoverflow.com/questions/3874533/what-could-cause-a-color-index-out-of-range-error-for-imagecolorsforindex
|
||||
$transindex = imagecolortransparent( $this->image );
|
||||
if( $transindex >= 0 )
|
||||
$palletsize = imagecolorstotal($this->image);
|
||||
if( $transindex >= 0 AND $transindex < $palletsize )
|
||||
{
|
||||
$transcol = @imagecolorsforindex( $this->image, $transindex );
|
||||
$transindex = imagecolorallocatealpha( $newImage, $transcol['red'], $transcol['green'], $transcol['blue'], 127 );
|
||||
|
||||
@@ -158,6 +158,35 @@ class _Imagemagick extends \IPS\Image
|
||||
$this->setDimensions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resize to maximum
|
||||
*
|
||||
* @param int|NULL $maxWidth Max Width (in pixels) or NULL
|
||||
* @param int|NULL $maxHeight Max Height (in pixels) or NULL
|
||||
* @param bool $retainRatio If TRUE, the image will keep it's current width/height ratio rather than being squashed
|
||||
* @return bool Returns TRUE if it actually resized, or FALSE if it wasn't necessary
|
||||
*/
|
||||
public function resizeToMax( $maxWidth=NULL, $maxHeight=NULL, $retainRatio=TRUE )
|
||||
{
|
||||
/* If we want to strip exif, we need to process this image regardless of size */
|
||||
if( ! \IPS\Settings::i()->imagick_strip_exif )
|
||||
{
|
||||
return parent::resizeToMax( $maxWidth, $maxHeight, $retainRatio );
|
||||
}
|
||||
|
||||
/* If the image is smaller, let's "resize" it keeping the same dimensions, so it runs via the __toString method */
|
||||
if( ( $maxWidth == NULL or $this->width < $maxWidth ) and ( $maxHeight == NULL or $this->height < $maxHeight ) )
|
||||
{
|
||||
$this->resize( $this->width, $this->height );
|
||||
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return parent::resizeToMax( $maxWidth, $maxHeight, $retainRatio );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crop to a given width and height (will attempt to downsize first)
|
||||
*
|
||||
|
||||
@@ -2001,7 +2001,7 @@ class _Lang extends \IPS\Node\Model
|
||||
}
|
||||
else
|
||||
{
|
||||
$replacement = mb_substr( json_encode( $replacement ), 1, -1 );
|
||||
$replacement = mb_substr( json_encode( $replacement, JSON_UNESCAPED_UNICODE ), 1, -1 );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -828,23 +828,23 @@ abstract class _Handler extends \IPS\Node\Model
|
||||
{
|
||||
$form->addHeader('login_method_basic_settings');
|
||||
$form->add( new \IPS\Helpers\Form\Translatable( 'login_method_name', NULL, TRUE, array( 'app' => 'core', 'key' => ( $this->id ? 'login_method_' . $this->id : NULL ) ) ) );
|
||||
if ( !( $this instanceof \IPS\Login\Handler\Standard ) )
|
||||
{
|
||||
if ( !( $this instanceof \IPS\Login\Handler\Standard ) ) {
|
||||
$self = $this;
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'login_acp', $this->id ? $this->acp : static::$enableAcpLoginByDefault, FALSE, array(), function( $val ) use ( $self )
|
||||
{
|
||||
if ( !$val )
|
||||
{
|
||||
foreach ( \IPS\Login::methods() as $method )
|
||||
{
|
||||
if ( $method != $self and $method->canProcess( \IPS\Member::loggedIn() ) and $method->acp )
|
||||
{
|
||||
$form->add(new \IPS\Helpers\Form\YesNo('login_acp', $this->id ? $this->acp : static::$enableAcpLoginByDefault, FALSE, array(), function ($val) use ($self) {
|
||||
if (!$val) {
|
||||
foreach (\IPS\Login::methods() as $method) {
|
||||
if ($method != $self and $method->canProcess(\IPS\Member::loggedIn()) and $method->acp) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
throw new \DomainException( 'login_handler_cannot_disable_acp' );
|
||||
throw new \DomainException('login_handler_cannot_disable_acp');
|
||||
}
|
||||
} ) );
|
||||
}));
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'login_front', $this->id ? $this->front : static::$enableAcpLoginByDefault, FALSE, array() ) );
|
||||
|
||||
if ( !( $this instanceof \IPS\Login\Handler\Standard ) ) {
|
||||
$form->add( new \IPS\Helpers\Form\Radio( 'login_register', $this->id ? $this->register : TRUE, FALSE, array(
|
||||
'options' => array(
|
||||
1 => 'login_register_enabled',
|
||||
@@ -856,6 +856,8 @@ abstract class _Handler extends \IPS\Node\Model
|
||||
) ) );
|
||||
}
|
||||
|
||||
$form->add( new \IPS\Helpers\Form\YesNo( 'login_front', $this->id ? $this->front : static::$enableAcpLoginByDefault, FALSE, array() ) );
|
||||
|
||||
foreach ( $this->acpForm() as $key => $field )
|
||||
{
|
||||
if ( \is_string( $field ) )
|
||||
|
||||
@@ -47,6 +47,7 @@ abstract class _OAuth1 extends \IPS\Login\Handler
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
'disabled' => 'login_handler_show_in_ucp_disabled'
|
||||
),
|
||||
) );
|
||||
|
||||
|
||||
@@ -402,7 +402,13 @@ class _Twitter extends \IPS\Login\Handler\OAuth1
|
||||
}
|
||||
return $this->_cachedUserData[ $accessToken ];
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief The length of the shortened URLs returned by Twitter's text parser https://developer.twitter.com/en/docs/counting-characters#:~:text=The%20current%20length%20of%20a,count%20towards%20the%20character%20limit.
|
||||
*/
|
||||
protected static $defaultShortenedUrlLength = 23;
|
||||
|
||||
/**
|
||||
* Post something to Twitter
|
||||
*
|
||||
@@ -419,15 +425,33 @@ class _Twitter extends \IPS\Login\Handler\OAuth1
|
||||
}
|
||||
|
||||
$data = array( 'status' => $content );
|
||||
|
||||
|
||||
/* If we have a url, add it to the end prepended by a space */
|
||||
if ( $url !== NULL )
|
||||
{
|
||||
/* Try to refresh if the stored response is more than a day old */
|
||||
if ( !isset( \IPS\Data\Store::i()->twitter_config ) or \IPS\Data\Store::i()->twitter_config['time'] > time() - 86400 )
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/help/configuration.json'), array(), $link['token_access_token'], $link['token_secret'] )->decodeJson();
|
||||
\IPS\Data\Store::i()->twitter_config = array_merge( $response, array( 'time' => time() ) );
|
||||
try
|
||||
{
|
||||
$response = $this->_sendRequest( 'get', \IPS\Http\Url::external('https://api.twitter.com/1.1/help/configuration.json'), array(), $link['token_access_token'], $link['token_secret'] );
|
||||
if ( ( $response->httpResponseCode === 200 ) AND ( $payload = $response->decodeJson() ) AND \is_array( $payload ) )
|
||||
{
|
||||
\IPS\Data\Store::i()->twitter_config = array_merge( $payload, array( 'time' => time() ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new \UnexpectedValueException();
|
||||
}
|
||||
}
|
||||
catch ( \Exception|\UnexpectedValueException $e )
|
||||
{
|
||||
/* We should be fine hard coding to 23 if the deprecated configuration endpoint is no longer online https://developer.twitter.com/en/docs/counting-characters#:~:text=The%20current%20length%20of%20a,count%20towards%20the%20character%20limit. */
|
||||
\IPS\Data\Store::i()->twitter_config['short_url_length'] = static::$defaultShortenedUrlLength;
|
||||
}
|
||||
}
|
||||
$maxUrlLen = \IPS\Data\Store::i()->twitter_config['short_url_length'];
|
||||
|
||||
$maxUrlLen = \IPS\Data\Store::i()->twitter_config['short_url_length'] ?? static::$defaultShortenedUrlLength;
|
||||
$data['status'] = mb_substr( $data['status'], 0, ( 140 - ( $maxUrlLen + 1 ) ) ) . ' ' . $url;
|
||||
}
|
||||
|
||||
|
||||
@@ -444,7 +444,7 @@ class _Custom extends \IPS\Login\Handler\OAuth2
|
||||
}
|
||||
|
||||
/* Check for any errors */
|
||||
if ( static::getValueFromArray( $response, $this->settings['uid_field'] ) === NULL )
|
||||
if ( $response === NULL OR static::getValueFromArray( $response, $this->settings['uid_field'] ) === NULL )
|
||||
{
|
||||
\IPS\Log::log( print_r( $response, TRUE ), 'oauth_custom' );
|
||||
throw new \IPS\Login\Exception( 'generic_error', \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
|
||||
@@ -374,7 +374,16 @@ class _Google extends \IPS\Login\Handler\OAuth2
|
||||
|
||||
if ( isset( $response['error'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error']['errors'][0]['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
if ( isset( $response['error_description'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error_description'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
// Keeping this for backwards compatibility..
|
||||
else if( isset( $response['error']['errors'][0]['message'] ) )
|
||||
{
|
||||
throw new \IPS\Login\Exception( $response['error']['errors'][0]['message'], \IPS\Login\Exception::INTERNAL_ERROR );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->_cachedUserData[ $accessToken ] = $response;
|
||||
|
||||
@@ -73,6 +73,7 @@ abstract class _OAuth2 extends \IPS\Login\Handler
|
||||
'options' => array(
|
||||
'always' => 'login_handler_show_in_ucp_always',
|
||||
'loggedin' => 'login_handler_show_in_ucp_loggedin',
|
||||
'disabled' => 'login_handler_show_in_ucp_disabled'
|
||||
),
|
||||
) );
|
||||
|
||||
|
||||
+20
-8
@@ -148,8 +148,8 @@ class _Login
|
||||
foreach ( static::methods() as $k => $method )
|
||||
{
|
||||
if (
|
||||
$this->type === static::LOGIN_FRONT
|
||||
or $this->type === static::LOGIN_UCP
|
||||
( $this->type === static::LOGIN_FRONT and $method->front )
|
||||
or ( $this->type === static::LOGIN_UCP and $method->front )
|
||||
or ( $this->type === static::LOGIN_ACP and $method->acp )
|
||||
or ( $this->type === static::LOGIN_REGISTRATION_FORM and $method->register )
|
||||
or ( $this->type === static::LOGIN_REAUTHENTICATE and $method->canProcess( $this->reauthenticateAs ) and !( $method instanceof \IPS\Login\LoginAbstract ) )
|
||||
@@ -605,19 +605,31 @@ class _Login
|
||||
{
|
||||
if ( static::$_registrationType === NULL )
|
||||
{
|
||||
static::$_registrationType = \IPS\Settings::i()->allow_reg;
|
||||
|
||||
if ( static::$_registrationType == 1 )
|
||||
/* If registrations are enabled */
|
||||
if ( \IPS\Settings::i()->allow_reg )
|
||||
{
|
||||
static::$_registrationType = \IPS\Settings::i()->quick_register ? 'normal' : 'full';
|
||||
switch( \IPS\Settings::i()->allow_reg )
|
||||
{
|
||||
// just kept this here for legacy reasons, even if we have an upgrade step to change this now
|
||||
case 1 :
|
||||
case 'full':
|
||||
static::$_registrationType ='full';
|
||||
break;
|
||||
default:
|
||||
return \IPS\Settings::i()->allow_reg;
|
||||
}
|
||||
}
|
||||
|
||||
else
|
||||
{
|
||||
static::$_registrationType = 'disabled';
|
||||
}
|
||||
|
||||
if ( \in_array( static::$_registrationType, array( 'normal', 'full' ) ) and !\IPS\Login\Handler::findMethod( 'IPS\Login\Handler\Standard' ) )
|
||||
{
|
||||
static::$_registrationType = 'disabled';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return static::$_registrationType;
|
||||
}
|
||||
|
||||
|
||||
@@ -258,7 +258,7 @@ class _Device extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$member = \IPS\Member::load( $this->member_id );
|
||||
|
||||
if ( \IPS\Settings::i()->new_device_email )
|
||||
if ( \IPS\Settings::i()->new_device_email and $member->members_bitoptions['new_device_email'] )
|
||||
{
|
||||
try
|
||||
{
|
||||
|
||||
@@ -79,7 +79,7 @@ class _Group extends \IPS\Patterns\ActiveRecord
|
||||
'gbw_post_highlight' => 1, // 1 means on, 0 means off
|
||||
'gbw_hide_group' => 2, // Hide this group on the front end in search and similar areas: 1 is on, 0 is off
|
||||
// 4 is deprecated (previously gbw_lock_unlock_own)
|
||||
'gbw_promote' => 8, // Allow users in this group to promote to facebook/twitter
|
||||
'gbw_promote' => 8, // Allow users in this group to promote to twitter
|
||||
'gbw_immune_auto_mod' => 16, // Users in this group can not be auto moderated
|
||||
'gbw_paid_clubs' => 32, // Users in this group can create paid clubs
|
||||
'gbw_can_post_anonymously' => 64, // Users in this group can post anonymously (where allowed)
|
||||
|
||||
+75
-82
@@ -110,16 +110,30 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
'created_externally' => 16384,
|
||||
'is_support_account' => 32768, // Created by ACP support tool
|
||||
'is_anon' => 65536, // Has chosen to log in anonymously
|
||||
'has_new_acp_features' => 131072,
|
||||
// 131072 (has_new_acp_features) is deprecated
|
||||
'password_reset_forced' => 262144, // A password reset has been forced
|
||||
'link_pref_unread' => 524288, // Take me to comments I haven't read
|
||||
'link_pref_last' => 1048576, // Take me to the latest comment
|
||||
'mobile_notifications' => 2097152, // Has the user has enabled push notifications on at least 1 mobile device?
|
||||
'link_pref_first' => 4194304, // Take me to the first comment
|
||||
'datalayer_event_fired' => 8388608,
|
||||
'new_device_email' => 16777216, // Send an email when a new device is used to log in.
|
||||
'no_solved_reenage' => 33554432, // User does not want to get topic re-engagement emails
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Member Changes to skip for the member_edited webhook
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public static $changesToSkipForMemberEditWebhooks = [
|
||||
'last_visit',
|
||||
'last_activity',
|
||||
'profilesync_lastsync',
|
||||
|
||||
];
|
||||
|
||||
/* !Follow */
|
||||
|
||||
@@ -158,7 +172,14 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
/* If we haven't loaded the member yet, or if the session member has changed since we last loaded the member, reload and cache */
|
||||
if( static::$loggedInMember === NULL )
|
||||
{
|
||||
static::$loggedInMember = \IPS\Session::i()->member;
|
||||
if( \IPS\Request::isCliEnvironment() )
|
||||
{
|
||||
static::$loggedInMember = new \IPS\Member;
|
||||
}
|
||||
else
|
||||
{
|
||||
static::$loggedInMember = \IPS\Session::i()->member;
|
||||
}
|
||||
|
||||
if ( isset( $_SESSION['logged_in_as_key'] ) )
|
||||
{
|
||||
@@ -288,6 +309,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
$this->members_bitoptions['view_sigs'] = TRUE;
|
||||
}
|
||||
|
||||
$this->members_bitoptions['new_device_email'] = TRUE;
|
||||
$this->members_bitoptions['show_pm_popup'] = ( \IPS\Settings::i()->notification_prefs_popup ) ? TRUE : FALSE;
|
||||
$this->members_bitoptions['email_notifications_once'] = ( \IPS\Settings::i()->notification_prefs_one_per_view ) ? TRUE : FALSE;
|
||||
|
||||
@@ -398,6 +420,8 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
elseif( !$this->completed AND $this->name AND $this->email )
|
||||
{
|
||||
$this->completed = TRUE;
|
||||
\IPS\Api\Webhook::fire( 'member_registration_complete', $this, $this->webhookFilters() );
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1848,79 +1872,6 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
return $this->acpRestrictions() !== FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Feature highlight cache
|
||||
*/
|
||||
protected $_features = NULL;
|
||||
|
||||
/**
|
||||
* Get unread feature highlights
|
||||
*
|
||||
* @param bool $markRead Mark returned features as read? Useful for debugging.
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function getFeatureHighlights( bool $markRead = TRUE ): ?array
|
||||
{
|
||||
if ( $this->members_bitoptions['is_support_account'] )
|
||||
{
|
||||
/* Support account doesn't need these. */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if ( !$this->members_bitoptions['has_new_acp_features'] )
|
||||
{
|
||||
/* No new features to see */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Compile all unread features */
|
||||
if ( $this->_features === NULL )
|
||||
{
|
||||
$this->_features = array();
|
||||
foreach( new \IPS\Patterns\ActiveRecordIterator( \IPS\Db::i()->select( '*', 'core_members_features', array( \IPS\Db::i()->in( 'feature_id', $this->getFeatureReadIds(), true ) ) ), 'IPS\Application\Feature' ) AS $feature )
|
||||
{
|
||||
$this->_features[] = $feature;
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $this->_features ) )
|
||||
{
|
||||
/* Mark them as read */
|
||||
if ( $markRead === TRUE )
|
||||
{
|
||||
/* Only set the flag if we're actually marking them as read. */
|
||||
$this->members_bitoptions['has_new_acp_features'] = FALSE;
|
||||
$this->save();
|
||||
|
||||
\IPS\Application\Feature::markRead( $this, ...$this->_features );
|
||||
}
|
||||
return $this->_features;
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Even if we aren't marking them as read, there are none anyway, so just reset the flag. */
|
||||
$this->members_bitoptions['has_new_acp_features'] = FALSE;
|
||||
$this->save();
|
||||
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get read feature ID's
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getFeatureReadIds(): array
|
||||
{
|
||||
$return = array();
|
||||
foreach( \IPS\Db::i()->select( 'feature_id', 'core_members_feature_seen', array( "member_id=?", $this->member_id ) ) AS $id )
|
||||
{
|
||||
$return[] = $id;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cache the session data if we pull it for location, etc.
|
||||
@@ -3101,7 +3052,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
/**
|
||||
* Moderator Permissions
|
||||
*
|
||||
* @return array
|
||||
* @return array|false
|
||||
*/
|
||||
public function modPermissions()
|
||||
{
|
||||
@@ -3210,18 +3161,16 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
if ( $this->reportCount === NULL OR $force === TRUE )
|
||||
{
|
||||
if ( \IPS\Member::loggedIn()->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) ) )
|
||||
if ( $this->canAccessModule( \IPS\Application\Module::get( 'core', 'modcp' ) ) )
|
||||
{
|
||||
if( !\IPS\Member::loggedIn()->members_bitoptions['no_report_count'] OR $force === TRUE )
|
||||
if( !$this->members_bitoptions['no_report_count'] OR $force === TRUE )
|
||||
{
|
||||
$where = '( perm_id IN (?) OR perm_id IS NULL ) AND status IN( 1,2 )';
|
||||
$where = [ \IPS\core\Reports\Report::where( $this ), [ 'status IN( 1,2 )' ] ];
|
||||
|
||||
/* fetch only reports for content of enabled applications */
|
||||
$where .= " AND " . \IPS\Db::i()->in( 'class', array_merge( array( 'IPS\core\Messenger\Conversation', 'IPS\core\Messenger\Message' ), array_values( \IPS\Content::routedClasses( FALSE, TRUE ) ) ) );
|
||||
$reportCount = \IPS\Db::i()->select(
|
||||
'COUNT(*)',
|
||||
'core_rc_index',
|
||||
array( $where, \IPS\Db::i()->select( 'perm_id', 'core_permission_index', \IPS\Db::i()->findInSet( 'perm_view', $this->permissionArray() ) . " OR perm_view='*'" ) )
|
||||
$where
|
||||
)->first();
|
||||
|
||||
if( $force === FALSE )
|
||||
@@ -3491,6 +3440,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
* @apiresponse [\IPS\core\Achievements\Rank] rank Rank
|
||||
* @apiresponse int achievements_points Points
|
||||
* @apiresponse bool allowAdminEmails Whether or not this member wants to receive admin emails
|
||||
* @apiresponse bool completed Whether or not the registration is completed
|
||||
*/
|
||||
public function apiOutput( \IPS\Member $authorizedMember = NULL, $otherFields = NULL )
|
||||
{
|
||||
@@ -3592,6 +3542,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
$return['rank'] = $this->rank() ? $this->rank()->apiOutput( $authorizedMember ) : NULL;
|
||||
$return['achievements_points'] = $this->achievements_points;
|
||||
$return['allowAdminEmails'] = (bool) $this->allow_admin_emails;
|
||||
$return['completed'] = (bool) $this->completed;
|
||||
|
||||
return $return;
|
||||
}
|
||||
@@ -4380,6 +4331,23 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Can use the messenger system
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function canUseMessenger(): bool
|
||||
{
|
||||
/* Test member */
|
||||
if ( $this->members_disable_pm == 2 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Test module permissions */
|
||||
return $this->canAccessModule( \IPS\Application\Module::get( 'core', 'messaging' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a unique member hash for use in RSS keys and such.
|
||||
*
|
||||
@@ -6051,6 +6019,7 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
'action_log'=> $logId,
|
||||
'actor' => implode( ',', $actor ),
|
||||
'new_rank' => $newRank ? $newRank->id : NULL,
|
||||
'time_to_new_rank' => $newRank ? time() - $this->joined->getTimestamp() : NULL,
|
||||
'recognize' => $recognize
|
||||
], FALSE, TRUE );
|
||||
|
||||
@@ -6193,4 +6162,28 @@ class _Member extends \IPS\Patterns\ActiveRecord
|
||||
}
|
||||
catch( \Exception $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook filters
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function webhookFilters() : array
|
||||
{
|
||||
return [
|
||||
'completed' => $this->completed,
|
||||
'allowAdminEmails' => $this->allow_admin_emails,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get unread feature highlights
|
||||
*
|
||||
* @param bool $markRead Mark returned features as read? Useful for debugging.
|
||||
* @return array|NULL
|
||||
*/
|
||||
public function getFeatureHighlights( bool $markRead = TRUE ): ?array
|
||||
{
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
@@ -61,7 +61,7 @@ class _NodeController extends \IPS\Api\Controller
|
||||
$select,
|
||||
isset( \IPS\Request::i()->page ) ? \IPS\Request::i()->page : 1,
|
||||
$class,
|
||||
$select->count(),
|
||||
NULL,
|
||||
$this->member,
|
||||
isset( \IPS\Request::i()->perPage ) ? \IPS\Request::i()->perPage : NULL
|
||||
);
|
||||
@@ -148,7 +148,7 @@ class _NodeController extends \IPS\Api\Controller
|
||||
* @param \IPS\node\Model $node The node
|
||||
* @return \IPS\node\Model
|
||||
*/
|
||||
protected function _createOrUpdate( \IPS\node\Model $node )
|
||||
protected function _createOrUpdate( \IPS\Node\Model $node )
|
||||
{
|
||||
$node->save();
|
||||
|
||||
|
||||
@@ -231,7 +231,8 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
$this->_descriptionHtml,
|
||||
$node->canAdd(),
|
||||
( $node instanceof $nodeClass ),
|
||||
$this->_getRowHtml( $node )
|
||||
$this->_getRowHtml( $node ),
|
||||
isset( $node->_lockedLang ) ? $node->_lockedLang : null
|
||||
);
|
||||
}
|
||||
|
||||
@@ -565,11 +566,18 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
|
||||
/* Get the value */
|
||||
$key = \IPS\Request::i()->key;
|
||||
$sessionKey = 'MC' . $key;
|
||||
$dummyForm = new \IPS\Helpers\Form;
|
||||
$node->form( $dummyForm );
|
||||
if( isset( $_SESSION[ $key ] ) )
|
||||
|
||||
if( isset( \IPS\Request::i()->value ) )
|
||||
{
|
||||
$value = $_SESSION[ $key ];
|
||||
$_SESSION[ $sessionKey ] = \IPS\Request::i()->value;
|
||||
}
|
||||
|
||||
if( isset( $_SESSION[ $sessionKey ] ) )
|
||||
{
|
||||
$value = $_SESSION[ $sessionKey ];
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -586,7 +594,7 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
}
|
||||
}
|
||||
|
||||
$_SESSION[ $key ] = $value;
|
||||
$_SESSION[ $sessionKey ] = $value;
|
||||
}
|
||||
|
||||
/* Build Form */
|
||||
@@ -609,7 +617,7 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
$multiRedirectUrl = $multiRedirectUrl->setQueryString( 'subnode', 1 );
|
||||
}
|
||||
\IPS\Output::i()->output = new \IPS\Helpers\MultipleRedirect( $multiRedirectUrl,
|
||||
function( $doneSoFar ) use ( $nodeClass )
|
||||
function( $doneSoFar ) use ( $nodeClass, $sessionKey )
|
||||
{
|
||||
$count = \IPS\Db::i()->select( 'COUNT(*)', $nodeClass::$databaseTable, \IPS\Request::i()->nodes == 0 ? NULL : \IPS\Db::i()->in( $nodeClass::$databasePrefix . $nodeClass::$databaseColumnId, explode( ',',\IPS\Request::i()->nodes) ) )->first();
|
||||
if ( !$count )
|
||||
@@ -625,9 +633,9 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
$did++;
|
||||
$_node = $nodeClass::constructFromData( $row );
|
||||
|
||||
if ( $_node->canCopyValue( \IPS\Request::i()->key, $_SESSION[ \IPS\Request::i()->key ] ) )
|
||||
if ( $_node->canCopyValue( \IPS\Request::i()->key, $_SESSION[ $sessionKey ] ) )
|
||||
{
|
||||
$values = $_node->formatFormValues( array( \IPS\Request::i()->key => $_SESSION[ \IPS\Request::i()->key ] ) );
|
||||
$values = $_node->formatFormValues( array( \IPS\Request::i()->key => $_SESSION[ $sessionKey ] ) );
|
||||
|
||||
foreach( $values as $k => $v )
|
||||
{
|
||||
@@ -661,13 +669,13 @@ class _Controller extends \IPS\Dispatcher\Controller
|
||||
$doneSoFar += 50;
|
||||
return array( $doneSoFar, \IPS\Member::loggedIn()->language()->addToStack('copying'), 100 / $count * $doneSoFar );
|
||||
},
|
||||
function() use( $url )
|
||||
function() use( $url, $sessionKey )
|
||||
{
|
||||
/* Clear guest page caches */
|
||||
\IPS\Data\Cache::i()->clearAll();
|
||||
|
||||
/* Clear the value in the session */
|
||||
unset( $_SESSION[ \IPS\Request::i()->key ] );
|
||||
unset( $_SESSION[ $sessionKey ] );
|
||||
|
||||
/* And redirect */
|
||||
$finishUrl = $url->setQueryString( array( 'do' => 'form', 'id' => \IPS\Request::i()->from ) );
|
||||
|
||||
+44
-1
@@ -854,6 +854,16 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get a lang string to display when hovering over the locked toggle, or null to display nothing
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
protected function get__lockedLang()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get position
|
||||
*
|
||||
@@ -2049,13 +2059,34 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
|
||||
{
|
||||
$groupsToCheck[] = \IPS\Settings::i()->member_group;
|
||||
}
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'core_permission_index.perm_' . static::$permissionMap[ $permission ], $groupsToCheck ) . ' OR core_permission_index.perm_' . static::$permissionMap[ $permission ] . "='*'" );
|
||||
|
||||
if ( \IPS\Settings::i()->clubs AND \IPS\Settings::i()->club_nodes_in_apps AND \IPS\IPS::classUsesTrait( \get_called_class(), 'IPS\Content\ClubContainer' ) )
|
||||
{
|
||||
$clubWhere = array();
|
||||
foreach( $member->clubs() AS $club )
|
||||
{
|
||||
$clubWhere[] = "core_permission_index.perm_" . static::$permissionMap[ $permission ] . "='cm,c{$club}'";
|
||||
}
|
||||
if ( \count( $clubWhere ) )
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'core_permission_index.perm_' . static::$permissionMap[ $permission ], $groupsToCheck ) . ' OR core_permission_index.perm_' . static::$permissionMap[ $permission ] . "='*' OR core_permission_index.perm_" . static::$permissionMap[ $permission ] . "='ca' OR " . implode( ' OR ', $clubWhere ) );
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'core_permission_index.perm_' . static::$permissionMap[ $permission ], $groupsToCheck ) . ' OR core_permission_index.perm_' . static::$permissionMap[ $permission ] . "='*' OR core_permission_index.perm_" . static::$permissionMap[ $permission ] . "='ca'" );
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
$where[] = array( \IPS\Db::i()->findInSet( 'core_permission_index.perm_' . static::$permissionMap[ $permission ], $groupsToCheck ) . ' OR core_permission_index.perm_' . static::$permissionMap[ $permission ] . "='*'" );
|
||||
}
|
||||
|
||||
/* Run query and return */
|
||||
if ( static::$databaseColumnEnabledDisabled )
|
||||
{
|
||||
$where[] = array( static::$databasePrefix . static::$databaseColumnEnabledDisabled . '=1' );
|
||||
}
|
||||
|
||||
static::$_canOnAny[ $_key ][ $permission ] = (bool) \IPS\Db::i()->select( 'COUNT(*)', static::$databaseTable, $where )
|
||||
->join( 'core_permission_index', array( "core_permission_index.app=? AND core_permission_index.perm_type=? AND core_permission_index.perm_type_id=" . static::$databaseTable . "." . static::$databasePrefix . static::$databaseColumnId, static::$permApp, static::$permType ) )
|
||||
->first();
|
||||
@@ -3850,4 +3881,16 @@ abstract class _Model extends \IPS\Patterns\ActiveRecord
|
||||
return $this->_dataLayerProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Content was held for approval by container
|
||||
* Allow node classes that can determine if content should be held for approval in individual nodes
|
||||
*
|
||||
* @param string $content The type of content we are checking (item, comment, review).
|
||||
* @param \IPS\Member|NULL $member Member to check or NULL for currently logged in member.
|
||||
* @return bool
|
||||
*/
|
||||
public function contentHeldForApprovalByNode( string $content, ?\IPS\Member $member = NULL ): bool
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* @brief Provides notifications results in RESTAPI-friendly format
|
||||
* @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 29 August 2022
|
||||
*/
|
||||
|
||||
namespace IPS\Notification;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* REST API Notification RESPONSE
|
||||
*/
|
||||
class _RestApi extends \IPS\Notification\Inline
|
||||
{
|
||||
/**
|
||||
* Get data from extension
|
||||
*
|
||||
* @param bool $htmlEscape TRUE to escape HTML
|
||||
*/
|
||||
public function getData( $htmlEscape = TRUE )
|
||||
{
|
||||
$methodName = "parse_rest_{$this->notification_key}";
|
||||
|
||||
foreach ( $this->notification_app->extensions( 'core', 'Notifications' ) as $class )
|
||||
{
|
||||
if ( method_exists( $class, $methodName ) )
|
||||
{
|
||||
$return = $class->$methodName( $this, $htmlEscape );
|
||||
if ( !isset( $return['unread'] ) )
|
||||
{
|
||||
$return['unread'] = !$this->read_time;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
// if there's no REST specific method, just let the parent class handle everything else
|
||||
return parent::getData($htmlEscape);
|
||||
}
|
||||
}
|
||||
@@ -729,7 +729,7 @@ class _Output
|
||||
$faulty = \IPS\Member::loggedIn()->language()->addToStack( 'faulty_app', FALSE, array( 'sprintf' => array( $app->_title, \IPS\Http\Url::internal( 'app=core&module=applications&controller=applications', 'admin' ) ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* Send default Cache Headers if this is a front-end page. */
|
||||
$allowGuestCache = FALSE;
|
||||
if ( !isset( $httpHeaders['Cache-Control'] ) AND ( $httpStatusCode == 404 OR $httpStatusCode == 403 ) )
|
||||
@@ -737,13 +737,12 @@ class _Output
|
||||
if ( \IPS\CACHE_PAGE_TIMEOUT AND $this->pageCaching AND \IPS\Dispatcher::hasInstance() AND \IPS\Dispatcher::i()->controllerLocation === 'front' AND !\IPS\Member::loggedIn()->member_id )
|
||||
{
|
||||
$httpHeaders += static::getCacheHeaders( time(), \IPS\CACHE_PAGE_TIMEOUT );
|
||||
$allowGuestCache = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
/* Send output */
|
||||
\IPS\Output::i()->sidebar['enabled'] = FALSE;
|
||||
$this->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( $title, \IPS\Theme::i()->getTemplate( 'global', 'core' )->error( $title, $message, $code, $extra, $member, $faulty, $httpStatusCode ), array( 'app' => \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : NULL, 'module' => \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : NULL, 'controller' => \IPS\Dispatcher::i()->controller ) ), $httpStatusCode, 'text/html', $httpHeaders, $allowGuestCache, FALSE );
|
||||
$this->sendOutput( \IPS\Theme::i()->getTemplate( 'global', 'core' )->globalTemplate( $title, \IPS\Theme::i()->getTemplate( 'global', 'core' )->error( $title, $message, $code, $extra, $member, $faulty, $httpStatusCode ), array( 'app' => \IPS\Dispatcher::i()->application ? \IPS\Dispatcher::i()->application->directory : NULL, 'module' => \IPS\Dispatcher::i()->module ? \IPS\Dispatcher::i()->module->key : NULL, 'controller' => \IPS\Dispatcher::i()->controller ) ), $httpStatusCode, 'text/html', $httpHeaders, FALSE, FALSE );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -833,16 +832,6 @@ class _Output
|
||||
$output = $this->replaceEmojiWithImages( $output );
|
||||
}
|
||||
|
||||
/* Do we want to preload anything? Only send preload headers for full page requests with a text/html response that have output. */
|
||||
if( !$pageIsCached AND !\IPS\IN_DEV AND $output AND !\IPS\Request::i()->isAjax() AND $contentType == 'text/html' )
|
||||
{
|
||||
if( $urls = $this->getPreloadUrls() AND \count( $urls ) )
|
||||
{
|
||||
/* Send a link header for preloading */
|
||||
//$httpHeaders['Link'] = implode( ', ', $urls );
|
||||
}
|
||||
}
|
||||
|
||||
/* Combine some common checks for re-use */
|
||||
$canCache = function( $contentType, $obj ) {
|
||||
if( $obj->pageCaching and
|
||||
@@ -856,7 +845,6 @@ class _Output
|
||||
return FALSE;
|
||||
};
|
||||
|
||||
/* Full page caching for guests */
|
||||
if (
|
||||
\IPS\CACHE_PAGE_TIMEOUT and // Page caching is enabled
|
||||
$cacheThisPage and // Some pages can specify not to be cached (for example, when displaying a cached page, you don't want it recached)
|
||||
@@ -865,21 +853,6 @@ class _Output
|
||||
!isset( \IPS\Request::i()->csrfKey ) and // CSRF key isn't present (which would be like a POST request)
|
||||
!\IPS\Member::loggedIn()->member_id // User is not logged in
|
||||
) {
|
||||
/* Store copy of cached page if output caching is enabled */
|
||||
if( !empty( \IPS\OUTPUT_CACHE_METHOD ) AND \IPS\OUTPUT_CACHE_METHOD !== 'None' )
|
||||
{
|
||||
$theme = isset( \IPS\Request::i()->cookie['theme'] ) ? \IPS\Request::i()->cookie['theme'] : '';
|
||||
|
||||
\IPS\Output\Cache::i()->set( 'page_' . md5( (string) \IPS\Request::i()->url() ) . '_' . \IPS\Member::loggedIn()->language()->id . '_' . $theme,
|
||||
str_replace( \IPS\Session::i()->csrfKey, '{{csrfKey}}', $output ),
|
||||
array(
|
||||
'code' => $httpStatusCode,
|
||||
'contentType' => $contentType,
|
||||
'httpHeaders' => array_merge( $httpHeaders, array( 'X-IPS-Cached-Response' => \IPS\DateTime::create()->rfc1123() ) ),
|
||||
'lastUpdated' => time()
|
||||
), \IPS\DateTime::create()->add( new \DateInterval( 'PT' . \IPS\CACHE_PAGE_TIMEOUT . 'S' ) ), TRUE );
|
||||
}
|
||||
|
||||
/* Add caching headers */
|
||||
if( !isset( $httpHeaders['Cache-Control'] ) )
|
||||
{
|
||||
@@ -1278,7 +1251,7 @@ class _Output
|
||||
$image = str_replace( '00', '', $image );
|
||||
}
|
||||
|
||||
return '<img src="https://twemoji.maxcdn.com/2/72x72/' . $image . '.png" class="ipsEmoji" alt="' . $matches[1] . '">';
|
||||
return '<img src="https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72/' . $image . '.png" class="ipsEmoji" alt="' . $matches[1] . '">';
|
||||
}, $output );
|
||||
}
|
||||
return $output;
|
||||
|
||||
@@ -768,20 +768,33 @@ abstract class _ActiveRecord
|
||||
*/
|
||||
public function coverPhoto()
|
||||
{
|
||||
$photoCol = static::$databaseColumnMap[ 'cover_photo' ];
|
||||
$photoOffset = static::$databaseColumnMap[ 'cover_photo_offset' ];
|
||||
$photo = new \IPS\Helpers\CoverPhoto;
|
||||
if ( isset( static::$databaseColumnMap['cover_photo'] ) and $this->$photoCol )
|
||||
{
|
||||
$photo->file = \IPS\File::get( static::$coverPhotoStorageExtension, $this->$photoCol );
|
||||
$photo->offset = $this->$photoOffset;
|
||||
}
|
||||
|
||||
$photo->editable = $this->canEdit();
|
||||
$photo->object = $this;
|
||||
|
||||
return $photo;
|
||||
$photo = new \IPS\Helpers\CoverPhoto;
|
||||
if( $file = $this->coverPhotoFile() )
|
||||
{
|
||||
$photoOffset = static::$databaseColumnMap[ 'cover_photo_offset' ];
|
||||
$photo->file = $file;
|
||||
$photo->offset = $this->$photoOffset;
|
||||
}
|
||||
$photo->editable = $this->canEdit();
|
||||
$photo->object = $this;
|
||||
return $photo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the CoverPhoto File Instance or NULL if there's none
|
||||
*
|
||||
* @return null|\IPS\File
|
||||
*/
|
||||
public function coverPhotoFile() : ?\IPS\File
|
||||
{
|
||||
$photoCol = static::$databaseColumnMap[ 'cover_photo' ];
|
||||
|
||||
if ( isset( static::$databaseColumnMap['cover_photo'] ) and $this->$photoCol )
|
||||
{
|
||||
return \IPS\File::get( static::$coverPhotoStorageExtension, $this->$photoCol );
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Produce a random hex color for a background
|
||||
|
||||
@@ -378,7 +378,7 @@ class _Hook extends \IPS\Patterns\ActiveRecord
|
||||
$returnType = ( $method->getReturnType()->isBuiltin() ) ? ": " . $method->getReturnType()->getName() : ":\\" . $method->getReturnType()->getName();
|
||||
}
|
||||
|
||||
$method->codeToInject = json_encode( "\n\t{$method->getDocComment()}\n\t" . ( $method->isFinal() ? 'final ' : '' ) . ( $method->isAbstract() ? 'abstract ' : '' ) . ( $method->isStatic() ? 'static ' : '' ) . ( $method->isPublic() ? 'public ' : 'protected ' ) . "function {$method->name}(" . ( empty( $parameters ) ? '' : ( ' ' . implode( ', ', $parameters ) . ' ' ) ) . ")" . $returnType . "\n\t{\n\t\treturn parent::{$method->name}(" . ( empty( $parameters ) ? '' : ( ' ' . implode( ', ', array_map( function( $param ){ return '$' . $param->name; }, $method->getParameters() ) ) . ' ' ) ) . ");\n\t}\n" );
|
||||
$method->codeToInject = json_encode( "\n\t{$method->getDocComment()}\n\t" . ( $method->isFinal() ? 'final ' : '' ) . ( $method->isAbstract() ? 'abstract ' : '' ) . ( $method->isPublic() ? 'public ' : 'protected ' ) . ( $method->isStatic() ? 'static ' : '' ) . "function {$method->name}(" . ( empty( $parameters ) ? '' : ( ' ' . implode( ', ', $parameters ) . ' ' ) ) . ")" . $returnType . "\n\t{\n\t\treturn parent::{$method->name}(" . ( empty( $parameters ) ? '' : ( ' ' . implode( ', ', array_map( function( $param ){ return '$' . $param->name; }, $method->getParameters() ) ) . ' ' ) ) . ");\n\t}\n" );
|
||||
|
||||
$data[ $method->getDeclaringClass()->getName() ]['methods'][ $method->getName() ] = $method;
|
||||
}
|
||||
|
||||
@@ -373,6 +373,26 @@ CODE;
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] See if this is locked
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function get__locked()
|
||||
{
|
||||
return (bool) $this->requires_manual_intervention;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Lang string for the tooltip when this is locked
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function get__lockedLang()
|
||||
{
|
||||
return $this->requires_manual_intervention ? 'invalid_php8_customization' : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* [Node] Get buttons to display in tree
|
||||
@@ -471,7 +491,6 @@ CODE;
|
||||
}
|
||||
}
|
||||
|
||||
// nulled by ipbmafia.ru
|
||||
if ( file_exists( \IPS\ROOT_PATH . "/plugins/{$this->location}/{$this->name}_{$this->version_human}.xml" ) AND !\IPS\IN_DEV )
|
||||
{
|
||||
$buttons['download'] = array(
|
||||
@@ -480,7 +499,6 @@ CODE;
|
||||
'link' => \IPS\Http\Url::internal( "app=core&module=applications&controller=plugins&do=downloadNull&id={$this->_id}" )->csrf(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/* Return */
|
||||
return $buttons;
|
||||
@@ -787,7 +805,7 @@ CODE;
|
||||
|
||||
if ( $depth == 2 and $closeBraces )
|
||||
{
|
||||
$contents .= "{$tabs}}\n{$tabs}catch ( \Error | \RuntimeException \$e )\n{$tabs}{\n{$tabs}\tif ( method_exists( get_parent_class(), __FUNCTION__ ) )\n{$tabs}\t{\n{$tabs}\t\treturn \\call_user_func_array( 'parent::' . __FUNCTION__, \\func_get_args() );\n{$tabs}\t}\n{$tabs}\telse\n{$tabs}\t{\n{$tabs}\t\tthrow \$e;\n{$tabs}\t}\n{$tabs}}\n";
|
||||
$contents .= "{$tabs}}\n{$tabs}catch ( \Error | \RuntimeException \$e )\n{$tabs}{\n{$tabs}\tif( \defined( '\IPS\DEBUG_HOOKS' ) AND \IPS\DEBUG_HOOKS )\n{$tabs}\t{\n{$tabs}\t\t\IPS\Log::log( \$e, 'hook_exception' );\n{$tabs}\t}\n\n{$tabs}\tif ( method_exists( get_parent_class(), __FUNCTION__ ) )\n{$tabs}\t{\n{$tabs}\t\treturn \\call_user_func_array( 'parent::' . __FUNCTION__, \\func_get_args() );\n{$tabs}\t}\n{$tabs}\telse\n{$tabs}\t{\n{$tabs}\t\tthrow \$e;\n{$tabs}\t}\n{$tabs}}\n";
|
||||
$depth--;
|
||||
}
|
||||
|
||||
|
||||
@@ -468,7 +468,7 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function requestMethod()
|
||||
public function requestMethod() :string
|
||||
{
|
||||
return mb_strtoupper( $_SERVER['REQUEST_METHOD'] );
|
||||
}
|
||||
@@ -519,6 +519,26 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check, if this was called by command line
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isCliEnvironment(): bool
|
||||
{
|
||||
return php_sapi_name() == 'cli';
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a request coming from Zapier?
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isZapier() : bool
|
||||
{
|
||||
return str_starts_with( \IPS\Request::i()->userAgent(), 'IPS Zapier Integration' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation check
|
||||
@@ -717,4 +737,14 @@ class _Request extends \IPS\Patterns\Singleton
|
||||
/* Still here? We got nothing */
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user agent
|
||||
*
|
||||
* @return string|null
|
||||
*/
|
||||
public function userAgent() : ?string
|
||||
{
|
||||
return $_SERVER['HTTP_USER_AGENT'] ?? NULL;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 1085 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user