gnu-social/plugins/ActivityPub/Util/Type/Util.php

567 lines
13 KiB
PHP

<?php
/*
* This file is part of the ActivityPhp package.
*
* Copyright (c) landrok at github.com/landrok
*
* For the full copyright and license information, please see
* <https://github.com/landrok/activitypub/blob/master/LICENSE>.
*/
namespace Plugin\ActivityPub\Util\Type;
use DateInterval;
use DateTime;
use Exception;
use Plugin\ActivityPub\Util\Type;
/**
* \Plugin\ActivityPub\Util\Type\Util is an abstract class for
* supporting validators checks & transformations.
*/
abstract class Util
{
/**
* Allowed units
*
* @var array<string>
*/
protected static array $units = [
'cm', 'feet', 'inches', 'km', 'm', 'miles',
];
/**
* Transform an array into an ActivityStreams type
*
* @param array $item
*
* @throws Exception
*
* @return AbstractObject|array An ActivityStreams
* type or given array if type key is not defined.
*/
public static function arrayToType(array $item): AbstractObject|array
{
// Maybe an array representing an AS object
// It must have a type key
if (isset($item['type'])) {
return Type::create($item['type'], $item);
}
return $item;
}
/**
* Validate an URL
*
* @param mixed $value
*
* @return bool
*/
public static function validateUrl(mixed $value): bool
{
return is_string($value)
&& filter_var($value, FILTER_VALIDATE_URL) !== false
&& in_array(
parse_url($value, PHP_URL_SCHEME),
['http', 'https', 'magnet']
);
}
/**
* Validate a magnet link
*
* @param mixed $value
*
* @return bool
*
* @see https://en.wikipedia.org/wiki/Magnet_URI_scheme
*
* @todo Make a better validation as xs is not the only parameter
*/
public static function validateMagnet(mixed $value): bool
{
return is_string($value)
&& strlen($value) < 262144
&& preg_match(
'#^magnet:\?xs=(https?)://.*$#iu',
urldecode($value)
);
}
/**
* Validate an OStatus tag string
*
* @param mixed $value
*
* @return bool
*/
public static function validateOstatusTag(mixed $value): bool
{
return is_string($value)
&& strlen($value) < 262144
&& preg_match(
'#^tag:([\w\-\.]+),([\d]{4}-[\d]{2}-[\d]{2}):([\w])+Id=([\d]+):objectType=([\w]+)#iu',
$value
);
}
/**
* Validate a rel attribute value.
*
* @see https://tools.ietf.org/html/rfc5988
*
* @param string $value
*
* @return bool
*/
public static function validateRel(string $value): bool
{
return is_string($value)
&& preg_match("/^[^\\s\r\n\\,]+\\z/i", $value);
}
/**
* Validate a non negative integer.
*
* @param int $value
*
* @return bool
*/
public static function validateNonNegativeInteger(int $value): bool
{
return is_int($value)
&& $value >= 0;
}
/**
* Validate a non negative number.
*
* @param float|int $value
*
* @return bool
*/
public static function validateNonNegativeNumber(float|int $value): bool
{
return is_numeric($value)
&& $value >= 0;
}
/**
* Validate units format.
*
* @param string $value
*
* @return bool
*/
public static function validateUnits(string $value): bool
{
if (is_string($value)) {
if (in_array($value, self::$units)
|| self::validateUrl($value)
) {
return true;
}
}
return false;
}
/**
* Validate an Object type
*
* @param object $item
*
* @throws Exception
*
* @return bool
*/
public static function validateObject(object $item): bool
{
return self::hasProperties($item, ['type'])
&& is_string($item->type)
&& $item->type === 'Object';
}
/**
* Decode a JSON string
*
* @throws Exception if JSON decoding process has failed
*/
public static function decodeJson(string $value): array
{
$json = json_decode($value, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new Exception(
'JSON decoding failed for string: ' . $value
);
}
return $json;
}
/**
* Checks that all properties exist for a stdClass
*
* @param object $item
* @param array $properties
* @param bool $strict If true throws an \Exception,
* otherwise, returns false
*
* @throws Exception if a property is not set
*
* @return bool
*/
public static function hasProperties(
object $item,
array $properties,
bool $strict = false
): bool {
foreach ($properties as $property) {
if (!property_exists($item, $property)) {
if ($strict) {
throw new Exception(
sprintf(
'Attribute "%s" MUST be set for item: %s',
$property,
print_r($item, true)
)
);
}
return false;
}
}
return true;
}
/**
* Validate a reference with a Link or an Object with a URL
*
* @param object $item
*
* @throws Exception
*
* @return bool
*/
public static function isLinkOrUrlObject(object $item): bool
{
self::hasProperties($item, ['type'], true);
// Validate Link type
if ($item->type === 'Link') {
return self::validateLink($item);
}
// Validate Object type
self::hasProperties($item, ['url'], true);
return self::validateUrl($item->url);
}
/**
* Validate a reference as Link
*
* @param array|object $item
*
* @throws Exception
*
* @return bool
*/
public static function validateLink(object|array $item): bool
{
if (is_array($item)) {
$item = (object) $item;
}
if (!is_object($item)) {
return false;
}
self::hasProperties($item, ['type'], true);
// Validate Link type
if ($item->type !== 'Link') {
return false;
}
// Validate Object type
self::hasProperties($item, ['href'], true);
return self::validateUrl($item->href)
|| self::validateMagnet($item->href);
}
/**
* Validate a datetime
*
* @param mixed $value
*/
public static function validateDatetime($value): bool
{
if (!is_string($value)
|| !preg_match(
'/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(.*)$/',
$value
)
) {
return false;
}
try {
$dt = new DateTime($value);
return true;
} catch (Exception $e) {
return false;
}
}
/**
* Check that container class is a subclass of a given class
*
* @param object $container
* @param array|string $classes
* @param bool $strict If true, throws an exception
*
* @throws Exception
*
* @return bool
*/
public static function subclassOf(object $container, array|string $classes, bool $strict = false): bool
{
if (!is_array($classes)) {
$classes = [$classes];
}
foreach ($classes as $class) {
if (get_class($container) === $class
|| is_subclass_of($container, $class)
) {
return true;
}
}
if ($strict) {
throw new Exception(
sprintf(
'Class "%s" MUST be a subclass of "%s"',
get_class($container),
implode(', ', $classes)
)
);
}
return false;
}
/**
* Checks that a numeric value is part of a range.
* If a minimal value is null, value has to be inferior to max value
* If a maximum value is null, value has to be superior to min value
*
* @param float|int $value
* @param null|float|int $min
* @param null|float|int $max
*
* @return bool
*/
public static function between(float|int $value, float|int|null $min, float|int|null $max): bool
{
if (!is_numeric($value)) {
return false;
}
return match (true) {
is_null($min) && is_null($max) => false,
is_null($min) => $value <= $max,
is_null($max) => $value >= $min,
default => $value >= $min
&& $value <= $max,
};
}
/**
* Check that a given string is a valid XML Schema xsd:duration
*
* @param string $duration
* @param bool $strict If true, throws an exception
*
* @throws Exception
*
* @return bool
*/
public static function isDuration(string $duration, bool $strict = false): bool
{
try {
new DateInterval($duration);
return true;
} catch (Exception $e) {
if ($strict) {
throw new Exception(
sprintf(
'Duration "%s" MUST respect xsd:duration',
$duration
)
);
}
}
return false;
}
/**
* Checks that it's an object type
*
* @param object $item
*
* @return bool
*/
public static function isObjectType(object $item): bool
{
return TypeResolver::isScope($item);
}
/**
* Checks that it's an actor type
*
* @param object $item
*
* @return bool
*/
public static function isActorType(object $item): bool
{
return TypeResolver::isScope($item, 'actor');
}
/**
* Validate an object type with type attribute
*
* @param object $item
* @param string $type An expected type
*
* @return bool
*/
public static function isType(object $item, string $type): bool
{
// Validate that container is a certain type
if (!is_object($item)) {
return false;
}
if (property_exists($item, 'type')
&& is_string($item->type)
&& $item->type === $type
) {
return true;
}
return false;
}
/**
* Validate a BCP 47 language value
*
* @param string $value
*
* @return bool
*/
public static function validateBcp47(string $value): bool
{
return is_string($value)
&& preg_match(
'/^(((en-GB-oed|i-ami|i-bnn|i-default|i-enochian|i-hak|i-klingon|i-lux|i-mingo|i-navajo|i-pwn|i-tao|i-tay|i-tsu|sgn-BE-FR|sgn-BE-NL|sgn-CH-DE)|(art-lojban|cel-gaulish|no-bok|no-nyn|zh-guoyu|zh-hakka|zh-min|zh-min-nan|zh-xiang))|((([A-Za-z]{2,3}(-([A-Za-z]{3}(-[A-Za-z]{3}){0,2}))?)|[A-Za-z]{4}|[A-Za-z]{5,8})(-([A-Za-z]{4}))?(-([A-Za-z]{2}|[0-9]{3}))?(-([A-Za-z0-9]{5,8}|[0-9][A-Za-z0-9]{3}))*(-([0-9A-WY-Za-wy-z](-[A-Za-z0-9]{2,8})+))*(-(x(-[A-Za-z0-9]{1,8})+))?)|(x(-[A-Za-z0-9]{1,8})+))$/',
$value
);
}
/**
* Validate a plain text value
*
* @param string $value
*
* @return bool
*/
public static function validatePlainText(string $value): bool
{
return is_string($value)
&& preg_match(
'/^([^<]+)$/',
$value
);
}
/**
* Validate mediaType format
*
* @param string $value
*
* @return bool
*/
public static function validateMediaType(string $value): bool
{
return is_string($value)
&& preg_match(
'#^(([\w]+[\w\-]+[\w+])/(([\w]+[\w\-\.\+]+[\w]+)|(\*));?)+$#',
$value
);
}
/**
* Validate a Collection type
*
* @param object $item
*
* @throws Exception
*
* @return bool
*/
public static function validateCollection(object $item): bool
{
if (is_scalar($item)) {
return false;
}
if (!is_object($item)) {
$item = (object) $item;
}
self::hasProperties(
$item,
[/*totalItems', 'current', 'first', 'last', */ 'items'],
true
);
return true;
}
/**
* Validate a CollectionPage type
*
* @param object $item
*
* @throws Exception
*
* @return bool
*/
public static function validateCollectionPage(object $item): bool
{
// Must be a Collection
if (!self::validateCollection($item)) {
return false;
}
self::hasProperties(
$item,
['partOf'/*, 'next', 'prev'*/],
true
);
return true;
}
}