diff --git a/apps/oauth2/lib/Controller/SettingsController.php b/apps/oauth2/lib/Controller/SettingsController.php index 046e6d7704188..3fd29618c15ca 100644 --- a/apps/oauth2/lib/Controller/SettingsController.php +++ b/apps/oauth2/lib/Controller/SettingsController.php @@ -39,16 +39,15 @@ use OCP\IL10N; use OCP\IRequest; use OCP\Security\ISecureRandom; +use OCP\Validator\Constraints\Url; +use OCP\Validator\IValidator; class SettingsController extends Controller { - /** @var ClientMapper */ - private $clientMapper; - /** @var ISecureRandom */ - private $secureRandom; - /** @var AccessTokenMapper */ - private $accessTokenMapper; - /** @var IL10N */ - private $l; + private ClientMapper $clientMapper; + private ISecureRandom $secureRandom; + private AccessTokenMapper $accessTokenMapper; + private IL10N $l; + private IValidator $validator; public const validChars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; @@ -57,18 +56,20 @@ public function __construct(string $appName, ClientMapper $clientMapper, ISecureRandom $secureRandom, AccessTokenMapper $accessTokenMapper, - IL10N $l + IL10N $l, + IValidator $validator ) { parent::__construct($appName, $request); $this->secureRandom = $secureRandom; $this->clientMapper = $clientMapper; $this->accessTokenMapper = $accessTokenMapper; $this->l = $l; + $this->validator = $validator; } public function addClient(string $name, string $redirectUri): JSONResponse { - if (filter_var($redirectUri, FILTER_VALIDATE_URL) === false) { + if (count($this->validator->validate($redirectUri, [new Url()])) > 0) { return new JSONResponse(['message' => $this->l->t('Your redirect URL needs to be a full URL for example: https://yourdomain.com/path')], Http::STATUS_BAD_REQUEST); } diff --git a/apps/settings/lib/Controller/CheckSetupController.php b/apps/settings/lib/Controller/CheckSetupController.php index 5225cd04f0922..dad95e7ec9c79 100644 --- a/apps/settings/lib/Controller/CheckSetupController.php +++ b/apps/settings/lib/Controller/CheckSetupController.php @@ -84,6 +84,9 @@ use OCP\Lock\ILockingProvider; use OCP\Notification\IManager; use OCP\Security\ISecureRandom; +use OCP\Validator\Constraints\Url; +use OCP\Validator\Constraints\NotBlank; +use OCP\Validator\IValidator; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; @@ -125,6 +128,7 @@ class CheckSetupController extends Controller { private $appManager; /** @var IServerContainer */ private $serverContainer; + private IValidator $validator; public function __construct($AppName, IRequest $request, @@ -145,7 +149,8 @@ public function __construct($AppName, ITempManager $tempManager, IManager $manager, IAppManager $appManager, - IServerContainer $serverContainer + IServerContainer $serverContainer, + IValidator $validator ) { parent::__construct($AppName, $request); $this->config = $config; @@ -166,6 +171,7 @@ public function __construct($AppName, $this->manager = $manager; $this->appManager = $appManager; $this->serverContainer = $serverContainer; + $this->validator = $validator; } /** @@ -618,7 +624,10 @@ protected function getSuggestedOverwriteCliURL(): string { $suggestedOverwriteCliUrl = $this->request->getServerProtocol() . '://' . $this->request->getInsecureServerHost() . \OC::$WEBROOT; // Check correctness by checking if it is a valid URL - if (filter_var($currentOverwriteCliUrl, FILTER_VALIDATE_URL)) { + if ($this->validator->isValid($currentOverwriteCliUrl, [ + new NotBlank(), + new Url(), + ])) { $suggestedOverwriteCliUrl = ''; } diff --git a/apps/theming/lib/Controller/ThemingController.php b/apps/theming/lib/Controller/ThemingController.php index a735dfafc2ce8..0d189aec29788 100644 --- a/apps/theming/lib/Controller/ThemingController.php +++ b/apps/theming/lib/Controller/ThemingController.php @@ -54,6 +54,11 @@ use OCP\IRequest; use OCP\ITempManager; use OCP\IURLGenerator; +use OCP\Validator\Constraints\CssColor; +use OCP\Validator\Constraints\Length; +use OCP\Validator\Constraints\Url; +use OCP\Validator\IValidator; +use OCP\Validator\Violation; /** * Class ThemingController @@ -63,39 +68,20 @@ * @package OCA\Theming\Controller */ class ThemingController extends Controller { - /** @var ThemingDefaults */ - private $themingDefaults; - /** @var IL10N */ - private $l10n; - /** @var IConfig */ - private $config; - /** @var ITempManager */ - private $tempManager; - /** @var IAppData */ - private $appData; - /** @var SCSSCacher */ - private $scssCacher; - /** @var IURLGenerator */ - private $urlGenerator; - /** @var IAppManager */ - private $appManager; - /** @var ImageManager */ - private $imageManager; + private ThemingDefaults $themingDefaults; + private IL10N $l10n; + private IConfig $config; + private ITempManager $tempManager; + private IAppData $appData; + private SCSSCacher $scssCacher; + private IURLGenerator $urlGenerator; + private IAppManager $appManager; + private ImageManager $imageManager; + private IValidator $validator; /** * ThemingController constructor. - * * @param string $appName - * @param IRequest $request - * @param IConfig $config - * @param ThemingDefaults $themingDefaults - * @param IL10N $l - * @param ITempManager $tempManager - * @param IAppData $appData - * @param SCSSCacher $scssCacher - * @param IURLGenerator $urlGenerator - * @param IAppManager $appManager - * @param ImageManager $imageManager */ public function __construct( $appName, @@ -108,7 +94,8 @@ public function __construct( SCSSCacher $scssCacher, IURLGenerator $urlGenerator, IAppManager $appManager, - ImageManager $imageManager + ImageManager $imageManager, + IValidator $validator ) { parent::__construct($appName, $request); @@ -121,6 +108,7 @@ public function __construct( $this->urlGenerator = $urlGenerator; $this->appManager = $appManager; $this->imageManager = $imageManager; + $this->validator = $validator; } /** @@ -132,52 +120,62 @@ public function __construct( */ public function updateStylesheet($setting, $value) { $value = trim($value); - $error = null; + $violations = []; switch ($setting) { case 'name': - if (strlen($value) > 250) { - $error = $this->l10n->t('The given name is too long'); - } + $violations = $this->validator->validate($value, [ + new Length([ + 'max' => 250, + 'maxMessage' => $this->l10n->t('The given name is too long'), + ]) + ]); break; case 'url': - if (strlen($value) > 500) { - $error = $this->l10n->t('The given web address is too long'); - } - if (!$this->isValidUrl($value)) { - $error = $this->l10n->t('The given web address is not a valid URL'); - } + $violations = $this->validator->validate($value, [ + new Length([ + 'max' => 500, + 'maxMessage' => $this->l10n->t('The given web address is too long'), + ]), + new Url(false, ['http', 'https'], $this->l10n->t('The given web address is not a valid URL')), + ]); + break; case 'imprintUrl': - if (strlen($value) > 500) { - $error = $this->l10n->t('The given legal notice address is too long'); - } - if (!$this->isValidUrl($value)) { - $error = $this->l10n->t('The given legal notice address is not a valid URL'); - } + $violations = $this->validator->validate($value, [ + new Length([ + 'max' => 500, + 'maxMessage' => $this->l10n->t('The given legal notice address is too long'), + ]), + new Url(false, ['http', 'https'], $this->l10n->t('The given legal notice address is not a valid URL')), + ]); break; case 'privacyUrl': - if (strlen($value) > 500) { - $error = $this->l10n->t('The given privacy policy address is too long'); - } - if (!$this->isValidUrl($value)) { - $error = $this->l10n->t('The given privacy policy address is not a valid URL'); - } + $violations = $this->validator->validate($value, [ + new Length([ + 'max' => 500, + 'maxMessage' => $this->l10n->t('The given privacy policy address is too long'), + ]), + new Url(false, ['http', 'https'], $this->l10n->t('The given privacy policy address is not a valid URL')), + ]); break; case 'slogan': - if (strlen($value) > 500) { - $error = $this->l10n->t('The given slogan is too long'); - } + $violations = $this->validator->validate($value, [ + new Length([ + 'max' => 500, + 'maxMessage' => $this->l10n->t('The given slogan is too long'), + ]) + ]); break; case 'color': - if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $value)) { - $error = $this->l10n->t('The given color is invalid'); - } + $violations = $this->validator->validate($value, [ + new CssColor($this->l10n->t('The given color is invalid')), + ]); break; } - if ($error !== null) { + if (count($violations) > 0) { return new DataResponse([ 'data' => [ - 'message' => $error, + 'message' => implode('. ', array_map(fn (Violation $violation) => $violation->getMessage(), $violations)) . '.', ], 'status' => 'error' ], Http::STATUS_BAD_REQUEST); @@ -200,14 +198,6 @@ public function updateStylesheet($setting, $value) { ); } - /** - * Check that a string is a valid http/https url - */ - private function isValidUrl(string $url): bool { - return ((strpos($url, 'http://') === 0 || strpos($url, 'https://') === 0) && - filter_var($url, FILTER_VALIDATE_URL) !== false); - } - /** * @AuthorizedAdminSetting(settings=OCA\Theming\Settings\Admin) * @return DataResponse diff --git a/apps/theming/lib/ThemingDefaults.php b/apps/theming/lib/ThemingDefaults.php index 3d7aaee2064a3..e6377725c8510 100644 --- a/apps/theming/lib/ThemingDefaults.php +++ b/apps/theming/lib/ThemingDefaults.php @@ -49,6 +49,11 @@ use OCP\IL10N; use OCP\INavigationManager; use OCP\IURLGenerator; +use OCP\Util as OCPUtil; +use OCP\Validator\Constraints\CssColor; +use OCP\Validator\Constraints\NotBlank; +use OCP\Validator\Constraints\Url; +use OCP\Validator\IValidator; class ThemingDefaults extends \OC_Defaults { @@ -90,6 +95,7 @@ class ThemingDefaults extends \OC_Defaults { private $AndroidClientUrl; /** @var string */ private $FDroidClientUrl; + private IValidator $validator; /** * ThemingDefaults constructor. @@ -109,7 +115,8 @@ public function __construct(IConfig $config, Util $util, ImageManager $imageManager, IAppManager $appManager, - INavigationManager $navigationManager + INavigationManager $navigationManager, + IValidator $validator ) { parent::__construct(); $this->config = $config; @@ -120,6 +127,7 @@ public function __construct(IConfig $config, $this->util = $util; $this->appManager = $appManager; $this->navigationManager = $navigationManager; + $this->validator = $validator; $this->name = parent::getName(); $this->title = parent::getTitle(); @@ -208,9 +216,10 @@ public function getShortFooter() { $legalLinks = ''; $divider = ''; foreach ($links as $link) { - if ($link['url'] !== '' - && filter_var($link['url'], FILTER_VALIDATE_URL) - ) { + if ($this->validator->isValid($link['url'], [ + new NotBlank(), + new Url(), + ])) { $legalLinks .= $divider . '' . $link['text'] . ''; $divider = ' · '; @@ -230,7 +239,7 @@ public function getShortFooter() { */ public function getColorPrimary() { $color = $this->config->getAppValue('theming', 'color', $this->color); - if (!preg_match('/^\#([0-9a-f]{3}|[0-9a-f]{6})$/i', $color)) { + if (!$this->validator->isValid($color, [new CssColor()])) { $color = '#0082c9'; } return $color; diff --git a/apps/theming/tests/Controller/ThemingControllerTest.php b/apps/theming/tests/Controller/ThemingControllerTest.php index cff2028809dc9..34fe930db2c5a 100644 --- a/apps/theming/tests/Controller/ThemingControllerTest.php +++ b/apps/theming/tests/Controller/ThemingControllerTest.php @@ -35,6 +35,7 @@ use OC\L10N\L10N; use OC\Template\SCSSCacher; +use OC\Validator\Validator; use OCA\Theming\Controller\ThemingController; use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; @@ -107,7 +108,8 @@ protected function setUp(): void { $this->scssCacher, $this->urlGenerator, $this->appManager, - $this->imageManager + $this->imageManager, + new Validator() ); parent::setUp(); @@ -139,7 +141,7 @@ public function testUpdateStylesheetSuccess($setting, $value, $message) { ->method('set') ->with($setting, $value); $this->l10n - ->expects($this->once()) + ->expects($this->any()) ->method('t') ->willReturnCallback(function ($str) { return $str; @@ -170,18 +172,18 @@ public function testUpdateStylesheetSuccess($setting, $value, $message) { public function dataUpdateStylesheetError() { return [ - ['name', str_repeat('a', 251), 'The given name is too long'], - ['url', 'http://example.com/' . str_repeat('a', 501), 'The given web address is too long'], - ['url', str_repeat('a', 501), 'The given web address is not a valid URL'], - ['url', 'javascript:alert(1)', 'The given web address is not a valid URL'], - ['slogan', str_repeat('a', 501), 'The given slogan is too long'], - ['color', '0082C9', 'The given color is invalid'], - ['color', '#0082Z9', 'The given color is invalid'], - ['color', 'Nextcloud', 'The given color is invalid'], - ['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'], - ['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL'], - ['imprintUrl', 'javascript:foo', 'The given legal notice address is not a valid URL'], - ['privacyUrl', '#0082Z9', 'The given privacy policy address is not a valid URL'], + ['name', str_repeat('a', 251), 'The given name is too long.'], + ['url', 'http://example.com/' . str_repeat('a', 501), 'The given web address is too long.'], + ['url', str_repeat('a', 501), 'The given web address is too long. The given web address is not a valid URL.'], + ['url', 'javascript:alert(1)', 'The given web address is not a valid URL.'], + ['slogan', str_repeat('a', 501), 'The given slogan is too long.'], + ['color', '0082C9', 'The given color is invalid.'], + ['color', '#0082Z9', 'The given color is invalid.'], + ['color', 'Nextcloud', 'The given color is invalid.'], + ['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL.'], + ['imprintUrl', '0082C9', 'The given legal notice address is not a valid URL.'], + ['imprintUrl', 'javascript:foo', 'The given legal notice address is not a valid URL.'], + ['privacyUrl', '#0082Z9', 'The given privacy policy address is not a valid URL.'], ]; } diff --git a/apps/theming/tests/ThemingDefaultsTest.php b/apps/theming/tests/ThemingDefaultsTest.php index c8ef147dc94bc..a89399ce4bc3c 100644 --- a/apps/theming/tests/ThemingDefaultsTest.php +++ b/apps/theming/tests/ThemingDefaultsTest.php @@ -34,6 +34,7 @@ */ namespace OCA\Theming\Tests; +use OC\Validator\Validator; use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; @@ -98,7 +99,8 @@ protected function setUp(): void { $this->util, $this->imageManager, $this->appManager, - $this->navigationManager + $this->navigationManager, + new Validator() ); } diff --git a/build/psalm-baseline-ocp.xml b/build/psalm-baseline-ocp.xml index 87a994ea720f8..570eb02d5de9b 100644 --- a/build/psalm-baseline-ocp.xml +++ b/build/psalm-baseline-ocp.xml @@ -16,6 +16,11 @@ \OC + + + \OC + + $this->request->server diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 44a113f1238e9..24ba57f3d92ca 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2030,15 +2030,6 @@ - - $isUnmapped - - - $result - - - bool - isset($qb) @@ -3806,12 +3797,6 @@ isAdmin - - - $sortMode - self::SORT_NONE - - string|resource diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index 86efab3ad52b2..a42557573f772 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -582,6 +582,15 @@ 'OCP\\User\\Events\\UserLoggedOutEvent' => $baseDir . '/lib/public/User/Events/UserLoggedOutEvent.php', 'OCP\\User\\GetQuotaEvent' => $baseDir . '/lib/public/User/GetQuotaEvent.php', 'OCP\\Util' => $baseDir . '/lib/public/Util.php', + 'OCP\\Validator\\Constraints\\Constraint' => $baseDir . '/lib/public/Validator/Constraints/Constraint.php', + 'OCP\\Validator\\Constraints\\CssColor' => $baseDir . '/lib/public/Validator/Constraints/CssColor.php', + 'OCP\\Validator\\Constraints\\Email' => $baseDir . '/lib/public/Validator/Constraints/Email.php', + 'OCP\\Validator\\Constraints\\Length' => $baseDir . '/lib/public/Validator/Constraints/Length.php', + 'OCP\\Validator\\Constraints\\NotBlank' => $baseDir . '/lib/public/Validator/Constraints/NotBlank.php', + 'OCP\\Validator\\Constraints\\Url' => $baseDir . '/lib/public/Validator/Constraints/Url.php', + 'OCP\\Validator\\IConstraintValidator' => $baseDir . '/lib/public/Validator/IConstraintValidator.php', + 'OCP\\Validator\\IValidator' => $baseDir . '/lib/public/Validator/IValidator.php', + 'OCP\\Validator\\Violation' => $baseDir . '/lib/public/Validator/Violation.php', 'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayText' => $baseDir . '/lib/public/WorkflowEngine/EntityContext/IDisplayText.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index ea37771d63c37..9bc2935e83b98 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -611,6 +611,15 @@ class ComposerStaticInit53792487c5a8370acc0b06b1a864ff4c 'OCP\\User\\Events\\UserLoggedOutEvent' => __DIR__ . '/../../..' . '/lib/public/User/Events/UserLoggedOutEvent.php', 'OCP\\User\\GetQuotaEvent' => __DIR__ . '/../../..' . '/lib/public/User/GetQuotaEvent.php', 'OCP\\Util' => __DIR__ . '/../../..' . '/lib/public/Util.php', + 'OCP\\Validator\\Constraints\\Constraint' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/Constraint.php', + 'OCP\\Validator\\Constraints\\CssColor' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/CssColor.php', + 'OCP\\Validator\\Constraints\\Email' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/Email.php', + 'OCP\\Validator\\Constraints\\Length' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/Length.php', + 'OCP\\Validator\\Constraints\\NotBlank' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/NotBlank.php', + 'OCP\\Validator\\Constraints\\Url' => __DIR__ . '/../../..' . '/lib/public/Validator/Constraints/Url.php', + 'OCP\\Validator\\IConstraintValidator' => __DIR__ . '/../../..' . '/lib/public/Validator/IConstraintValidator.php', + 'OCP\\Validator\\IValidator' => __DIR__ . '/../../..' . '/lib/public/Validator/IValidator.php', + 'OCP\\Validator\\Violation' => __DIR__ . '/../../..' . '/lib/public/Validator/Violation.php', 'OCP\\WorkflowEngine\\EntityContext\\IContextPortation' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IContextPortation.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayName' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayName.php', 'OCP\\WorkflowEngine\\EntityContext\\IDisplayText' => __DIR__ . '/../../..' . '/lib/public/WorkflowEngine/EntityContext/IDisplayText.php', diff --git a/lib/private/Server.php b/lib/private/Server.php index 38720ab71c013..76d568599aa25 100644 --- a/lib/private/Server.php +++ b/lib/private/Server.php @@ -109,8 +109,8 @@ use OC\IntegrityCheck\Helpers\AppLocator; use OC\IntegrityCheck\Helpers\EnvironmentHelper; use OC\IntegrityCheck\Helpers\FileAccessHelper; -use OC\LDAP\NullLDAPProviderFactory; use OC\KnownUser\KnownUserService; +use OC\LDAP\NullLDAPProviderFactory; use OC\Lock\DBLockingProvider; use OC\Lock\MemcacheLockingProvider; use OC\Lock\NoopLockingProvider; @@ -145,10 +145,14 @@ use OC\SystemTag\ManagerFactory as SystemTagManagerFactory; use OC\Tagging\TagMapper; use OC\Template\JSCombiner; +use OC\Validator\Validator as ValidationValidator; +use OCA\Files_External\Service\BackendService; +use OCA\Files_External\Service\GlobalStoragesService; +use OCA\Files_External\Service\UserGlobalStoragesService; +use OCA\Files_External\Service\UserStoragesService; use OCA\Theming\ImageManager; use OCA\Theming\ThemingDefaults; use OCA\Theming\Util; -use OCA\WorkflowEngine\Service\Logger; use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\Authentication\LoginCredentials\IStore; @@ -235,27 +239,21 @@ use OCP\SystemTag\ISystemTagManager; use OCP\SystemTag\ISystemTagObjectMapper; use OCP\User\Events\BeforePasswordUpdatedEvent; -use OCP\User\Events\BeforeUserCreatedEvent; -use OCP\User\Events\BeforeUserDeletedEvent; use OCP\User\Events\BeforeUserLoggedInEvent; use OCP\User\Events\BeforeUserLoggedInWithCookieEvent; use OCP\User\Events\BeforeUserLoggedOutEvent; use OCP\User\Events\PasswordUpdatedEvent; use OCP\User\Events\PostLoginEvent; use OCP\User\Events\UserChangedEvent; -use OCP\User\Events\UserDeletedEvent; use OCP\User\Events\UserLoggedInEvent; use OCP\User\Events\UserLoggedInWithCookieEvent; use OCP\User\Events\UserLoggedOutEvent; +use OCP\Validator\IValidator as IValidationValidator; use Psr\Container\ContainerExceptionInterface; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\GenericEvent; -use OCA\Files_External\Service\UserStoragesService; -use OCA\Files_External\Service\UserGlobalStoragesService; -use OCA\Files_External\Service\GlobalStoragesService; -use OCA\Files_External\Service\BackendService; /** * Class Server @@ -1192,7 +1190,8 @@ public function __construct($webRoot, \OC\Config $config) { $this->get(ITempManager::class) ), $c->get(IAppManager::class), - $c->get(INavigationManager::class) + $c->get(INavigationManager::class), + $c->get(IValidationValidator::class) ); } return new \OC_Defaults(); @@ -1400,6 +1399,10 @@ public function __construct($webRoot, \OC\Config $config) { $this->registerAlias(\OCP\UserStatus\IManager::class, \OC\UserStatus\Manager::class); + $this->registerService(IValidationValidator::class, function (ContainerInterface $c): IValidationValidator { + return new ValidationValidator(); + }); + $this->connectDispatcher(); } diff --git a/lib/private/Setup.php b/lib/private/Setup.php index 177ede1e29254..e3795194cfb98 100644 --- a/lib/private/Setup.php +++ b/lib/private/Setup.php @@ -59,24 +59,21 @@ use OCP\Defaults; use OCP\IGroup; use OCP\IL10N; +use OCP\L10N\IFactory; use OCP\Security\ISecureRandom; +use OCP\Validator\Constraints\Url; +use OCP\Validator\IValidator; use Psr\Log\LoggerInterface; +use OCP\Util; class Setup { - /** @var SystemConfig */ - protected $config; - /** @var IniGetWrapper */ - protected $iniWrapper; - /** @var IL10N */ - protected $l10n; - /** @var Defaults */ - protected $defaults; - /** @var LoggerInterface */ - protected $logger; - /** @var ISecureRandom */ - protected $random; - /** @var Installer */ - protected $installer; + protected SystemConfig $config; + protected IniGetWrapper $iniWrapper; + protected IL10N $l10n; + protected Defaults $defaults; + protected LoggerInterface $logger; + protected ISecureRandom $random; + protected Installer $installer; public function __construct( SystemConfig $config, @@ -339,7 +336,7 @@ public function install($options) { 'trusted_domains' => $trustedDomains, 'datadirectory' => $dataDir, 'dbtype' => $dbType, - 'version' => implode('.', \OCP\Util::getVersion()), + 'version' => implode('.', Util::getVersion()), ]; if ($this->config->getValue('overwrite.cli.url', null) === null) { @@ -475,7 +472,9 @@ private static function findWebRoot(SystemConfig $config): string { if ($webRoot === '') { throw new InvalidArgumentException('overwrite.cli.url is empty'); } - if (!filter_var($webRoot, FILTER_VALIDATE_URL)) { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + if ($validator->isValid($webRoot, [new Url()])) { throw new InvalidArgumentException('invalid value for overwrite.cli.url'); } $webRoot = rtrim((parse_url($webRoot, PHP_URL_PATH) ?? ''), '/'); @@ -504,11 +503,11 @@ public static function updateHtaccess() { $setupHelper = new \OC\Setup( $config, \OC::$server->get(IniGetWrapper::class), - \OC::$server->getL10N('lib'), - \OC::$server->query(Defaults::class), + \OC::$server->get(IFactory::class)->get('lib'), + \OC::$server->get(Defaults::class), \OC::$server->get(LoggerInterface::class), - \OC::$server->getSecureRandom(), - \OC::$server->query(Installer::class) + \OC::$server->get(ISecureRandom::class), + \OC::$server->get(Installer::class) ); $htaccessContent = file_get_contents($setupHelper->pathToHtaccess()); diff --git a/lib/private/Validator/Validator.php b/lib/private/Validator/Validator.php new file mode 100644 index 0000000000000..d9b78727a8b7c --- /dev/null +++ b/lib/private/Validator/Validator.php @@ -0,0 +1,45 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OC\Validator; + +use OCP\Validator\IValidator; +use OCP\Validator\Violation; + +class Validator implements IValidator { + public function validate($value, array $constraints): array { + /** @var Violation[] $violations */ + $violations = []; + foreach ($constraints as $constraint) { + $violations = array_merge($violations, $constraint->validate($value)); + } + return $violations; + } + + public function isValid($value, array $constraints): bool { + foreach ($constraints as $constraint) { + if (count($constraint->validate($value)) > 0) { + return false; + } + } + return true; + } +} diff --git a/lib/public/Validator/Constraints/Constraint.php b/lib/public/Validator/Constraints/Constraint.php new file mode 100644 index 0000000000000..0d99457e090c8 --- /dev/null +++ b/lib/public/Validator/Constraints/Constraint.php @@ -0,0 +1,37 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use OCP\IL10N; +use OCP\L10N\IFactory; +use OCP\Validator\IConstraintValidator; + +/** + * Abstract class for validation constraint. + */ +abstract class Constraint implements IConstraintValidator { + protected IL10N $l10n; + + public function __construct() { + $this->l10n = \OC::$server->get(IFactory::class)->get('core'); + } +} diff --git a/lib/public/Validator/Constraints/CssColor.php b/lib/public/Validator/Constraints/CssColor.php new file mode 100644 index 0000000000000..1a7645bbee9c1 --- /dev/null +++ b/lib/public/Validator/Constraints/CssColor.php @@ -0,0 +1,128 @@ + + * @copyright Mathieu Santostefano + * + * @license AGPL-3.0-or-later AND MIT + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use OCP\Validator\Violation; + +/** + * Constraint that validate that a value is a CSS3 compatible color. + */ +class CssColor extends Constraint { + public const HEX_LONG = 'hex_long'; + public const HEX_LONG_WITH_ALPHA = 'hex_long_with_alpha'; + public const HEX_SHORT = 'hex_short'; + public const HEX_SHORT_WITH_ALPHA = 'hex_short_with_alpha'; + public const BASIC_NAMED_COLORS = 'basic_named_colors'; + public const EXTENDED_NAMED_COLORS = 'extended_named_colors'; + public const SYSTEM_COLORS = 'system_colors'; + public const KEYWORDS = 'keywords'; + public const RGB = 'rgb'; + public const RGBA = 'rgba'; + public const HSL = 'hsl'; + public const HSLA = 'hsla'; + private string $message; + + private const PATTERN_HEX_LONG = '/^#[0-9a-f]{6}$/i'; + private const PATTERN_HEX_LONG_WITH_ALPHA = '/^#[0-9a-f]{8}$/i'; + private const PATTERN_HEX_SHORT = '/^#[0-9a-f]{3}$/i'; + private const PATTERN_HEX_SHORT_WITH_ALPHA = '/^#[0-9a-f]{4}$/i'; + // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Basic_Colors + private const PATTERN_BASIC_NAMED_COLORS = '/^(black|silver|gray|white|maroon|red|purple|fuchsia|green|lime|olive|yellow|navy|blue|teal|aqua)$/i'; + // List comes from https://www.w3.org/wiki/CSS/Properties/color/keywords#Extended_colors + private const PATTERN_EXTENDED_NAMED_COLORS = '/^(aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen)$/i'; + // List comes from https://drafts.csswg.org/css-color/#css-system-colors + private const PATTERN_SYSTEM_COLORS = '/^(Canvas|CanvasText|LinkText|VisitedText|ActiveText|ButtonFace|ButtonText|ButtonBorder|Field|FieldText|Highlight|HighlightText|SelectedItem|SelectedItemText|Mark|MarkText|GrayText)$/i'; + private const PATTERN_KEYWORDS = '/^(transparent|currentColor)$/i'; + private const PATTERN_RGB = '/^rgb\(\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d)\s*\)$/i'; + private const PATTERN_RGBA = '/^rgba\(\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|255|25[0-4]|2[0-4]\d|1\d\d|0?\d?\d),\s*(0|0?\.\d+|1(\.0)?)\s*\)$/i'; + private const PATTERN_HSL = '/^hsl\(\s*(0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s*(0|100|\d{1,2})%,\s*(0|100|\d{1,2})%\s*\)$/i'; + private const PATTERN_HSLA = '/^hsla\(\s*(0|360|35\d|3[0-4]\d|[12]\d\d|0?\d?\d),\s*(0|100|\d{1,2})%,\s*(0|100|\d{1,2})%,\s*(0|0?\.\d+|1(\.0)?)\s*\)$/i'; + + private const COLOR_PATTERNS = [ + CssColor::HEX_LONG => self::PATTERN_HEX_LONG, + CssColor::HEX_LONG_WITH_ALPHA => self::PATTERN_HEX_LONG_WITH_ALPHA, + CssColor::HEX_SHORT => self::PATTERN_HEX_SHORT, + CssColor::HEX_SHORT_WITH_ALPHA => self::PATTERN_HEX_SHORT_WITH_ALPHA, + CssColor::BASIC_NAMED_COLORS => self::PATTERN_BASIC_NAMED_COLORS, + CssColor::EXTENDED_NAMED_COLORS => self::PATTERN_EXTENDED_NAMED_COLORS, + CssColor::SYSTEM_COLORS => self::PATTERN_SYSTEM_COLORS, + CssColor::KEYWORDS => self::PATTERN_KEYWORDS, + CssColor::RGB => self::PATTERN_RGB, + CssColor::RGBA => self::PATTERN_RGBA, + CssColor::HSL => self::PATTERN_HSL, + CssColor::HSLA => self::PATTERN_HSLA, + ]; + + private array $formats; + + /** + * @param string|null $message The violation message displayed to the user + * @param array|null $formats The list of allowed color formats, by default all + */ + public function __construct(?string $message = null, ?array $formats = null) { + parent::__construct(); + $this->message = $message ?? $this->l10n->t('"{{ value }}" is not a valid email address'); + $this->formats = $formats ?? [ + self::HEX_LONG, + self::HEX_LONG_WITH_ALPHA, + self::HEX_SHORT, + self::HEX_SHORT_WITH_ALPHA, + self::BASIC_NAMED_COLORS, + self::EXTENDED_NAMED_COLORS, + self::SYSTEM_COLORS, + self::KEYWORDS, + self::RGB, + self::RGBA, + self::HSL, + self::HSLA, + ]; + } + + public function getMessage(): string { + return $this->message; + } + + /** + * @return string[] + */ + public function getFormats(): array { + return $this->formats; + } + + + public function validate($value): array { + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new \RuntimeException('The CssColorValidator can only validate scalar values or object convertible to string.'); + } + + foreach ($this->getFormats() as $regex) { + if (preg_match(self::COLOR_PATTERNS[$regex], (string)$value)) { + return []; + } + } + + return [ + (new Violation($this->getMessage()))->addParameter('{{ value }}', (string)$value), + ]; + } +} diff --git a/lib/public/Validator/Constraints/Email.php b/lib/public/Validator/Constraints/Email.php new file mode 100644 index 0000000000000..ab518e22b5055 --- /dev/null +++ b/lib/public/Validator/Constraints/Email.php @@ -0,0 +1,66 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use Egulias\EmailValidator\Validation\NoRFCWarningsValidation; +use Egulias\EmailValidator\EmailValidator as EguliasEmailValidator; +use OCP\Validator\Violation; + +class Email extends Constraint { + private string $message; + /** + * @param string|null $message Overwrite the default translated error message + * to use when the constraint is not fulfilled. + */ + public function __construct(?string $message = null) { + parent::__construct(); + $this->message = $message === null ? $this->l10n->t('"{{ value }}" is not a valid email address') : $message; + } + + public function getMessage(): string { + return $this->message; + } + + public function validate($value): array { + if ($value === null || $value == '') { + return []; + } + + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new \RuntimeException('The EmailValidator can only validate scalar values or object convertible to string.'); + } + + $value = (string) $value; + if ($value === '') { + return []; + } + + $internalValidator = new EguliasEmailValidator(); + if (!$internalValidator->isValid($value, new NoRFCWarningsValidation())) { + return [ + (new Violation($this->getMessage()))->addParameter('{{ value }}', $value) + ]; + } + + return []; + } +} diff --git a/lib/public/Validator/Constraints/Length.php b/lib/public/Validator/Constraints/Length.php new file mode 100644 index 0000000000000..49594d3808131 --- /dev/null +++ b/lib/public/Validator/Constraints/Length.php @@ -0,0 +1,126 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use OCP\Validator\Violation; + +/** + * Length constrains for strings + * + * ```php + * $name = ... + * $validator = ... + * $validator->validate($name, [new Length([ + * 'min' => 2, + * 'max' => 200, + * 'minMessage' => "Your first name must be at least {{ limit }} characters long", + * 'maxMessage' => "Your first name must be at most {{ limit }} characters long", + * ])]); + * ``` + */ +class Length extends Constraint { + private ?int $min; + private ?int $max; + private ?int $exact; + private string $minMessage; + private string $maxMessage; + private string $exactMessage; + + /** + * @psalm-param array{min?: ?int, max?: ?int, exact?: ?int, minMessage?: ?string, maxMessage?: ?string, exactMessage?: ?string} $options + * @param array $options An array of options. Either min, max or exact needs to be defined. + */ + public function __construct(array $options) { + parent::__construct(); + + $this->min = $options['min'] ?? null; + $this->max = $options['max'] ?? null; + $this->exact = $options['exact'] ?? null; + + $this->minMessage = $options['minMessage'] ?? $this->l10n->t('"This value is too short. It should be at least {{ limit }} characters long.'); + $this->maxMessage = $options['maxMessage'] ?? $this->l10n->t('"This value is too long. It should be at most {{ limit }} characters long.'); + $this->exactMessage = $options['exactMessage'] ?? $this->l10n->t('"This value is incorrect. It should be exactly {{ limit }} characters long.'); + + assert($this->min !== null || $this->max !== null || $this->exact !== null); + } + + public function getMin(): ?int { + return $this->min; + } + + public function getMax(): ?int { + return $this->max; + } + + public function getExact(): ?int { + return $this->exact; + } + + public function getMinMessage(): string { + return $this->minMessage; + } + + public function getMaxMessage(): string { + return $this->maxMessage; + } + + public function getExactMessage(): string { + return $this->exactMessage; + } + + public function validate($value): array { + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new \RuntimeException('The LengthValidator can only validate scalar values or object convertible to string.'); + } + + $stringValue = (string)$value; + $length = mb_strlen($stringValue); + + if ($this->getExact() !== null && $this->getExact() !== $length) { + return [ + (new Violation($this->getExactMessage())) + ->addParameter('{{ limit }}', (string)$this->getMax()) + ->addParameter('{{ value }}', $stringValue) + ->addParameter('{{ stringLength }}', (string)$length), + ]; + } + + if ($this->getMin() !== null && $this->getMin() > $length) { + return [ + (new Violation($this->getMinMessage())) + ->addParameter('{{ limit }}', (string)$this->getMax()) + ->addParameter('{{ value }}', $stringValue) + ->addParameter('{{ stringLength }}', (string)$length), + ]; + } + if ($this->getMax() !== null && $this->getMax() < $length) { + return [ + (new Violation($this->getMaxMessage())) + ->addParameter('{{ limit }}', (string)$this->getMax()) + ->addParameter('{{ value }}', $stringValue) + ->addParameter('{{ stringLength }}', (string)$length), + ]; + } + + return []; + } +} diff --git a/lib/public/Validator/Constraints/NotBlank.php b/lib/public/Validator/Constraints/NotBlank.php new file mode 100644 index 0000000000000..b6e62fe2eef88 --- /dev/null +++ b/lib/public/Validator/Constraints/NotBlank.php @@ -0,0 +1,60 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use OCP\Validator\Violation; + +class NotBlank extends Constraint { + private string $message; + private bool $allowNull; + + /** + * @param string|null $message Overwrite the default translated error message + * to use when the constraint is not fulfilled. + */ + public function __construct(bool $allowNull = false, ?string $message = null) { + parent::__construct(); + $this->allowNull = $allowNull; + $this->message = $message === null ? $this->l10n->t('The value is blank') : $message; + } + + public function allowNull(): bool { + return $this->allowNull; + } + + public function getMessage(): string { + return $this->message; + } + + public function validate($value): array { + if ($this->allowNull() && null === $value) { + return []; + } + + if (false === $value || (empty($value) && '0' != $value)) { + return [ + (new Violation($this->getMessage())) + ]; + } + return []; + } +} diff --git a/lib/public/Validator/Constraints/Url.php b/lib/public/Validator/Constraints/Url.php new file mode 100644 index 0000000000000..2ea22e3f225b4 --- /dev/null +++ b/lib/public/Validator/Constraints/Url.php @@ -0,0 +1,94 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator\Constraints; + +use OCP\Validator\Violation; + +class Url extends Constraint { + /** @var string[] */ + private array $protocols; + private bool $relativeUrl; + private string $message; + + public const PATTERN = '~^ + (%s):// # protocol + (((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+:)?((?:[\_\.\pL\pN-]|%%[0-9A-Fa-f]{2})+)@)? # basic auth + ( + (?: + (?:xn--[a-z0-9-]++\.)*+xn--[a-z0-9-]++ # a domain name using punycode + | + (?:[\pL\pN\pS\pM\-\_]++\.)+[\pL\pN\pM]++ # a multi-level domain name + | + [a-z0-9\-\_]++ # a single-level domain name + )\.? + | # or + \d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3} # an IP address + | # or + \[ + (?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))) + \] # an IPv6 address + ) + (:[0-9]+)? # a port (optional) + (?:/ (?:[\pL\pN\-._\~!$&\'()*+,;=:@]|%%[0-9A-Fa-f]{2})* )* # a path + (?:\? (?:[\pL\pN\-._\~!$&\'\[\]()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a query (optional) + (?:\# (?:[\pL\pN\-._\~!$&\'()*+,;=:@/?]|%%[0-9A-Fa-f]{2})* )? # a fragment (optional) + $~ixu'; + + /** + * @param string|null $message Overwrite the default translated error message + * to use when the constraint is not fulfilled. + */ + public function __construct(bool $relativeUrl = false, array $protocols = ['http', 'https'], ?string $message = null) { + parent::__construct(); + $this->protocols = $protocols; + $this->message = $message === null ? $this->l10n->t('"{{ value }}" is not an url') : $message; + $this->relativeUrl = $relativeUrl; + } + + public function getProtocols(): array { + return $this->protocols; + } + + public function isRelativeUrl(): bool { + return $this->relativeUrl; + } + + public function getMessage(): string { + return $this->message; + } + + public function validate($value): array { + if (!is_scalar($value) && !(\is_object($value) && method_exists($value, '__toString'))) { + throw new \RuntimeException('The UrlValidator can only validate scalar values or object convertible to string.'); + } + + $stringValue = (string)$value; + + $pattern = $this->isRelativeUrl() ? str_replace('(%s):', '(?:(%s):)?', static::PATTERN) : static::PATTERN; + $pattern = sprintf($pattern, implode('|', $this->getProtocols())); + + if (!preg_match($pattern, $stringValue)) { + return [(new Violation($this->getMessage()))->addParameter('{{ value }}', $stringValue)]; + } + return []; + } +} diff --git a/lib/public/Validator/IConstraintValidator.php b/lib/public/Validator/IConstraintValidator.php new file mode 100644 index 0000000000000..73d2b89491cf2 --- /dev/null +++ b/lib/public/Validator/IConstraintValidator.php @@ -0,0 +1,30 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator; + +interface IConstraintValidator { + /** + * @param mixed The value to validate + * @return Violation[] An array of violations + */ + public function validate($value): array; +} diff --git a/lib/public/Validator/IValidator.php b/lib/public/Validator/IValidator.php new file mode 100644 index 0000000000000..9e76ff909a4c0 --- /dev/null +++ b/lib/public/Validator/IValidator.php @@ -0,0 +1,43 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator; + +interface IValidator { + /** + * Validate a value according to one or more constraints. + * + * @param mixed $value The value to validate + * @param IConstraintValidator[] $constraints The validator constraints for the value + * @return Violation[] An array of constraints violations. Empty if the value + * is conforming to every constrains. + */ + public function validate($value, array $constraints): array; + + /** + * Validate a value according to one or more constraints. This + * + * @param mixed $value The value to validate + * @param IConstraintValidator[] $constraints The validator constraints for the value + * @return bool Whether the value is valid + */ + public function isValid($value, array $constraints): bool; +} diff --git a/lib/public/Validator/Violation.php b/lib/public/Validator/Violation.php new file mode 100644 index 0000000000000..632810100aff1 --- /dev/null +++ b/lib/public/Validator/Violation.php @@ -0,0 +1,65 @@ + + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCP\Validator; + +/** + * This object represents a constraint violation when validating a value. + */ +class Violation { + private string $message; + private array $parameters; + + public function __construct(string $message) { + $this->message = $message; + $this->parameters = []; + } + + /** + * Returns the violation message. This can be directly displayed to the + * user, if wanted. + */ + public function getMessage(): string { + $message = $this->message; + foreach ($this->parameters as $value => $representation) { + $message = str_replace($representation, $value, $message); + } + return $message; + } + + /** + * Inject a parameter inside the violation message. + * + * This allows to inject dynamic information in the violation message. + * + * ```php + * $violation = new Violation('This value should be less than {{ max }}.'); + * $violation->addParameter('{{ max }}', 100); + * assert($violation->getMessage() === 'This value should be less than 100.') + * ``` + */ + public function addParameter(string $representation, string $value): self { + $this->parameters[] = [ + $representation => $value, + ]; + return $this; + } +} diff --git a/tests/lib/Validator/ValidatorTest.php b/tests/lib/Validator/ValidatorTest.php new file mode 100644 index 0000000000000..8474890810fee --- /dev/null +++ b/tests/lib/Validator/ValidatorTest.php @@ -0,0 +1,123 @@ + + * Copyright (c) 2014 Vincent Petry + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +namespace Test\Validator; + +use OCP\Validator\Constraints\Email; +use OCP\Validator\Constraints\Length; +use OCP\Validator\Constraints\NotBlank; +use OCP\Validator\Constraints\Url; +use OCP\Validator\IValidator; +use Test\TestCase; + +class ValidatorTest extends TestCase { + public function testEmailConstraint() { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate('carl@example.org', [ + new NotBlank(), + new Email(), + ]); + + $this->assertEmpty($violations); + } + + public function testNotBlankConstraintInvalid() { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate('', [ + new NotBlank(), + ]); + + $this->assertEquals(1, count($violations)); + $this->assertEquals('The value is blank', $violations[0]->getMessage()); + } + + /** + * @dataProvider urlProviderValid + */ + public function testUrl($url, $relativeUrl, $protocols) { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate($url, [ + new Url($relativeUrl, $protocols), + ]); + + $this->assertEmpty($violations); + } + + /** + * @dataProvider urlProviderInvalid + */ + public function testUrlInvalid($url, $relativeUrl, $protocols) { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate($url, [ + new Url($relativeUrl, $protocols), + ]); + + $this->assertEquals(1, count($violations)); + } + /** + * @dataProvider lengthProviderValid + */ + public function testLengthValid($value, $options) { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate($value, [ + new Length($options) + ]); + + $this->assertEmpty($violations); + } + + public function lengthProviderValid(): array { + return [ + ['helloworld', ['max' => 300, 'min' => 2]], + ['helloworld', ['exact' => 10]], + ]; + } + + /** + * @dataProvider lengthProviderInvalid + */ + public function testLengthInvalid($value, $options) { + /** @var IValidator $validator */ + $validator = \OC::$server->get(IValidator::class); + $violations = $validator->validate($value, [ + new Length($options) + ]); + + $this->assertEquals(1, count($violations)); + } + + public function lengthProviderInvalid(): array { + return [ + ['helloworld', ['max' => 2]], + ['helloworld', ['min' => 300]], + ['helloworld', ['exact' => 300]], + ]; + } + + public function urlProviderValid(): array { + return [ + ['https://hello.world', false, ['https']], + ['http://hello.world', false, ['http']], + ['http://⌘.ws/', false, ['http']], + ['http://➡.ws/䨹', false, ['http']], + ]; + } + + public function urlProviderInvalid(): array { + return [ + ['example.com/legal', false, ['http', 'https']], # missing scheme + ['https:///legal', false, ['https']], # missing host + ]; + } +}