Version 5.0.0 beta 9
This commit is contained in:
1 parent
214f649f1d
commit
56979f8ee8
202 files changed
+4403
-1123
No files matched your search
+241
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum;
|
||||
|
||||
use DASPRiD\Enum\Exception\CloneNotSupportedException;
|
||||
use DASPRiD\Enum\Exception\IllegalArgumentException;
|
||||
use DASPRiD\Enum\Exception\MismatchException;
|
||||
use DASPRiD\Enum\Exception\SerializeNotSupportedException;
|
||||
use DASPRiD\Enum\Exception\UnserializeNotSupportedException;
|
||||
use ReflectionClass;
|
||||
|
||||
abstract class AbstractEnum
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $name;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $ordinal;
|
||||
|
||||
/**
|
||||
* @var array<string, array<string, static>>
|
||||
*/
|
||||
private static $values = [];
|
||||
|
||||
/**
|
||||
* @var array<string, bool>
|
||||
*/
|
||||
private static $allValuesLoaded = [];
|
||||
|
||||
/**
|
||||
* @var array<string, array>
|
||||
*/
|
||||
private static $constants = [];
|
||||
|
||||
/**
|
||||
* The constructor is private by default to avoid arbitrary enum creation.
|
||||
*
|
||||
* When creating your own constructor for a parameterized enum, make sure to declare it as protected, so that
|
||||
* the static methods are able to construct it. Avoid making it public, as that would allow creation of
|
||||
* non-singleton enum instances.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic getter which forwards all calls to {@see self::valueOf()}.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
final public static function __callStatic(string $name, array $arguments) : self
|
||||
{
|
||||
return static::valueOf($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an enum with the specified name.
|
||||
*
|
||||
* The name must match exactly an identifier used to declare an enum in this type (extraneous whitespace characters
|
||||
* are not permitted).
|
||||
*
|
||||
* @return static
|
||||
* @throws IllegalArgumentException if the enum has no constant with the specified name
|
||||
*/
|
||||
final public static function valueOf(string $name) : self
|
||||
{
|
||||
if (isset(self::$values[static::class][$name])) {
|
||||
return self::$values[static::class][$name];
|
||||
}
|
||||
|
||||
$constants = self::constants();
|
||||
|
||||
if (array_key_exists($name, $constants)) {
|
||||
return self::createValue($name, $constants[$name][0], $constants[$name][1]);
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(sprintf('No enum constant %s::%s', static::class, $name));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return static
|
||||
*/
|
||||
private static function createValue(string $name, int $ordinal, array $arguments) : self
|
||||
{
|
||||
$instance = new static(...$arguments);
|
||||
$instance->name = $name;
|
||||
$instance->ordinal = $ordinal;
|
||||
self::$values[static::class][$name] = $instance;
|
||||
return $instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtains all possible types defined by this enum.
|
||||
*
|
||||
* @return static[]
|
||||
*/
|
||||
final public static function values() : array
|
||||
{
|
||||
if (isset(self::$allValuesLoaded[static::class])) {
|
||||
return self::$values[static::class];
|
||||
}
|
||||
|
||||
if (! isset(self::$values[static::class])) {
|
||||
self::$values[static::class] = [];
|
||||
}
|
||||
|
||||
foreach (self::constants() as $name => $constant) {
|
||||
if (array_key_exists($name, self::$values[static::class])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
static::createValue($name, $constant[0], $constant[1]);
|
||||
}
|
||||
|
||||
uasort(self::$values[static::class], function (self $a, self $b) {
|
||||
return $a->ordinal() <=> $b->ordinal();
|
||||
});
|
||||
|
||||
self::$allValuesLoaded[static::class] = true;
|
||||
return self::$values[static::class];
|
||||
}
|
||||
|
||||
private static function constants() : array
|
||||
{
|
||||
if (isset(self::$constants[static::class])) {
|
||||
return self::$constants[static::class];
|
||||
}
|
||||
|
||||
self::$constants[static::class] = [];
|
||||
$reflectionClass = new ReflectionClass(static::class);
|
||||
$ordinal = -1;
|
||||
|
||||
foreach ($reflectionClass->getReflectionConstants() as $reflectionConstant) {
|
||||
if (! $reflectionConstant->isProtected()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $reflectionConstant->getValue();
|
||||
|
||||
self::$constants[static::class][$reflectionConstant->name] = [
|
||||
++$ordinal,
|
||||
is_array($value) ? $value : []
|
||||
];
|
||||
}
|
||||
|
||||
return self::$constants[static::class];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of this enum constant, exactly as declared in its enum declaration.
|
||||
*
|
||||
* Most programmers should use the {@see self::__toString()} method in preference to this one, as the toString
|
||||
* method may return a more user-friendly name. This method is designed primarily for use in specialized situations
|
||||
* where correctness depends on getting the exact name, which will not vary from release to release.
|
||||
*/
|
||||
final public function name() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ordinal of this enumeration constant (its position in its enum declaration, where the initial
|
||||
* constant is assigned an ordinal of zero).
|
||||
*
|
||||
* Most programmers will have no use for this method. It is designed for use by sophisticated enum-based data
|
||||
* structures.
|
||||
*/
|
||||
final public function ordinal() : int
|
||||
{
|
||||
return $this->ordinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this enum with the specified object for order.
|
||||
*
|
||||
* Returns negative integer, zero or positive integer as this object is less than, equal to or greater than the
|
||||
* specified object.
|
||||
*
|
||||
* Enums are only comparable to other enums of the same type. The natural order implemented by this method is the
|
||||
* order in which the constants are declared.
|
||||
*
|
||||
* @throws MismatchException if the passed enum is not of the same type
|
||||
*/
|
||||
final public function compareTo(self $other) : int
|
||||
{
|
||||
if (! $other instanceof static) {
|
||||
throw new MismatchException(sprintf(
|
||||
'The passed enum %s is not of the same type as %s',
|
||||
get_class($other),
|
||||
static::class
|
||||
));
|
||||
}
|
||||
|
||||
return $this->ordinal - $other->ordinal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid cloning enums.
|
||||
*
|
||||
* @throws CloneNotSupportedException
|
||||
*/
|
||||
final public function __clone()
|
||||
{
|
||||
throw new CloneNotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid serializing enums.
|
||||
*
|
||||
* @throws SerializeNotSupportedException
|
||||
*/
|
||||
final public function __sleep() : array
|
||||
{
|
||||
throw new SerializeNotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid unserializing enums.
|
||||
*
|
||||
* @throws UnserializeNotSupportedException
|
||||
*/
|
||||
final public function __wakeup() : void
|
||||
{
|
||||
throw new UnserializeNotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Turns the enum into a string representation.
|
||||
*
|
||||
* You may override this method to give a more user-friendly version.
|
||||
*/
|
||||
public function __toString() : string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
+385
@@ -0,0 +1,385 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum;
|
||||
|
||||
use DASPRiD\Enum\Exception\ExpectationException;
|
||||
use DASPRiD\Enum\Exception\IllegalArgumentException;
|
||||
use IteratorAggregate;
|
||||
use Serializable;
|
||||
use Traversable;
|
||||
|
||||
/**
|
||||
* A specialized map implementation for use with enum type keys.
|
||||
*
|
||||
* All of the keys in an enum map must come from a single enum type that is specified, when the map is created. Enum
|
||||
* maps are represented internally as arrays. This representation is extremely compact and efficient.
|
||||
*
|
||||
* Enum maps are maintained in the natural order of their keys (the order in which the enum constants are declared).
|
||||
* This is reflected in the iterators returned by the collection views {@see self::getIterator()} and
|
||||
* {@see self::values()}.
|
||||
*
|
||||
* Iterators returned by the collection views are not consistent: They may or may not show the effects of modifications
|
||||
* to the map that occur while the iteration is in progress.
|
||||
*/
|
||||
final class EnumMap implements Serializable, IteratorAggregate
|
||||
{
|
||||
/**
|
||||
* The class name of the key.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $keyType;
|
||||
|
||||
/**
|
||||
* The type of the value.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $valueType;
|
||||
|
||||
/**
|
||||
* @var bool
|
||||
*/
|
||||
private $allowNullValues;
|
||||
|
||||
/**
|
||||
* All of the constants comprising the enum, cached for performance.
|
||||
*
|
||||
* @var array<int, AbstractEnum>
|
||||
*/
|
||||
private $keyUniverse;
|
||||
|
||||
/**
|
||||
* Array representation of this map. The ith element is the value to which universe[i] is currently mapped, or null
|
||||
* if it isn't mapped to anything, or NullValue if it's mapped to null.
|
||||
*
|
||||
* @var array<int, mixed>
|
||||
*/
|
||||
private $values;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
private $size = 0;
|
||||
|
||||
/**
|
||||
* Creates a new enum map.
|
||||
*
|
||||
* @param string $keyType the type of the keys, must extend AbstractEnum
|
||||
* @param string $valueType the type of the values
|
||||
* @param bool $allowNullValues whether to allow null values
|
||||
* @throws IllegalArgumentException when key type does not extend AbstractEnum
|
||||
*/
|
||||
public function __construct(string $keyType, string $valueType, bool $allowNullValues)
|
||||
{
|
||||
if (! is_subclass_of($keyType, AbstractEnum::class)) {
|
||||
throw new IllegalArgumentException(sprintf(
|
||||
'Class %s does not extend %s',
|
||||
$keyType,
|
||||
AbstractEnum::class
|
||||
));
|
||||
}
|
||||
|
||||
$this->keyType = $keyType;
|
||||
$this->valueType = $valueType;
|
||||
$this->allowNullValues = $allowNullValues;
|
||||
$this->keyUniverse = $keyType::values();
|
||||
$this->values = array_fill(0, count($this->keyUniverse), null);
|
||||
}
|
||||
|
||||
public function __serialize(): array
|
||||
{
|
||||
$values = [];
|
||||
|
||||
foreach ($this->values as $ordinal => $value) {
|
||||
if (null === $value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$values[$ordinal] = $this->unmaskNull($value);
|
||||
}
|
||||
|
||||
return [
|
||||
'keyType' => $this->keyType,
|
||||
'valueType' => $this->valueType,
|
||||
'allowNullValues' => $this->allowNullValues,
|
||||
'values' => $values,
|
||||
];
|
||||
}
|
||||
|
||||
public function __unserialize(array $data): void
|
||||
{
|
||||
$this->unserialize(serialize($data));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether the map types match the supplied ones.
|
||||
*
|
||||
* You should call this method when an EnumMap is passed to you and you want to ensure that it's made up of the
|
||||
* correct types.
|
||||
*
|
||||
* @throws ExpectationException when supplied key type mismatches local key type
|
||||
* @throws ExpectationException when supplied value type mismatches local value type
|
||||
* @throws ExpectationException when the supplied map allows null values, abut should not
|
||||
*/
|
||||
public function expect(string $keyType, string $valueType, bool $allowNullValues) : void
|
||||
{
|
||||
if ($keyType !== $this->keyType) {
|
||||
throw new ExpectationException(sprintf(
|
||||
'Callee expected an EnumMap with key type %s, but got %s',
|
||||
$keyType,
|
||||
$this->keyType
|
||||
));
|
||||
}
|
||||
|
||||
if ($valueType !== $this->valueType) {
|
||||
throw new ExpectationException(sprintf(
|
||||
'Callee expected an EnumMap with value type %s, but got %s',
|
||||
$keyType,
|
||||
$this->keyType
|
||||
));
|
||||
}
|
||||
|
||||
if ($allowNullValues !== $this->allowNullValues) {
|
||||
throw new ExpectationException(sprintf(
|
||||
'Callee expected an EnumMap with nullable flag %s, but got %s',
|
||||
($allowNullValues ? 'true' : 'false'),
|
||||
($this->allowNullValues ? 'true' : 'false')
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of key-value mappings in this map.
|
||||
*/
|
||||
public function size() : int
|
||||
{
|
||||
return $this->size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this map maps one or more keys to the specified value.
|
||||
*/
|
||||
public function containsValue($value) : bool
|
||||
{
|
||||
return in_array($this->maskNull($value), $this->values, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if this map contains a mapping for the specified key.
|
||||
*/
|
||||
public function containsKey(AbstractEnum $key) : bool
|
||||
{
|
||||
$this->checkKeyType($key);
|
||||
return null !== $this->values[$key->ordinal()];
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value to which the specified key is mapped, or null if this map contains no mapping for the key.
|
||||
*
|
||||
* More formally, if this map contains a mapping from a key to a value, then this method returns the value;
|
||||
* otherwise it returns null (there can be at most one such mapping).
|
||||
*
|
||||
* A return value of null does not necessarily indicate that the map contains no mapping for the key; it's also
|
||||
* possible that hte map explicitly maps the key to null. The {@see self::containsKey()} operation may be used to
|
||||
* distinguish these two cases.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get(AbstractEnum $key)
|
||||
{
|
||||
$this->checkKeyType($key);
|
||||
return $this->unmaskNull($this->values[$key->ordinal()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associates the specified value with the specified key in this map.
|
||||
*
|
||||
* If the map previously contained a mapping for this key, the old value is replaced.
|
||||
*
|
||||
* @return mixed the previous value associated with the specified key, or null if there was no mapping for the key.
|
||||
* (a null return can also indicate that the map previously associated null with the specified key.)
|
||||
* @throws IllegalArgumentException when the passed values does not match the internal value type
|
||||
*/
|
||||
public function put(AbstractEnum $key, $value)
|
||||
{
|
||||
$this->checkKeyType($key);
|
||||
|
||||
if (! $this->isValidValue($value)) {
|
||||
throw new IllegalArgumentException(sprintf('Value is not of type %s', $this->valueType));
|
||||
}
|
||||
|
||||
$index = $key->ordinal();
|
||||
$oldValue = $this->values[$index];
|
||||
$this->values[$index] = $this->maskNull($value);
|
||||
|
||||
if (null === $oldValue) {
|
||||
++$this->size;
|
||||
}
|
||||
|
||||
return $this->unmaskNull($oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the mapping for this key frm this map if present.
|
||||
*
|
||||
* @return mixed the previous value associated with the specified key, or null if there was no mapping for the key.
|
||||
* (a null return can also indicate that the map previously associated null with the specified key.)
|
||||
*/
|
||||
public function remove(AbstractEnum $key)
|
||||
{
|
||||
$this->checkKeyType($key);
|
||||
|
||||
$index = $key->ordinal();
|
||||
$oldValue = $this->values[$index];
|
||||
$this->values[$index] = null;
|
||||
|
||||
if (null !== $oldValue) {
|
||||
--$this->size;
|
||||
}
|
||||
|
||||
return $this->unmaskNull($oldValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all mappings from this map.
|
||||
*/
|
||||
public function clear() : void
|
||||
{
|
||||
$this->values = array_fill(0, count($this->keyUniverse), null);
|
||||
$this->size = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the specified map with this map for quality.
|
||||
*
|
||||
* Returns true if the two maps represent the same mappings.
|
||||
*/
|
||||
public function equals(self $other) : bool
|
||||
{
|
||||
if ($this === $other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->size !== $other->size) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->values === $other->values;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the values contained in this map.
|
||||
*
|
||||
* The array will contain the values in the order their corresponding keys appear in the map, which is their natural
|
||||
* order (the order in which the num constants are declared).
|
||||
*/
|
||||
public function values() : array
|
||||
{
|
||||
return array_values(array_map(function ($value) {
|
||||
return $this->unmaskNull($value);
|
||||
}, array_filter($this->values, function ($value) : bool {
|
||||
return null !== $value;
|
||||
})));
|
||||
}
|
||||
|
||||
public function serialize() : string
|
||||
{
|
||||
return serialize($this->__serialize());
|
||||
}
|
||||
|
||||
public function unserialize($serialized) : void
|
||||
{
|
||||
$data = unserialize($serialized);
|
||||
$this->__construct($data['keyType'], $data['valueType'], $data['allowNullValues']);
|
||||
|
||||
foreach ($this->keyUniverse as $key) {
|
||||
if (array_key_exists($key->ordinal(), $data['values'])) {
|
||||
$this->put($key, $data['values'][$key->ordinal()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getIterator() : Traversable
|
||||
{
|
||||
foreach ($this->keyUniverse as $key) {
|
||||
if (null === $this->values[$key->ordinal()]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
yield $key => $this->unmaskNull($this->values[$key->ordinal()]);
|
||||
}
|
||||
}
|
||||
|
||||
private function maskNull($value)
|
||||
{
|
||||
if (null === $value) {
|
||||
return NullValue::instance();
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function unmaskNull($value)
|
||||
{
|
||||
if ($value instanceof NullValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws IllegalArgumentException when the passed key does not match the internal key type
|
||||
*/
|
||||
private function checkKeyType(AbstractEnum $key) : void
|
||||
{
|
||||
if (get_class($key) !== $this->keyType) {
|
||||
throw new IllegalArgumentException(sprintf(
|
||||
'Object of type %s is not the same type as %s',
|
||||
get_class($key),
|
||||
$this->keyType
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private function isValidValue($value) : bool
|
||||
{
|
||||
if (null === $value) {
|
||||
if ($this->allowNullValues) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ($this->valueType) {
|
||||
case 'mixed':
|
||||
return true;
|
||||
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
return is_bool($value);
|
||||
|
||||
case 'int':
|
||||
case 'integer':
|
||||
return is_int($value);
|
||||
|
||||
case 'float':
|
||||
case 'double':
|
||||
return is_float($value);
|
||||
|
||||
case 'string':
|
||||
return is_string($value);
|
||||
|
||||
case 'object':
|
||||
return is_object($value);
|
||||
|
||||
case 'array':
|
||||
return is_array($value);
|
||||
}
|
||||
|
||||
return $value instanceof $this->valueType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class CloneNotSupportedException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Throwable;
|
||||
|
||||
interface ExceptionInterface extends Throwable
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class ExpectationException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class IllegalArgumentException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class MismatchException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class SerializeNotSupportedException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum\Exception;
|
||||
|
||||
use Exception;
|
||||
|
||||
final class UnserializeNotSupportedException extends Exception implements ExceptionInterface
|
||||
{
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace DASPRiD\Enum;
|
||||
|
||||
use DASPRiD\Enum\Exception\CloneNotSupportedException;
|
||||
use DASPRiD\Enum\Exception\SerializeNotSupportedException;
|
||||
use DASPRiD\Enum\Exception\UnserializeNotSupportedException;
|
||||
|
||||
final class NullValue
|
||||
{
|
||||
/**
|
||||
* @var self
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
|
||||
public static function instance() : self
|
||||
{
|
||||
return self::$instance ?: self::$instance = new self();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid cloning enums.
|
||||
*
|
||||
* @throws CloneNotSupportedException
|
||||
*/
|
||||
final public function __clone()
|
||||
{
|
||||
throw new CloneNotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid serializing enums.
|
||||
*
|
||||
* @throws SerializeNotSupportedException
|
||||
*/
|
||||
final public function __sleep() : array
|
||||
{
|
||||
throw new SerializeNotSupportedException();
|
||||
}
|
||||
|
||||
/**
|
||||
* Forbid unserializing enums.
|
||||
*
|
||||
* @throws UnserializeNotSupportedException
|
||||
*/
|
||||
final public function __wakeup() : void
|
||||
{
|
||||
throw new UnserializeNotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -479,7 +479,7 @@ abstract class Comment extends Content
|
||||
if ( IPS::classUsesTrait( $this, 'IPS\Content\Anonymous' ) and isset( $item::$databaseColumnMap['last_comment_anon'] ) and isset( static::$databaseColumnMap['is_anon'] ) )
|
||||
{
|
||||
$lastCommentAnon = $item::$databaseColumnMap['last_comment_anon'];
|
||||
$item->$lastCommentAnon = $this->mapped('is_anon');
|
||||
$item->$lastCommentAnon = (int) $this->mapped('is_anon');
|
||||
}
|
||||
|
||||
$item->save();
|
||||
@@ -1035,11 +1035,6 @@ abstract class Comment extends Content
|
||||
{
|
||||
Index::i()->index( $this );
|
||||
}
|
||||
|
||||
if( Content\Search\SearchContent::isSearchable( $item ) )
|
||||
{
|
||||
$item->clearCachedStatistics();
|
||||
}
|
||||
}
|
||||
|
||||
/* Send the content items URL to IndexNow */
|
||||
@@ -1188,7 +1183,7 @@ abstract class Comment extends Content
|
||||
|
||||
if ( $item AND IPS::classUsesTrait( $this->item(), 'IPS\Content\Statistics' ) )
|
||||
{
|
||||
$item->clearCachedStatistics();
|
||||
$item->clearCachedStatistics( TRUE );
|
||||
}
|
||||
|
||||
/* Delete any notifications telling people about this */
|
||||
@@ -1883,7 +1878,7 @@ abstract class Comment extends Content
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! Output::i()->reduceLinks() )
|
||||
if ( ! Output::i()->reduceLinks() and $this->canShare() )
|
||||
{
|
||||
if ( $this->mapped( 'first' ) )
|
||||
{
|
||||
@@ -1921,7 +1916,7 @@ abstract class Comment extends Content
|
||||
}
|
||||
}
|
||||
|
||||
if( $this->canEdit($member ) or (IPS::classUsesTrait( $this, "\\IPS\\Content\\Hideable" ) and $this->canHide( $member )) or $this->canSplit( $member ) )
|
||||
if( $this->canEdit( $member ) or ( IPS::classUsesTrait( $this, "\\IPS\\Content\\Hideable" ) and $this->canHide( $member ) ) or $this->canSplit( $member ) )
|
||||
{
|
||||
$menu->addSeparator();
|
||||
}
|
||||
@@ -1965,7 +1960,7 @@ abstract class Comment extends Content
|
||||
|
||||
if( $this->canUnhide($member) )
|
||||
{
|
||||
$menu->add( new ContentMenuLink( url: $this->url( 'unhide' )->csrf(), languageString: 'unhide', dataAttributes: [
|
||||
$menu->add( new ContentMenuLink( url: $this->url( 'unhide' )->csrf(), languageString: ( $this->hidden() === 1 ? 'approve' : 'unhide' ), dataAttributes: [
|
||||
'data-ipsDialog-destructOnClose' => 'true'
|
||||
] ) );
|
||||
}
|
||||
|
||||
@@ -1475,8 +1475,10 @@ class Controller extends CoverPhotoController
|
||||
{
|
||||
$options[ $type->id ] = $type->_title;
|
||||
}
|
||||
|
||||
$form->add( new Radio( 'report_type', NULL, FALSE, array( 'options' => $options ) ) );
|
||||
if ( count( $options ) > 1 )
|
||||
{
|
||||
$form->add( new Radio( 'report_type', NULL, FALSE, ['options' => $options] ) );
|
||||
}
|
||||
}
|
||||
|
||||
$form->add( new Editor( 'report_message', NULL, FALSE, array( 'app' => 'core', 'key' => 'Reports', 'autoSaveKey' => "report-{$class::$application}-{$class::$module}-{$item->$idColumn}", 'minimize' => Request::i()->isAjax() ? 'report_message_placeholder' : NULL ) ) );
|
||||
|
||||
@@ -42,6 +42,12 @@ trait Featurable
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Skip for hidden content */
|
||||
if( $this->hidden() !== 0 )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Is this the first comment in an item that requires first comments? */
|
||||
if ( $this instanceof Comment )
|
||||
{
|
||||
|
||||
@@ -231,7 +231,7 @@ trait Hideable
|
||||
/* Restoring as hidden? */
|
||||
if ( $hidden )
|
||||
{
|
||||
$column = static::$databaseColumnMap['hidden'] ?? static::$databaseColumnId['approved'];
|
||||
$column = static::$databaseColumnMap['hidden'] ?? static::$databaseColumnMap['approved'];
|
||||
$this->$column = -1;
|
||||
}
|
||||
else
|
||||
@@ -461,7 +461,7 @@ trait Hideable
|
||||
{
|
||||
try
|
||||
{
|
||||
$item->clearCachedStatistics();
|
||||
$item->clearCachedStatistics( TRUE );
|
||||
}
|
||||
catch ( Exception )
|
||||
{
|
||||
@@ -1354,7 +1354,7 @@ trait Hideable
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $this->hidden() === -1 )
|
||||
if ( $this->hidden() <> 0 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
@@ -1381,7 +1381,7 @@ trait Hideable
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( !$this->hidden() )
|
||||
if ( $this->hidden() === 0 )
|
||||
{
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
@@ -832,7 +832,7 @@ abstract class Item extends Content
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name'), 'htmlAutocomplete' => "username" ), NULL, NULL, NULL, 'guest_name' );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' and Settings::i()->guest_captcha )
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
{
|
||||
$return['captcha'] = new Captcha;
|
||||
}
|
||||
@@ -4871,7 +4871,7 @@ abstract class Item extends Content
|
||||
$return['guest_name'] = new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ) );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' and Settings::i()->guest_captcha )
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
{
|
||||
$return['captcha'] = new Captcha;
|
||||
}
|
||||
@@ -5033,7 +5033,7 @@ abstract class Item extends Content
|
||||
$form->add( new Text( 'guest_name', NULL, FALSE, array( 'minLength' => Settings::i()->min_user_name_length, 'maxLength' => Settings::i()->max_user_name_length, 'placeholder' => Member::loggedIn()->language()->addToStack('comment_guest_name') ) ) );
|
||||
}
|
||||
}
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' and Settings::i()->guest_captcha )
|
||||
if ( Settings::i()->bot_antispam_type !== 'none' )
|
||||
{
|
||||
$form->add( new Captcha );
|
||||
}
|
||||
|
||||
@@ -68,6 +68,13 @@ trait ItemTopic
|
||||
*/
|
||||
abstract function getForumId() : int;
|
||||
|
||||
/**
|
||||
* Determine if the topic sync is enabled
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
abstract function isTopicSyncEnabled() : bool;
|
||||
|
||||
/**
|
||||
* Create/Update Topic
|
||||
*
|
||||
@@ -75,6 +82,12 @@ trait ItemTopic
|
||||
*/
|
||||
public function syncTopic(): void
|
||||
{
|
||||
/* If the topic sync is disabled, stop here */
|
||||
if( !$this->isTopicSyncEnabled() )
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* If this item is pending deletion, do nothing */
|
||||
if( $this->hidden() == -2 )
|
||||
{
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @brief Bluesky share link
|
||||
* @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 19 November 2024
|
||||
*/
|
||||
|
||||
namespace IPS\Content\ShareServices;
|
||||
|
||||
use IPS\Content\ShareServices;
|
||||
use IPS\Theme;
|
||||
|
||||
/* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bluesky share link
|
||||
*/
|
||||
class Bluesky extends ShareServices
|
||||
{
|
||||
/**
|
||||
* Return the HTML code to show the share link
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return Theme::i()->getTemplate( 'sharelinks', 'core' )->bluesky( urlencode( $this->url ), $this->title ? urlencode( $this->title ) : NULL );
|
||||
}
|
||||
}
|
||||
@@ -36,14 +36,15 @@ abstract class ShareServices
|
||||
*/
|
||||
public static function services(): array
|
||||
{
|
||||
return array(
|
||||
return [
|
||||
'Bluesky' => 'IPS\Content\ShareServices\Bluesky',
|
||||
'Facebook' => 'IPS\Content\ShareServices\Facebook',
|
||||
'Linkedin' => 'IPS\Content\ShareServices\Linkedin',
|
||||
'Pinterest' => 'IPS\Content\ShareServices\Pinterest',
|
||||
'Reddit' => 'IPS\Content\ShareServices\Reddit',
|
||||
'Email' => 'IPS\Content\ShareServices\Email',
|
||||
'X' => 'IPS\Content\ShareServices\X',
|
||||
);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -553,13 +553,22 @@ trait Statistics
|
||||
/**
|
||||
* Clear any cached stats
|
||||
*
|
||||
* @param bool $force Force immediate cache delete
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
public function clearCachedStatistics(): void
|
||||
public function clearCachedStatistics( bool $force=FALSE )
|
||||
{
|
||||
$idField = static::$databaseColumnId;
|
||||
Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', get_class( $this ), $this->$idField ) );
|
||||
if( $force )
|
||||
{
|
||||
Db::i()->delete( 'core_item_statistics_cache', [ 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ] );
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Set cache to time out in 10 minutes. */
|
||||
Db::i()->update( 'core_item_statistics_cache', [ 'cache_added' => ( time() - 172200 ) ], [ 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -728,6 +737,11 @@ trait Statistics
|
||||
*/
|
||||
public static array|null $cachedActivity = NULL;
|
||||
|
||||
/**
|
||||
* @var bool|null Process has rebuild lock
|
||||
*/
|
||||
protected ?bool $hasRebuildLock = NULL;
|
||||
|
||||
/**
|
||||
* Get the cached data
|
||||
*
|
||||
@@ -738,28 +752,57 @@ trait Statistics
|
||||
protected function _getCached( string $key ): mixed
|
||||
{
|
||||
$idField = static::$databaseColumnId;
|
||||
if( $this->hasRebuildLock === FALSE )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$class = get_class( $this );
|
||||
$arrayKey = $class.'.'.$this->$idField;
|
||||
if ( !isset( static::$cachedActivity[$arrayKey] ) )
|
||||
$arrayKey = $class . '.' . $this->$idField;
|
||||
if ( !isset( static::$cachedActivity[ $arrayKey ] ) )
|
||||
{
|
||||
try
|
||||
{
|
||||
$cache = Db::i()->select( '*', 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', $class, $this->$idField ) )->first();
|
||||
|
||||
if ( $cache['cache_added'] > time() - 86400 )
|
||||
$cache = Db::i()->select( '*', 'core_item_statistics_cache', [ 'cache_class=? and cache_item_id=?', $class, $this->$idField ] )->first();
|
||||
if ( $cache['cache_added'] > ( time() - 172800 ) )
|
||||
{
|
||||
static::$cachedActivity[ $arrayKey ] = json_decode( $cache['cache_contents'], TRUE );
|
||||
}
|
||||
else
|
||||
{
|
||||
Db::i()->delete( 'core_item_statistics_cache', array( 'cache_class=? and cache_item_id=?', get_class( $this ), $this->$idField ) );
|
||||
$this->hasRebuildLock = TRUE;
|
||||
$affectedRows = Db::i()->delete( 'core_item_statistics_cache', [ 'cache_class=? and cache_item_id=?', \get_class( $this ), $this->$idField ] );
|
||||
if( $affectedRows == 0 )
|
||||
{
|
||||
/* We did not delete the row, allow another process to rebuild the cache */
|
||||
$this->hasRebuildLock = FALSE;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
/* Try to get the lock for new cache generation */
|
||||
try
|
||||
{
|
||||
Db::i()->insert( 'core_item_statistics_cache', [
|
||||
'cache_class' => \get_class( $this ),
|
||||
'cache_item_id' => $this->$idField,
|
||||
'cache_contents' => '[]',
|
||||
'cache_added' => time()
|
||||
] );
|
||||
$this->hasRebuildLock = TRUE;
|
||||
}
|
||||
/* We didn't get the lock, return empty array and allow other process to build cache */
|
||||
catch( \IPS\Db\Exception $e )
|
||||
{
|
||||
$this->hasRebuildLock = FALSE;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
catch( UnderflowException $e ) { }
|
||||
}
|
||||
|
||||
if ( isset(static::$cachedActivity[ $arrayKey ] ) and isset( static::$cachedActivity[ $arrayKey ][ $key ] ) )
|
||||
if ( isset( static::$cachedActivity[ $arrayKey ] ) and isset( static::$cachedActivity[ $arrayKey ][ $key ] ) )
|
||||
{
|
||||
return static::$cachedActivity[ $arrayKey ][ $key ];
|
||||
}
|
||||
@@ -767,10 +810,10 @@ trait Statistics
|
||||
{
|
||||
if( !isset( static::$cachedActivity[ $arrayKey ] ) )
|
||||
{
|
||||
static::$cachedActivity[ $arrayKey ] = array();
|
||||
static::$cachedActivity[ $arrayKey ] = [];
|
||||
}
|
||||
|
||||
static::$cachedActivity[ $arrayKey ][ $key ] = array();
|
||||
static::$cachedActivity[ $arrayKey ][ $key ] = [];
|
||||
throw new OutOfRangeException;
|
||||
}
|
||||
}
|
||||
@@ -819,8 +862,8 @@ trait Statistics
|
||||
{
|
||||
Db::i()->insert( 'core_item_statistics_cache', array(
|
||||
'cache_class' => get_class( $this ),
|
||||
'cache_item_id' => str_replace( get_class($this) . '.', '', $key),
|
||||
'cache_contents' => json_encode( static::$cachedActivity[$key] ),
|
||||
'cache_item_id' => str_replace( \get_class( $this ) . '.', '', $key ),
|
||||
'cache_contents' => json_encode( static::$cachedActivity[ $key ] ) ?? '[]',
|
||||
'cache_added' => time()
|
||||
), TRUE );
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
|
||||
namespace IPS\Email\Outgoing;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
use IPS\Email;
|
||||
use IPS\Login;
|
||||
use IPS\Http\Url;
|
||||
use IPS\Request;
|
||||
use IPS\Settings;
|
||||
use function defined;
|
||||
@@ -21,6 +21,8 @@ use function stream_socket_enable_crypto;
|
||||
use const IPS\DEFAULT_REQUEST_TIMEOUT;
|
||||
use const IPS\VERY_LONG_REQUEST_TIMEOUT;
|
||||
|
||||
/* To prevent PHP errors (extending class does not exist) revealing path */
|
||||
|
||||
if ( !defined( '\IPS\SUITE_UNIQUE_KEY' ) )
|
||||
{
|
||||
header( ( $_SERVER['SERVER_PROTOCOL'] ?? 'HTTP/1.0' ) . ' 403 Forbidden' );
|
||||
@@ -128,15 +130,16 @@ class Smtp extends Email
|
||||
}
|
||||
|
||||
/* HELO/EHLO */
|
||||
$ehloFqdn = \IPS\Http\Url::internal('')->data['host'];
|
||||
try
|
||||
{
|
||||
$helo = 'EHLO';
|
||||
$responseCode = $this->_sendCommand('EHLO ' . $this->smtpHost, 250);
|
||||
$responseCode = $this->_sendCommand( 'EHLO ' . $ehloFqdn, 250 );
|
||||
}
|
||||
catch (Exception $e )
|
||||
{
|
||||
$helo = 'HELO';
|
||||
$responseCode = $this->_sendCommand('HELO ' . $this->smtpHost, 250);
|
||||
$responseCode = $this->_sendCommand( 'HELO ' . $ehloFqdn, 250 );
|
||||
}
|
||||
|
||||
/* Is TLS being used? */
|
||||
@@ -163,7 +166,7 @@ class Smtp extends Email
|
||||
}
|
||||
|
||||
/* Exchange server (at least) wants EHLO resending for STARTTLS */
|
||||
$this->_sendCommand($helo . ' ' . $this->smtpHost, 250);
|
||||
$this->_sendCommand( $helo . ' ' . $ehloFqdn, 250 );
|
||||
}
|
||||
|
||||
/* Authenticate */
|
||||
@@ -193,19 +196,24 @@ class Smtp extends Email
|
||||
$fromEmail = $fromEmail ?: Settings::i()->email_out;
|
||||
|
||||
/* SMTP requires you to do CC/BCC by sending a RCPT TO command for each recipient. We'll hide BCC by not actually setting that header */
|
||||
$recipieintsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
|
||||
$recipientsForSmtp = explode( ',', static::_parseRecipients( $to, TRUE ) );
|
||||
if ( $cc )
|
||||
{
|
||||
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
|
||||
$recipientsForSmtp = array_merge( $recipientsForSmtp, explode( ',', static::_parseRecipients( $cc, TRUE ) ) );
|
||||
}
|
||||
if ( $bcc )
|
||||
{
|
||||
$recipieintsForSmtp = array_merge( $recipieintsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
|
||||
$recipientsForSmtp = array_merge( $recipientsForSmtp, explode( ',', static::_parseRecipients( $bcc, TRUE ) ) );
|
||||
}
|
||||
$recipieintsForSmtp = array_unique( array_map( 'trim', $recipieintsForSmtp ) );
|
||||
|
||||
$recipientsForSmtp = array_unique( array_map( 'trim', $recipientsForSmtp ) );
|
||||
|
||||
if( empty( $additonalHeaders['Message-ID'] ) )
|
||||
{
|
||||
$additionalHeaders['Message-ID'] = $this->generateMessageId();
|
||||
}
|
||||
|
||||
/* Send */
|
||||
$this->_sendCompiled( $fromEmail, $recipieintsForSmtp, $this->compileFullEmail( $to, $cc, array(), $fromEmail, $fromName, $additionalHeaders ) );
|
||||
$this->_sendCompiled( $fromEmail, $recipientsForSmtp, $this->compileFullEmail( $to, $cc, array(), $fromEmail, $fromName, $additionalHeaders ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,6 +300,18 @@ class Smtp extends Email
|
||||
/* Return */
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate random message-id
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function generateMessageId(): string
|
||||
{
|
||||
$randomString = base_convert( microtime() . '.' . Login::generateRandomString(), 10, 36 );
|
||||
|
||||
return '<' . $randomString . '@' . Url::internal('')->data[ Url::COMPONENT_HOST ] . '>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the SMTP log
|
||||
|
||||
@@ -526,6 +526,11 @@ class GeoLocation
|
||||
* @brief VAT (used in Commerce)
|
||||
*/
|
||||
public ?string $vat = NULL;
|
||||
|
||||
/**
|
||||
* @brief Optional business name used for Commerce
|
||||
*/
|
||||
public ?string $business = null;
|
||||
|
||||
/**
|
||||
* Get Requester Location
|
||||
|
||||
@@ -125,6 +125,7 @@ class Editor extends FormAbstract
|
||||
protected static array $editorRestrictions = [
|
||||
"heading",
|
||||
"heading_1",
|
||||
'tables',
|
||||
"box",
|
||||
"box_color",
|
||||
"float",
|
||||
|
||||
@@ -166,6 +166,12 @@ class Menu
|
||||
*/
|
||||
public function addSeparator(): self
|
||||
{
|
||||
/* Skip if there are no elements yet */
|
||||
if( !count( $this->elements ) )
|
||||
{
|
||||
return $this;
|
||||
}
|
||||
|
||||
$this->elements[] = new Separator;
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ class Response
|
||||
}
|
||||
|
||||
$firstLineBreak = mb_strpos( $content, "\n" );
|
||||
if ( preg_match( "/^HTTP\/(.+?) (\d+?) (.+?)$/", mb_substr( $content, 0, $firstLineBreak ), $matches ) )
|
||||
if ( preg_match( "/^HTTP\/(.+?) (\d+?) (.+?)$/", \substr( $content, 0, $firstLineBreak ), $matches ) )
|
||||
{
|
||||
$this->httpResponseVersion = $matches[1];
|
||||
$this->httpResponseCode = $matches[2];
|
||||
@@ -93,12 +93,12 @@ class Response
|
||||
/* If we have received a 1xx response code, that means it's not the final response */
|
||||
if ( mb_substr( $this->httpResponseCode, 0, 1 ) == 1 OR ( $this->httpResponseCode == 200 AND trim( $this->httpResponseText ) == 'Connection established' ) )
|
||||
{
|
||||
$content = trim( mb_substr( $content, $firstLineBreak ) );
|
||||
$content = trim( \substr( $content, $firstLineBreak ) );
|
||||
}
|
||||
/* Or a 3xx response with another response */
|
||||
elseif ( mb_substr( $this->httpResponseCode, 0, 1 ) == 3 AND preg_match( "/^HTTP\//m", mb_substr( $content, $firstLineBreak ) ) )
|
||||
elseif ( mb_substr( $this->httpResponseCode, 0, 1 ) == 3 AND preg_match( "/^HTTP\//m", \substr( $content, $firstLineBreak ) ) )
|
||||
{
|
||||
$_content = trim( mb_substr( $content, $firstLineBreak ) );
|
||||
$_content = trim( \substr( $content, $firstLineBreak ) );
|
||||
if ( $_content )
|
||||
{
|
||||
$content = $_content;
|
||||
@@ -112,7 +112,7 @@ class Response
|
||||
while ( TRUE );
|
||||
|
||||
/* Split into headers and content */
|
||||
$split = preg_split( "/\r\n\r\n/", mb_substr( $content, $firstLineBreak ), 2 );
|
||||
$split = preg_split( "/\r\n\r\n/", \substr( $content, $firstLineBreak ), 2 );
|
||||
if ( isset( $split[1] ) )
|
||||
{
|
||||
$this->content = $split[1];
|
||||
|
||||
@@ -86,7 +86,7 @@ class Twitter extends OAuth1
|
||||
*/
|
||||
public function buttonColor(): string
|
||||
{
|
||||
return '#1d9bf0';
|
||||
return '#000000';
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ class Twitter extends OAuth1
|
||||
*/
|
||||
public function logoForDeviceInformation(): Url|string|null
|
||||
{
|
||||
return Theme::i()->resource( 'logos/login/Twitter.png', 'core', 'interface' );
|
||||
return Theme::i()->resource( 'logos/login/X.png', 'core', 'interface' );
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+12
-2
@@ -25,6 +25,7 @@ use IPS\Login\Success;
|
||||
use IPS\Member\Device;
|
||||
use IPS\Session\Front;
|
||||
use OutOfRangeException;
|
||||
use UnderflowException;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function function_exists;
|
||||
@@ -176,7 +177,7 @@ class Login
|
||||
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 Handler ) )
|
||||
or ( $this->type === static::LOGIN_REAUTHENTICATE and $method->canProcess( $this->reauthenticateAs ) and $method instanceof Handler )
|
||||
) {
|
||||
$methods[ $k ] = $method;
|
||||
}
|
||||
@@ -536,7 +537,16 @@ class Login
|
||||
$url = Url::internal( "app=core&module=system&controller=lostpass", "front", "lostpassword" );
|
||||
return Member::loggedIn()->language()->addToStack( 'member_email_exists', FALSE, array( 'sprintf' => array( (string) $url ) ) );
|
||||
}
|
||||
|
||||
|
||||
/* Check if someone requested to change to this email */
|
||||
try
|
||||
{
|
||||
$test = Db::i()->select( '*', 'core_validating', [ 'email_chg=? and new_email=?', 1, $email ] )->first();
|
||||
$url = Url::internal( "app=core&module=system&controller=lostpass", "front", "lostpassword" );
|
||||
return Member::loggedIn()->language()->addToStack( 'member_email_exists', FALSE, array( 'sprintf' => array( (string) $url ) ) );
|
||||
}
|
||||
catch( UnderflowException ){}
|
||||
|
||||
/* Check each handler */
|
||||
foreach( static::methods() as $k => $handler )
|
||||
{
|
||||
|
||||
+19
-12
@@ -169,6 +169,11 @@ class Club extends ActiveRecord implements Embeddable
|
||||
* @brief Status: leader
|
||||
*/
|
||||
const STATUS_LEADER = 'leader';
|
||||
|
||||
/**
|
||||
* @brief Not actually a status, but used for member history
|
||||
*/
|
||||
const STATUS_LEFT = 'left';
|
||||
|
||||
/**
|
||||
* @brief [ActiveRecord] Multiton Store
|
||||
@@ -1308,18 +1313,6 @@ class Club extends ActiveRecord implements Embeddable
|
||||
$save['joined'] = time();
|
||||
}
|
||||
|
||||
/* Log to Member History */
|
||||
if ( in_array( $status, array( Club::STATUS_MEMBER, Club::STATUS_BANNED ) ) )
|
||||
{
|
||||
$memberStatus = $this->memberStatus( $member, 2 );
|
||||
|
||||
/* Joining by invite */
|
||||
if ( $memberStatus['status'] == Club::STATUS_INVITED )
|
||||
{
|
||||
$addedBy = Member::load( $memberStatus['invited_by'] );
|
||||
}
|
||||
$member->logHistory( 'core', 'club_membership', array('club_id' => $this->id, 'type' => $status ), $addedBy );
|
||||
}
|
||||
Db::i()->update( 'core_clubs_memberships', $save, array( 'club_id=? AND member_id=?', $this->id, $member->member_id ) );
|
||||
|
||||
$member->rebuildPermissionArray();
|
||||
@@ -1335,6 +1328,17 @@ class Club extends ActiveRecord implements Embeddable
|
||||
}
|
||||
}
|
||||
|
||||
/* Log to Member History */
|
||||
$memberStatus = $this->memberStatus( $member, 2 );
|
||||
if( in_array( $memberStatus['status'], [ Club::STATUS_INVITED, Club::STATUS_INVITED_BYPASSING_PAYMENT, Club::STATUS_BANNED, Club::STATUS_MEMBER, Club::STATUS_DECLINED, Club::STATUS_REQUESTED ] ) )
|
||||
{
|
||||
if ( $memberStatus['status'] == Club::STATUS_INVITED )
|
||||
{
|
||||
$addedBy = Member::load( $memberStatus['invited_by'] );
|
||||
}
|
||||
$member->logHistory( 'core', 'club_membership', array('club_id' => $this->id, 'type' => $status ), $addedBy );
|
||||
}
|
||||
|
||||
$params = [
|
||||
'club' => $this,
|
||||
'member' => $member,
|
||||
@@ -1384,6 +1388,9 @@ class Club extends ActiveRecord implements Embeddable
|
||||
*/
|
||||
public function removeMember( Member $member ) : void
|
||||
{
|
||||
/* Get the member's current status */
|
||||
$memberStatus = $this->memberStatus( $member );
|
||||
|
||||
Db::i()->delete( 'core_clubs_memberships', array( 'club_id=? AND member_id=?', $this->id, $member->member_id ) );
|
||||
|
||||
Webhook::fire( 'club_member_removed', ['club' => $this, 'member' => $member] );
|
||||
|
||||
+106
-1
@@ -34,6 +34,7 @@ use IPS\core\ProfileFields\Api\Field;
|
||||
use IPS\core\ProfileFields\Api\FieldGroup;
|
||||
use IPS\core\Reports\Report;
|
||||
use IPS\Data\Store;
|
||||
use IPS\Db;
|
||||
use IPS\Db\Select;
|
||||
use IPS\Events\Event;
|
||||
use IPS\Helpers\CoverPhoto;
|
||||
@@ -56,6 +57,7 @@ use IPS\Member\UserMenu;
|
||||
use IPS\Patterns\ActiveRecord;
|
||||
use IPS\Patterns\ActiveRecordIterator;
|
||||
use IPS\Platform\Bridge;
|
||||
use IPS\Settings;
|
||||
use IPS\Text\Encrypt;
|
||||
use IPS\Xml\SimpleXML;
|
||||
use OutOfRangeException;
|
||||
@@ -102,6 +104,11 @@ class Member extends ActiveRecord
|
||||
*/
|
||||
const SPAM_CHECK_DORMANT_THRESHOLD = 'P6M';
|
||||
|
||||
/**
|
||||
* @brief Period for login after inactivity notification
|
||||
*/
|
||||
const LOGIN_INACTIVITY_NOTIFICATION = 'P6M';
|
||||
|
||||
/**
|
||||
* @brief Application
|
||||
*/
|
||||
@@ -2987,6 +2994,42 @@ class Member extends ActiveRecord
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the member's email address
|
||||
*
|
||||
* @param string $newEmail
|
||||
* @return void
|
||||
*/
|
||||
public function changeEmail( string $newEmail ) : void
|
||||
{
|
||||
/* Disable syncing */
|
||||
$profileSync = $this->profilesync;
|
||||
if ( isset( $profileSync['email'] ) )
|
||||
{
|
||||
unset( $profileSync['email'] );
|
||||
$this->profilesync = $profileSync;
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/* Change the email */
|
||||
$oldEmail = $this->email;
|
||||
$this->email = $newEmail;
|
||||
$this->save();
|
||||
foreach ( Login::methods() as $method )
|
||||
{
|
||||
try
|
||||
{
|
||||
$method->changeEmail( $this, $oldEmail, $newEmail );
|
||||
}
|
||||
catch( BadMethodCallException $e ){}
|
||||
}
|
||||
$this->logHistory( 'core', 'email_change', array( 'old' => $oldEmail, 'new' => $newEmail, 'by' => 'manual' ) );
|
||||
|
||||
Db::i()->delete( 'core_validating', [ 'member_id=? and email_chg=?', $this->member_id, 1 ] );
|
||||
|
||||
Event::fire( 'onEmailChange', $this, array( $newEmail, $oldEmail ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Cached notifications configuration
|
||||
* @note This property is public so it can be populated en-masse in areas that may benefit from doing so
|
||||
@@ -4965,7 +5008,7 @@ class Member extends ActiveRecord
|
||||
/* Send email for validation */
|
||||
if ( $validationType != 'admin' )
|
||||
{
|
||||
Email::buildFromTemplate( 'core', 'registration_validate', array( $this, $vid, $plainSecurityKey ), Email::TYPE_TRANSACTIONAL )->send( $this );
|
||||
Email::buildFromTemplate( 'core', 'registration_validate', array( $this, $vid, $plainSecurityKey, '' ), Email::TYPE_TRANSACTIONAL )->send( $this );
|
||||
}
|
||||
|
||||
/* Update core_post_before_registering */
|
||||
@@ -6570,6 +6613,8 @@ class Member extends ActiveRecord
|
||||
*/
|
||||
public function recordLogin() : void
|
||||
{
|
||||
$this->sendLoginAfterInactivityNotification();
|
||||
|
||||
try
|
||||
{
|
||||
Db::i()->insert( 'core_members_logins', [ 'member_id' => $this->member_id, 'member_timestamp' => time(), 'member_date' => date( 'Y-m-d', DateTime::ts( time() )->getTimestamp() ) ] );
|
||||
@@ -6577,6 +6622,66 @@ class Member extends ActiveRecord
|
||||
catch( Exception $e ) { }
|
||||
}
|
||||
|
||||
/**
|
||||
* If required, send an email notification when signing in after a period of inactivity.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function sendLoginAfterInactivityNotification(): bool
|
||||
{
|
||||
if( !Settings::i()->login_after_inactivity_notification )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
$lastLogin = Db::i()->select( 'member_timestamp', 'core_members_logins', [ 'member_id=?', $this->member_id ], 'member_timestamp DESC', 1 )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
/* This is the first login, or they haven't logged in since upgrading from < 106100 */
|
||||
try
|
||||
{
|
||||
$lastLogin = Db::i()->select( 'MIN(upgrade_date)', 'core_upgrade_history', [ 'upgrade_app=? AND upgrade_version_id>=?', 'core', 106100 ] )->first();
|
||||
}
|
||||
catch( \UnderflowException $e )
|
||||
{
|
||||
/* This could be a new install, or IN_DEV where upgrade history isn't available */
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$lastLoginObj = DateTime::ts( $lastLogin );
|
||||
|
||||
/* Likely to be the first login */
|
||||
if( $this->joined > $lastLoginObj )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* If this login is within the period, don't send an email */
|
||||
if( $lastLoginObj > DateTime::create()->sub( new \DateInterval( static::LOGIN_INACTIVITY_NOTIFICATION ) ) )
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Device data */
|
||||
$device = Device::loadOrCreate( $this, false );
|
||||
try
|
||||
{
|
||||
$location = GeoLocation::getRequesterLocation();
|
||||
}
|
||||
catch ( \Exception $e )
|
||||
{
|
||||
$location = NULL;
|
||||
}
|
||||
|
||||
Email::buildFromTemplate( 'core', 'loginAfterInactivity', array( $this, $device, $location ), Email::TYPE_TRANSACTIONAL )->send( $this );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook filters
|
||||
*
|
||||
|
||||
@@ -2241,7 +2241,8 @@ abstract class Model extends ActiveRecord
|
||||
$groupsToCheck = $member->groups;
|
||||
|
||||
/* Work out which to use taking Post Before Registering into consideration and return */
|
||||
$considerPostBeforeRegistering = ( $considerPostBeforeRegistering and Settings::i()->post_before_registering and Login::registrationType() != 'disabled' );
|
||||
$considerPostBeforeRegistering = ( $considerPostBeforeRegistering and Settings::i()->post_before_registering
|
||||
and Login::registrationType() != 'disabled' and Settings::i()->bot_antispam_type !== 'none' );
|
||||
if ( !$member->member_id and $considerPostBeforeRegistering )
|
||||
{
|
||||
$groupsToCheck[] = Settings::i()->member_group;
|
||||
@@ -2651,7 +2652,8 @@ abstract class Model extends ActiveRecord
|
||||
$relevantPermissionArray = array_filter( explode( ',', $relevantPermissionString ) );
|
||||
|
||||
/* Work out which to use taking Post Before Registering into consideration and return */
|
||||
$considerPostBeforeRegistering = ( $considerPostBeforeRegistering and Settings::i()->post_before_registering and Login::registrationType() != 'disabled' );
|
||||
$considerPostBeforeRegistering = ( $considerPostBeforeRegistering and Settings::i()->post_before_registering
|
||||
and Login::registrationType() != 'disabled' and Settings::i()->bot_antispam_type !== 'none' );
|
||||
if( $member instanceof Group )
|
||||
{
|
||||
if ( $considerPostBeforeRegistering and $member->g_id == Settings::i()->guest_group )
|
||||
@@ -3669,7 +3671,11 @@ abstract class Model extends ActiveRecord
|
||||
*/
|
||||
public function saveForm( array $values ): mixed
|
||||
{
|
||||
$values = UiExtension::i()->run( $this, 'processForm', [ $values ] );
|
||||
/* Loop through each extension and update the values */
|
||||
foreach( UiExtension::i()->getObjectExtensions( $this ) as $extension )
|
||||
{
|
||||
$values = $extension->processForm( $this, $values );
|
||||
}
|
||||
|
||||
foreach ( $values as $k => $v )
|
||||
{
|
||||
|
||||
+14
-12
@@ -1852,28 +1852,30 @@ class Output
|
||||
|
||||
foreach( $this->breadcrumb as $breadcrumb )
|
||||
{
|
||||
$crumb = [
|
||||
'@type' => "ListItem",
|
||||
'position' => $position,
|
||||
'item' => [
|
||||
'name' => $breadcrumb[1],
|
||||
]
|
||||
];
|
||||
|
||||
if( $breadcrumb[0] )
|
||||
{
|
||||
$elements[] = array(
|
||||
'@type' => "ListItem",
|
||||
'position' => $position,
|
||||
'item' => array(
|
||||
'@id' => (string) $breadcrumb[0],
|
||||
'name' => $breadcrumb[1],
|
||||
)
|
||||
);
|
||||
|
||||
$position++;
|
||||
$crumb['item']['@id'] = (string) $breadcrumb[0];
|
||||
}
|
||||
|
||||
$elements[] = $crumb;
|
||||
$position++;
|
||||
}
|
||||
|
||||
if( count( $elements ) )
|
||||
{
|
||||
$jsonLd['breadcrumbs'] = array(
|
||||
$jsonLd['breadcrumbs'] = [
|
||||
'@context' => "https://schema.org",
|
||||
'@type' => "BreadcrumbList",
|
||||
'itemListElement' => $elements,
|
||||
);
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -92,66 +92,20 @@ class UiExtension extends Singleton
|
||||
*/
|
||||
public function run( Content|Model|string $object, string $method, ?array $payload=array() ) : array
|
||||
{
|
||||
$className = is_string( $object ) ? $object : get_class( $object );
|
||||
$baseClass = null;
|
||||
switch( $className )
|
||||
{
|
||||
case '':
|
||||
default:
|
||||
return [];
|
||||
|
||||
case is_subclass_of( $className, Model::class ):
|
||||
|
||||
/* If this node class cannot be extended, just return and do nothing */
|
||||
if( !$className::$canBeExtended )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = 'Node';
|
||||
$baseClass = Model::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Item::class ):
|
||||
$type = 'Item';
|
||||
$baseClass = Content\Item::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Review::class ):
|
||||
$type = 'Review';
|
||||
$baseClass = Content\Review::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Comment::class ):
|
||||
$type = 'Comment';
|
||||
$baseClass = Content\Comment::class;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
|
||||
/* If we are working with an object (Item/Comment/Node), put it first in the parameter list */
|
||||
/* If we are working with an object (Item/Comment/Node), put it first in the parameter list */
|
||||
$params = is_array( $payload ) ? array_values( $payload ) : array();
|
||||
array_unshift( $params, is_string( $object ) ? null : $object );
|
||||
|
||||
foreach( $this->extensions( $type ) as $extension )
|
||||
foreach( $this->getObjectExtensions( $object ) as $extension )
|
||||
{
|
||||
/* Use subclass to account for classes in Pages */
|
||||
if( isset( $extension::$class ) AND $className != $extension::$class AND !is_subclass_of( $className, $extension::$class ) )
|
||||
if ( $response = $extension->$method( ...$params ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Block extension of base classes */
|
||||
if( $baseClass !== null AND $extension::$class == $baseClass )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if( $response = $extension->$method( ...$params ) )
|
||||
{
|
||||
if( is_array( $response ) )
|
||||
if ( is_array( $response ) )
|
||||
{
|
||||
$return = array_merge( $return, $response );
|
||||
}
|
||||
else
|
||||
} else
|
||||
{
|
||||
$return[] = $response;
|
||||
}
|
||||
@@ -160,4 +114,65 @@ class UiExtension extends Singleton
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all available extensions for this object
|
||||
*
|
||||
* @param Content|Model|string $object
|
||||
* @return array
|
||||
*/
|
||||
public function getObjectExtensions( Content|Model|string $object ) : array
|
||||
{
|
||||
$className = is_string( $object ) ? $object : get_class( $object );
|
||||
$baseClass = null;
|
||||
switch ( $className )
|
||||
{
|
||||
case '':
|
||||
default:
|
||||
return [];
|
||||
|
||||
case is_subclass_of( $className, Model::class ):
|
||||
|
||||
/* If this node class cannot be extended, just return and do nothing */
|
||||
if ( !$className::$canBeExtended )
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
$type = 'Node';
|
||||
$baseClass = Model::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Item::class ):
|
||||
$type = 'Item';
|
||||
$baseClass = Content\Item::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Review::class ):
|
||||
$type = 'Review';
|
||||
$baseClass = Content\Review::class;
|
||||
break;
|
||||
case is_subclass_of( $className, Content\Comment::class ):
|
||||
$type = 'Comment';
|
||||
$baseClass = Content\Comment::class;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = [];
|
||||
foreach ( $this->extensions( $type ) as $extension )
|
||||
{
|
||||
/* Use subclass to account for classes in Pages */
|
||||
if ( isset( $extension::$class ) and $className != $extension::$class and !is_subclass_of( $className, $extension::$class ) )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* Block extension of base classes */
|
||||
if ( $baseClass !== null and $extension::$class == $baseClass )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[] = $extension;
|
||||
}
|
||||
return $return;
|
||||
}
|
||||
}
|
||||
@@ -278,7 +278,7 @@ class Redis
|
||||
{
|
||||
if ( method_exists( 'Redis', $method ) )
|
||||
{
|
||||
$type = ( stristr( $method, 'get' ) or stristr( $method, 'RevRange' ) ) ? 'read' : 'write';
|
||||
$type = ( stristr( $method, 'get' ) or stristr( $method, 'RevRange' ) or $method === 'lRange' ) ? 'read' : 'write';
|
||||
$return = $this->connection( $type )->$method( ...$args );
|
||||
|
||||
if ( $type === 'write' and count( $args ) )
|
||||
|
||||
@@ -325,6 +325,11 @@ class Redis extends Store
|
||||
* Redis key
|
||||
*/
|
||||
protected int $fetchAttempt = 0;
|
||||
|
||||
/**
|
||||
* @var array|null cache online user data after fetching it
|
||||
*/
|
||||
protected ?array $onlineUsers = null;
|
||||
|
||||
/**
|
||||
* Fetch all online users (but not spiders)
|
||||
@@ -338,19 +343,20 @@ class Redis extends Store
|
||||
*/
|
||||
public function getOnlineUsers( int $flags=0, string $sort='desc', array $limit=NULL, int $memberGroup=NULL, bool $showAnonymous=FALSE ): array|int
|
||||
{
|
||||
try
|
||||
if( $this->onlineUsers === NULL )
|
||||
{
|
||||
$results = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
|
||||
}
|
||||
catch ( RedisException $e )
|
||||
{
|
||||
/* Something went wrong, so reset the key to force a new file */
|
||||
static::resetKey();
|
||||
|
||||
$results = NULL;
|
||||
try
|
||||
{
|
||||
$this->onlineUsers = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
|
||||
}
|
||||
catch ( RedisException $e )
|
||||
{
|
||||
/* Something went wrong, so reset the key to force a new file */
|
||||
static::resetKey();
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $results )
|
||||
if ( !$this->onlineUsers )
|
||||
{
|
||||
/* Ensure file is deleted */
|
||||
try
|
||||
@@ -383,10 +389,10 @@ class Redis extends Store
|
||||
|
||||
try
|
||||
{
|
||||
/* Force expiration in 30 seconds to prevent stale caches hanging around */
|
||||
RedisClass::i()->expire( static::_key( 'session_onlinelist' ), 30 );
|
||||
|
||||
$results = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
|
||||
/* Force expiration in 60 seconds to prevent stale caches hanging around */
|
||||
RedisClass::i()->expire( static::_key( 'session_onlinelist' ), 60 );
|
||||
|
||||
$this->onlineUsers = RedisClass::i()->lRange( static::_key( 'session_onlinelist' ), 0, -1 );
|
||||
}
|
||||
catch ( RedisException $e )
|
||||
{
|
||||
@@ -402,7 +408,7 @@ class Redis extends Store
|
||||
}
|
||||
else
|
||||
{
|
||||
$results = array();
|
||||
$this->onlineUsers = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -414,7 +420,7 @@ class Redis extends Store
|
||||
$return = array();
|
||||
$i = 0;
|
||||
|
||||
while( $i < count( $results ) )
|
||||
while( $i < count( $this->onlineUsers ) )
|
||||
{
|
||||
$fields = array();
|
||||
$data = NULL;
|
||||
@@ -424,7 +430,7 @@ class Redis extends Store
|
||||
{
|
||||
try
|
||||
{
|
||||
$data = RedisClass::i()->decode( $results[ $i++ ] );
|
||||
$data = RedisClass::i()->decode( $this->onlineUsers[ $i++ ] );
|
||||
}
|
||||
catch( RedisException $e )
|
||||
{
|
||||
@@ -434,11 +440,11 @@ class Redis extends Store
|
||||
/* login_type must be cast as an integer or else anonymous state can be lost when adjustSessions() runs */
|
||||
elseif( $field === 'login_type' )
|
||||
{
|
||||
$fields[ $field ] = (int) $results[ $i++ ];
|
||||
$fields[ $field ] = (int) $this->onlineUsers[ $i++ ];
|
||||
}
|
||||
else
|
||||
{
|
||||
$fields[ $field ] = $results[ $i++ ];
|
||||
$fields[ $field ] = $this->onlineUsers[ $i++ ];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+90
-1
@@ -1051,6 +1051,9 @@ class Parser
|
||||
$return[] = 'ipsEmbedded_og__' . $k;
|
||||
}
|
||||
|
||||
/* Tables */
|
||||
$return[] = "ipsRichText__table-wrapper";
|
||||
|
||||
/* Custom */
|
||||
foreach( Application::enabledApplications() as $app )
|
||||
{
|
||||
@@ -1280,6 +1283,90 @@ class Parser
|
||||
/* Note what $parent is */
|
||||
$originalParent = $parent;
|
||||
|
||||
/* Acronym? */
|
||||
if ( array_key_exists( $section, $this->caseSensitiveAcronyms ) and !in_array( $this->caseSensitiveAcronyms[ $section ], $this->openAbbrTags ) )
|
||||
{
|
||||
switch( $this->caseSensitiveAcronyms[ $section ]['a_type'] )
|
||||
{
|
||||
case 'acronym':
|
||||
$parent = $parent->appendChild( new DOMElement( 'abbr' ) );
|
||||
$parent->setAttribute( 'title', $this->caseSensitiveAcronyms[ $section ]['a_long'] );
|
||||
break;
|
||||
case 'link':
|
||||
$replace = ( $parent->tagName != 'a' );
|
||||
|
||||
$parentNode = $parent;
|
||||
while( ( $parentNode = $parentNode->parentNode ) !== NULL )
|
||||
{
|
||||
if( $parentNode instanceof DOMElement AND $parentNode->tagName == 'a' )
|
||||
{
|
||||
$replace = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( $replace )
|
||||
{
|
||||
$parent = $parent->appendChild( new DOMElement( 'a' ) );
|
||||
$parent->setAttribute( 'href', $this->caseSensitiveAcronyms[ $section ]['a_long'] );
|
||||
|
||||
try
|
||||
{
|
||||
$rels = $this->_getRelAttributes( Url::createFromString( $this->caseSensitiveAcronyms[ $section ]['a_long'] ) );
|
||||
}
|
||||
catch(Url\Exception $e )
|
||||
{
|
||||
$rels = array();
|
||||
}
|
||||
|
||||
/* Add rels */
|
||||
$parent->setAttribute( 'rel', implode( ' ', $rels ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
elseif ( array_key_exists( mb_strtolower( $section ), $this->caseInsensitiveAcronyms ) and !in_array( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ], $this->openAbbrTags ) )
|
||||
{
|
||||
switch( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_type'] )
|
||||
{
|
||||
case 'acronym':
|
||||
$parent = $parent->appendChild( new DOMElement( 'abbr' ) );
|
||||
$parent->setAttribute( 'title', $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] );
|
||||
break;
|
||||
case 'link':
|
||||
$replace = ( $parent->tagName != 'a' );
|
||||
|
||||
$parentNode = $parent;
|
||||
while( ( $parentNode = $parentNode->parentNode ) !== NULL )
|
||||
{
|
||||
if( $parentNode instanceof DOMElement AND $parentNode->tagName == 'a' )
|
||||
{
|
||||
$replace = FALSE;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if( $replace )
|
||||
{
|
||||
$parent = $parent->appendChild( new DOMElement( 'a' ) );
|
||||
$parent->setAttribute( 'href', $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] );
|
||||
|
||||
try
|
||||
{
|
||||
$rels = $this->_getRelAttributes( Url::createFromString( $this->caseInsensitiveAcronyms[ mb_strtolower( $section ) ]['a_long'] ) );
|
||||
}
|
||||
catch(Url\Exception $e )
|
||||
{
|
||||
$rels = array();
|
||||
}
|
||||
|
||||
/* Add rels */
|
||||
$parent->setAttribute( 'rel', implode( ' ', $rels ) );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Emoji? */
|
||||
if ( ( !$originalParent->getAttribute('class') or !in_array( 'ipsEmoji', explode( ' ', $originalParent->getAttribute('class') ) ) ) and preg_match( '/' . static::getEmojiRegex() . '/u', $section ) )
|
||||
{
|
||||
@@ -1641,7 +1728,8 @@ class Parser
|
||||
}
|
||||
}
|
||||
|
||||
$element->setAttribute( 'sandbox', 'allow-scripts' );
|
||||
$element->setAttribute( 'sandbox', 'allow-scripts allow-same-origin' );
|
||||
$element->setAttribute( 'allowfullscreen', '' );
|
||||
// the width and height need to be set in the style attribute
|
||||
$style = "";
|
||||
foreach ( [ 'width', 'height' ] as $attr )
|
||||
@@ -2322,6 +2410,7 @@ class Parser
|
||||
'tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
|
||||
'm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
|
||||
'vm.tiktok.com' => array( 'https://www.tiktok.com/oembed', static::EMBED_VIDEO ),
|
||||
'bsky.app' => [ 'https://embed.bsky.app/oembed', static::EMBED_TWEET ]
|
||||
);
|
||||
|
||||
/* Can we support Facebook and Instagram oembeds? */
|
||||
|
||||
@@ -3889,7 +3889,7 @@ EOF;
|
||||
{
|
||||
$content = $content . "\n<p class='ipsCopyright'>
|
||||
<span class='ipsCopyright__user'>{lang=\"copyright_line_value\"}</span>
|
||||
{{if !\$licenseData = \IPS\IPS::licenseKey() or !isset(\$licenseData['products']['copyright']) or !\$licenseData['products']['copyright']}}<a rel='nofollow' title='Invision Community' href='https://www.invisioncommunity.com/'>Powered by Invision Community</a>{{endif}}
|
||||
{{if !\$licenseData = \IPS\IPS::licenseKey() or !isset(\$licenseData['products']['copyright']) or !\$licenseData['products']['copyright']}}<a rel='nofollow' title='Invision Community' href='https://www.invisioncommunity.com/'>Powered by <span translate='no'>Invision Community</span></a>{{endif}}
|
||||
</p>";
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user