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
694 lines
21 KiB
PHP
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);
|
|
}
|
|
}
|