ldc-crm-client/src/Utils.php
Jérémy Munsch f36447854b
API V3 UPDATE
This removes api authentication with token in favor of signature method.
Refactored code and clean up. It add more unit test plus code coverage
in test/coverage.

- Added   : mardown documentation.
- Changed : model Stats::getQuestionStats  to pass question as route parameter
- Changed : now requests need to have a signature generated with a private key (crm password) using hmac sha256 wrapping url, method and parameters
- Changed : now timestamp will timeout if request is slower than "timestampTimeout", this means crm will reject the request
- Changed : throw ValidationException s instead of TypeErrors
- Changed : timestamp has to be send in request with a DateTime format, it also has to a UTC value
- Fixed   : Change WPCRM dynamic session key, this could prevent spoofing session in some cases.
- Fixed   : closure validation
- Fixed   : model mutator not catching reflectionException
- Fixed   : really send json request using Guzzle internal lib methods.
- Removed : Api AUTH system with api token
- Removed : ApiError::needAuth
- Removed : composer vlucas/dotenv, .env file is not handled any more.
- Removed : LoginException
- Removed : memcached dependency
- Removed : need of memcached
- Removed : unused service provider
- Removed : WpCrm::forceLogin
- Removed : WpCrm::sendArcadiaMailDefault
- Removed : WpCrm::sendArcadiaMailGame
- Updated : PHPCS rules, phpcs fixes and refactored code
2021-12-31 17:00:55 +01:00

694 lines
21 KiB
PHP

<?php
namespace LDC\LDCCRM;
use LDC\LDCCRM\Enums\HttpMethods;
use LDC\LDCCRM\Enums\LogLevel;
use LDC\LDCCRM\Exceptions\AccessError;
use LDC\LDCCRM\Exceptions\SkipValidationException;
use LDC\LDCCRM\Exceptions\ValidationException;
use LDC\LDCCRM\Transport\Request;
use Respect\Validation\Validator;
/**
* This is the LDCCRM utility class
*
* @category Utility
* @uses \LDC\LDCCRM\Enums\HttpMethods
* @uses \LDC\LDCCRM\Transport\Request
*
* @package LDCCrmClient
* @subpackage utilities
*/
class Utils
{
/**
* Package infos
*
* @var array
*/
private static $packageInfos;
/**
* Loaded translations
*
* @var array
*/
private static $translations = [];
/**
* Create a new Validator instance.
*
* @param array $data
* @param array $rules
* @return closure
* @throws \ErrorException If an unknown validation rule is used.
*/
public static function makeValidator(
array $data,
array $rules
): object {
foreach ($rules as $attribute => $attrRules) {
$validators = [];
if (\is_string($attrRules)) {
$attrRules = \explode('|', $attrRules);
}
foreach ($attrRules as $rule) {
$validators[] = self::makeValidatorFromRule($rule, $data, $attribute);
} //end foreach
} //end foreach
return function () use ($validators) {
foreach ($validators as $validator) {
try {
if (!$validator['validate']()) {
throw new ValidationException($validator['error']);
}
} catch (SkipValidationException $e) {
break;
}
}
};
}
/**
* Generate a validator
*
* @param mixed $rule
* @param array $data
* @param string $attribute
* @return array
* @throws \ErrorException If a rule is unknown.
*/
private static function makeValidatorFromRule(
$rule,
array $data,
string $attribute
): array {
$rParams = [];
$closure = null;
if (\is_callable($rule) and !is_string($rule)) {
/** @var \Closure $closure */
$closure = $rule;
$rule = '#closure#';
} elseif (\strpos($rule, ':') !== false) {
$rParams = \explode(':', $rule, 2);
$rule = \array_shift($rParams);
}
switch (true) {
case in_array($rule, [
'nullable', 'sometimes',
'int', 'bool', 'uuid', 'string',
'date', 'email', 'array',
'mixedCase', 'containDigit', 'containSymbol',
]):
return self::{sprintf('validation%s', \ucfirst($rule))}(
$attribute,
$data[$attribute] ?? null
);
case in_array($rule, ['min', 'max']):
return self::{sprintf('validation%s', \ucfirst($rule))}(
$attribute,
$data[$attribute],
$rParams
);
case in_array($rule, ['required', 'sometimes']):
return self::{sprintf('validation%s', \ucfirst($rule))}(
$attribute,
$data,
);
case $rule === 'date_format':
return self::validationDateFormat($attribute, $data[$attribute], $rParams);
case $rule === '#closure#':
/** @var \Closure $rule */
return self::validationClosure($attribute, $data[$attribute], $closure);
default:
throw new \ErrorException("Unknown validation rule $rule " . implode(': ', $rParams));
} //end switch
}
// * VALIDATION RULES
/**
* Make Required validator
*
* @param string $attribute
* @param array $data
* @return array
*/
private static function validationRequired(string $attribute, array $data): array
{
return [
'type' => 'required',
'error' => sprintf(
'%s est obligatoire',
self::translate($attribute)
),
'validate' => function () use ($attribute, $data) {
return isset($data[$attribute]);
}
];
}
/**
* Make Nullable validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationNullable(string $attribute, $value): array
{
return [
'type' => 'nullable',
'error' => sprintf(
'%s peut être égale à nul',
self::translate($attribute)
),
'validate' => function () use ($value) {
if (is_null($value)) {
throw new SkipValidationException();
}
return true;
}
];
}
/**
* Make Sometimes validator
*
* @param string $attribute
* @param mixed $data
* @return array
*/
private static function validationSometimes(string $attribute, $data): array
{
return [
'type' => 'sometimes',
'error' => sprintf(
'%s est falcutatif',
self::translate($attribute)
),
'validate' => function () use ($data) {
if (empty($data)) {
throw new SkipValidationException();
}
return true;
}
];
}
/**
* Make Int validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationInt(string $attribute, $value): array
{
return [
'type' => 'int',
'error' => sprintf(
'%s doit être un entier',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \is_int($value);
}
];
}
/**
* Make String validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationString(string $attribute, $value): array
{
return [
'type' => 'string',
'error' => sprintf(
'%s doit être une chaine de charactères',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \is_string($value);
}
];
}
/**
* Make Bool validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationBool(string $attribute, $value): array
{
return [
'type' => 'bool',
'error' => sprintf(
'%s doit être un booléen',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \is_bool($value) or $value === 1 or $value === 0;
}
];
}
/**
* Make Min validator
*
* @param string $attribute
* @param mixed $value
* @param array $params
* @return array
*/
private static function validationMin(string $attribute, $value, array $params): array
{
return [
'type' => 'min',
'error' => sprintf(
'%s doit avoir au moins %s caratères, ou être plus grand que %s si c\'est un nombre',
self::translate($attribute),
$params[0],
$params[0]
),
'validate' => function () use ($value, $params) {
if (\is_string($value)) {
return \strlen($value) >= (int)$params[0];
}
if (\is_int($value)) {
return $value >= (int)$params[0];
}
return false;
}
];
}
/**
* Make Max validator
*
* @param string $attribute
* @param mixed $value
* @param array $params
* @return array
*/
private static function validationMax(string $attribute, $value, array $params): array
{
return [
'type' => 'max',
'error' => sprintf(
'%s doit avoir au moins %s caratères, ou être plus grand que %s si c\'est un nombre',
self::translate($attribute),
$params[0],
$params[0]
),
'validate' => function () use ($value, $params) {
if (\is_string($value)) {
return \strlen($value) <= (int)$params[0];
}
if (\is_int($value)) {
return $value <= (int)$params[0];
}
return false;
}
];
}
/**
* Make Uuid validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationUuid(string $attribute, $value): array
{
return [
'type' => 'uuid',
'error' => sprintf(
'%s doit être un uuid valide',
self::translate($attribute)
),
'validate' => function () use ($value) {
return Validator::uuid()->validate($value);
}
];
}
/**
* Make MixedCase validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationMixedCase(string $attribute, $value): array
{
return [
'type' => 'mixedCase',
'error' => sprintf(
'%s doit contenir des lettre minuscules et majuscules',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \preg_match('/[a-z]/', $value) and \preg_match('/[A-Z]/', $value);
}
];
}
/**
* Make ContainDigit validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationContainDigit(string $attribute, $value): array
{
return [
'type' => 'containDigit',
'error' => sprintf(
'%s doit contenir au moins un chiffre',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \preg_match('/[0-9]/', $value);
}
];
}
/**
* Make ContainSymbol validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationContainSymbol(string $attribute, $value): array
{
return [
'type' => 'containSymbol',
'error' => sprintf(
'%s doit contenir au moins un caractère spécial comme une virgule par exemple',
self::translate($attribute)
),
'validate' => function () use ($value) {
return \preg_match('/[^a-zA-Z0-9]/', $value);
}
];
}
/**
* Make Date validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationDate(string $attribute, $value): array
{
return [
'type' => 'date',
'error' => sprintf('%s doit être une date valide', self::translate($attribute)),
'validate' => function () use ($value) {
return Validator::dateTime()->validate($value);
}
];
}
/**
* Make DateFormat validator
*
* @param string $attribute
* @param mixed $value
* @param array $params
* @return array
*/
private static function validationDateFormat(string $attribute, $value, array $params): array
{
return [
'type' => 'date_format',
'error' => sprintf('%s doit être une date au format %s', self::translate($attribute), $params[0]),
'validate' => function () use ($value, $params) {
return Validator::dateTime($params[0])->validate($value);
}
];
}
/**
* Make Email validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationEmail(string $attribute, $value): array
{
return [
'type' => 'email',
'error' => sprintf('%s doit être un email valide', self::translate($attribute)),
'validate' => function () use ($value) {
return Validator::email()->validate($value);
}
];
}
/**
* Make Array validator
*
* @param string $attribute
* @param mixed $value
* @return array
*/
private static function validationArray(string $attribute, $value): array
{
return [
'type' => 'array',
'error' => sprintf('%s doit être un tableau valide', self::translate($attribute)),
'validate' => function () use ($value) {
return Validator::arrayType()->validate($value);
}
];
}
/**
* Make Closure validator
*
* @param string $attribute
* @param mixed $value
* @param \Closure $closure
* @return array
*/
private static function validationClosure(string $attribute, $value, \Closure $closure): array
{
return [
'type' => 'closure',
'error' => sprintf('%s doit être validé', self::translate($attribute)),
'validate' => function () use ($attribute, $value, $closure) {
$closure($attribute, $value, function ($message) {
throw new ValidationException($message);
});
return true;
}
];
}
// * END VALIDATION RULES
/**
* This is only used for unit tests.
* Get config
*
* @param string $key
* @param string $default
* @return string|null
*/
public static function getConfig(string $key, string $default = null): ?string
{
$value = null;
if (function_exists('config')) {
$value = \config($key, $default);
}
return $value;
}
/**
* Generate a Request from the route name
*
* @param string $routeName
* @param array $query Params to pass on to the route.
* @param array $data Http body params.
* @return \LDC\LDCCRM\Transport\Request
* @throws \ErrorException If the requested route is unknown.
* @throws \ErrorException If a required parameter is missing.
*/
public static function getResquest(string $routeName, array $query = [], array $data = []): Request
{
// Convert bool to integer.
$query = \array_map(function ($v) {
return is_bool($v) ? (int)$v : $v;
}, $query);
$data = \array_map(function ($v) {
return is_bool($v) ? (int)$v : $v;
}, $data);
$routes = \json_decode(\file_get_contents(__DIR__ . '/routes.json'), true);
if (!isset($routes[$routeName])) {
throw new \ErrorException("Unknown route $routeName from the crm");
}
$route = $routes[$routeName];
$matches = [];
$matchedIndex = [];
\preg_match_all('/{([\w-]*)}/', $route['uri'], $matches);
if (\count($matches[0])) {
foreach ($matches[0] as $k => $match) {
if (!isset($query[$matches[1][$k]])) {
throw new \ErrorException(sprintf('Missing required parameter %s', $matches[1][$k]));
}
$route['uri'] = str_replace($match, $query[$matches[1][$k]], $route['uri']);
$matchedIndex[$matches[1][$k]] = $query[$matches[1][$k]];
}
}
foreach ($matchedIndex as $key => $value) {
unset($query[$key]);
}
if (count($query)) {
$route['uri'] = sprintf('%s?%s', $route['uri'], \http_build_query($query));
}
return new Request(HttpMethods::make($route['methods'][0]), $route['uri'], [], $data);
}
/**
* Get composer.json infos
*
* @return array
*/
public static function getPackageInfo(): array
{
if (!static::$packageInfos) {
static::$packageInfos = \json_decode(\file_get_contents(__DIR__ . '/../composer.json'), true);
}
return static::$packageInfos;
}
/**
* Translation a string using php file lang
*
* @param string $key
* @param string $lang
* @return string
*/
public static function translate(string $key, string $lang = 'fr'): string
{
try {
if (!array_key_exists($lang, self::$translations)) {
self::$translations = require __DIR__ . "/../resources/lang/$lang.php";
}
} finally {
if (!isset(self::$translations['validation'][$key])) {
return $key;
} else {
return self::$translations['validation'][$key];
}
}
}
/**
* Write log using syslog
* It also write log using \Illuminate\Support\Facades\Log
* if it is present
*
* @param string $message The message is prefixed with "LDC CRM:" .
* @param \LDC\LDCCRM\Enums\LogLevel $level The log level to apply.
* @return void
*/
public static function log(string $message, LogLevel $level)
{
$message = "LDC CRM: $message";
if (\class_exists('\Illuminate\Support\Facades\Log')) {
switch ($level) {
case LogLevel::EMERG():
\Illuminate\Support\Facades\Log::emergency($message);
break;
case LogLevel::ALERT():
\Illuminate\Support\Facades\Log::alert($message);
break;
case LogLevel::CRIT():
\Illuminate\Support\Facades\Log::critical($message);
break;
case LogLevel::ERR():
\Illuminate\Support\Facades\Log::error($message);
break;
case LogLevel::WARNING():
\Illuminate\Support\Facades\Log::warning($message);
break;
case LogLevel::NOTICE():
\Illuminate\Support\Facades\Log::notice($message);
break;
case LogLevel::INFO():
\Illuminate\Support\Facades\Log::info($message);
break;
case LogLevel::DEBUG():
\Illuminate\Support\Facades\Log::debug($message);
break;
}//end switch
\Illuminate\Support\Facades\Log::warning($message);
}//end if
\syslog(\constant($level), $message);
}
/**
* Set a property without any validation.
*
* ! Warning : this can change any property of the object, use with caution.
*
* @param object $object The object to inspect.
* @param string $property The object property to assign to.
* @param mixed $value The value to insert.
* @return void
* @throws LDC\LDCCRM\Exceptions\AccessError If trying to set a value on a unknown property.
*/
public static function setRawProperty(object $object, string $property, $value): void
{
if (is_object($value) and in_array(BaseEnum::class, class_parents($value))) {
$value = $value->value;
}
$objectName = \get_class($object);
$refClass = new \ReflectionClass($objectName);
try {
$refProp = $refClass->getProperty($property);
} catch (\ReflectionException $e) {
throw new AccessError($objectName, $property);
}
$refProp->setAccessible(true);
$refProp->setValue($object, $value);
}
/**
* Get a property of an object.
*
* ! Warning : this will return any property of the object, use with caution.
*
* @param object $object The object to inspect.
* @param string $property The object property to inspect.
* @return mixed|null Returns null if the property was not found any way.
*/
public static function getRawProperty(object $object, string $property)
{
$refClass = new \ReflectionClass(\get_class($object));
try {
$refProp = $refClass->getProperty($property);
} catch (\ReflectionException $e) {
return null;
}
$refProp->setAccessible(true);
return $refProp->getValue($object);
}
}