Compare commits

...

18 Commits

26 changed files with 778 additions and 290 deletions

14
.idea/php.xml generated
View File

@ -21,20 +21,16 @@
<path value="$PROJECT_DIR$/vendor/nulib/phpss" /> <path value="$PROJECT_DIR$/vendor/nulib/phpss" />
<path value="$PROJECT_DIR$/vendor/sebastian/code-unit" /> <path value="$PROJECT_DIR$/vendor/sebastian/code-unit" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" /> <path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" /> <path value="$PROJECT_DIR$/vendor/symfony/deprecation-contracts" />
<path value="$PROJECT_DIR$/vendor/sebastian/complexity" /> <path value="$PROJECT_DIR$/vendor/sebastian/complexity" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" /> <path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" />
<path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" /> <path value="$PROJECT_DIR$/vendor/sebastian/recursion-context" />
<path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" />
<path value="$PROJECT_DIR$/vendor/theseer/tokenizer" /> <path value="$PROJECT_DIR$/vendor/theseer/tokenizer" />
<path value="$PROJECT_DIR$/vendor/myclabs/php-enum" />
<path value="$PROJECT_DIR$/vendor/sebastian/global-state" /> <path value="$PROJECT_DIR$/vendor/sebastian/global-state" />
<path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" /> <path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" />
<path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" /> <path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" />
<path value="$PROJECT_DIR$/vendor/markbaker/complex" /> <path value="$PROJECT_DIR$/vendor/markbaker/complex" />
<path value="$PROJECT_DIR$/vendor/sebastian/diff" /> <path value="$PROJECT_DIR$/vendor/sebastian/diff" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" /> <path value="$PROJECT_DIR$/vendor/sebastian/object-enumerator" />
<path value="$PROJECT_DIR$/vendor/symfony/yaml" /> <path value="$PROJECT_DIR$/vendor/symfony/yaml" />
<path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" /> <path value="$PROJECT_DIR$/vendor/sebastian/object-reflector" />
@ -44,7 +40,6 @@
<path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" /> <path value="$PROJECT_DIR$/vendor/sebastian/lines-of-code" />
<path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" /> <path value="$PROJECT_DIR$/vendor/phpoffice/phpspreadsheet" />
<path value="$PROJECT_DIR$/vendor/sebastian/type" /> <path value="$PROJECT_DIR$/vendor/sebastian/type" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/sebastian/environment" /> <path value="$PROJECT_DIR$/vendor/sebastian/environment" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-timer" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-timer" />
<path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" /> <path value="$PROJECT_DIR$/vendor/phpunit/php-file-iterator" />
@ -64,6 +59,15 @@
<path value="$PROJECT_DIR$/vendor/psr/http-factory" /> <path value="$PROJECT_DIR$/vendor/psr/http-factory" />
<path value="$PROJECT_DIR$/vendor/composer" /> <path value="$PROJECT_DIR$/vendor/composer" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" /> <path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/doctrine/instantiator" />
<path value="$PROJECT_DIR$/vendor/ezyang/htmlpurifier" />
<path value="$PROJECT_DIR$/vendor/myclabs/php-enum" />
<path value="$PROJECT_DIR$/vendor/symfony/polyfill-mbstring" />
<path value="$PROJECT_DIR$/vendor/sebastian/resource-operations" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" />
<path value="$PROJECT_DIR$/vendor/nulib/php" /> <path value="$PROJECT_DIR$/vendor/nulib/php" />
<path value="$PROJECT_DIR$/vendor/nulib/spout" /> <path value="$PROJECT_DIR$/vendor/nulib/spout" />
</include_path> </include_path>

View File

@ -1,3 +1,17 @@
## Release 0.5.1p82 du 12/05/2025-15:37
## Release 0.5.1p74 du 12/05/2025-15:34
* `39b2fa5` générifier la méthode d'authentification
* `33a10a3` ajout de table-sticky
* `f7e692d` déplacer les fonctions vers nulib
* `0c40769` améliorer Cursor
* `a120143` possibilité de sauter une cellule
* `fea5cb8` corrections tarray
* `5862d35` maj doc
## Release 0.5.0p82 du 30/04/2025-05:35
## Release 0.5.0p74 du 30/04/2025-05:30 ## Release 0.5.0p74 du 30/04/2025-05:30
* `2d73f4d` documenter showmorePlugin * `2d73f4d` documenter showmorePlugin

51
README.md Normal file
View File

@ -0,0 +1,51 @@
# nur-ture
## Release
Exemple: release de la version 0.6.0
Avant de faire une release majeure sur nur/ture, faire d'abord la release
majeure correspondante sur
* nulib/php
* nulib/spout
* nulib/phpss
~~~sh
version=0.6.0
major="${version%.*}.0"
## branche dev74
git checkout dev74
sed -ri "\
/nulib\/.*:/s/[0-9]+.[0-9]+.0p74/${major}p74/
" .composer.pman.yml
pci "maj projet"
prel -v$version
# en cas de conflit, sélectionner HEAD
_merge82
## branche dev82
git checkout dev82
sed -ri "\
/nulib\/.*:/s/[0-9]+.[0-9]+.0p82/${major}p82/
" .composer.pman.yml
pci "maj projet"
prel -C
commit="$(git log --grep="Init changelog . version ${version}p82" --format=%H)" &&
echo "commit=$commit"
git checkout dev74
git cherry-pick "$commit"
pp -a
~~~
-*- coding: utf-8 mode: markdown -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8:noeol:binary

View File

@ -1 +1 @@
0.5.0 0.5.1

View File

@ -3,6 +3,18 @@
"type": "library", "type": "library",
"description": "espace de maturation pour les librairies", "description": "espace de maturation pour les librairies",
"repositories": [ "repositories": [
{
"type": "path",
"url": "../nulib"
},
{
"type": "path",
"url": "../nulib-spout"
},
{
"type": "path",
"url": "../nulib-phpss"
},
{ {
"type": "composer", "type": "composer",
"url": "https://repos.univ-reunion.fr/composer" "url": "https://repos.univ-reunion.fr/composer"
@ -18,9 +30,9 @@
"php": "^7.4" "php": "^7.4"
}, },
"require-dev": { "require-dev": {
"nulib/php": "^0.5.0p74", "nulib/php": "^7.4-dev",
"nulib/spout": "^0.5.0p74", "nulib/spout": "^7.4-dev",
"nulib/phpss": "^0.5.0p74", "nulib/phpss": "^7.4-dev",
"nulib/tests": "^7.4", "nulib/tests": "^7.4",
"ext-posix": "*", "ext-posix": "*",
"ext-pcntl": "*", "ext-pcntl": "*",

93
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "b4bd340f94d33a320d66b249b1c21edb", "content-hash": "0b1e015d12aecf1cdfbdc6a702d83d25",
"packages": [], "packages": [],
"packages-dev": [ "packages-dev": [
{ {
@ -585,11 +585,11 @@
}, },
{ {
"name": "nulib/php", "name": "nulib/php",
"version": "0.5.0p74", "version": "dev-dev74",
"source": { "dist": {
"type": "git", "type": "path",
"url": "https://git.univ-reunion.fr/sda-php/nulib.git", "url": "../nulib",
"reference": "4037bf20424eb48708e5fdf9fc8e10f2ef71d134" "reference": "c8dcc6fe279fe3589ca20d13bcbbdc9be2b50285"
}, },
"require": { "require": {
"ext-json": "*", "ext-json": "*",
@ -629,18 +629,20 @@
} }
], ],
"description": "fonctions et classes essentielles", "description": "fonctions et classes essentielles",
"time": "2025-04-30T00:32:10+00:00" "transport-options": {
"relative": true
}
}, },
{ {
"name": "nulib/phpss", "name": "nulib/phpss",
"version": "0.5.0p74", "version": "dev-dev74",
"source": { "dist": {
"type": "git", "type": "path",
"url": "https://git.univ-reunion.fr/sda-php/nulib-phpss.git", "url": "../nulib-phpss",
"reference": "26b4bfddf5646f9313d419e568cd930efb9353eb" "reference": "6fd59afb257e8add4d6b5f524ab2f90a6531c9c7"
}, },
"require": { "require": {
"nulib/php": "^0.5.0p74", "nulib/php": "^7.4-dev",
"php": "^7.4", "php": "^7.4",
"phpoffice/phpspreadsheet": "^1.0" "phpoffice/phpspreadsheet": "^1.0"
}, },
@ -671,15 +673,17 @@
} }
], ],
"description": "wrapper pour phpoffice/phpspreadsheet", "description": "wrapper pour phpoffice/phpspreadsheet",
"time": "2025-04-30T00:46:31+00:00" "transport-options": {
"relative": true
}
}, },
{ {
"name": "nulib/spout", "name": "nulib/spout",
"version": "0.5.0p74", "version": "dev-dev74",
"source": { "dist": {
"type": "git", "type": "path",
"url": "https://git.univ-reunion.fr/sda-php/nulib-spout.git", "url": "../nulib-spout",
"reference": "e650e27abe571553424524633deada32747d33a6" "reference": "242abe737b6cfe186c6d376e904a45a3d3f6f8cd"
}, },
"require": { "require": {
"ext-dom": "*", "ext-dom": "*",
@ -687,7 +691,7 @@
"ext-libxml": "*", "ext-libxml": "*",
"ext-xmlreader": "*", "ext-xmlreader": "*",
"ext-zip": "*", "ext-zip": "*",
"nulib/php": "^0.5.0p74", "nulib/php": "^7.4-dev",
"php": "^7.4" "php": "^7.4"
}, },
"replace": { "replace": {
@ -725,7 +729,9 @@
} }
], ],
"description": "wrapper pour openspout/openspout", "description": "wrapper pour openspout/openspout",
"time": "2025-04-30T00:40:11+00:00" "transport-options": {
"relative": true
}
}, },
{ {
"name": "nulib/tests", "name": "nulib/tests",
@ -1310,16 +1316,16 @@
}, },
{ {
"name": "phpunit/phpunit", "name": "phpunit/phpunit",
"version": "9.6.22", "version": "9.6.23",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git", "url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/43d2cb18d0675c38bd44982a5d1d88f6d53d8d95",
"reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", "reference": "43d2cb18d0675c38bd44982a5d1d88f6d53d8d95",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
@ -1330,7 +1336,7 @@
"ext-mbstring": "*", "ext-mbstring": "*",
"ext-xml": "*", "ext-xml": "*",
"ext-xmlwriter": "*", "ext-xmlwriter": "*",
"myclabs/deep-copy": "^1.12.1", "myclabs/deep-copy": "^1.13.1",
"phar-io/manifest": "^2.0.4", "phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1", "phar-io/version": "^3.2.1",
"php": ">=7.3", "php": ">=7.3",
@ -1393,7 +1399,7 @@
"support": { "support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues", "issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy", "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.23"
}, },
"funding": [ "funding": [
{ {
@ -1404,12 +1410,20 @@
"url": "https://github.com/sebastianbergmann", "url": "https://github.com/sebastianbergmann",
"type": "github" "type": "github"
}, },
{
"url": "https://liberapay.com/sebastianbergmann",
"type": "liberapay"
},
{
"url": "https://thanks.dev/u/gh/sebastianbergmann",
"type": "thanks_dev"
},
{ {
"url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit",
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-12-05T13:48:26+00:00" "time": "2025-05-02T06:40:34+00:00"
}, },
{ {
"name": "psr/http-client", "name": "psr/http-client",
@ -2654,7 +2668,7 @@
}, },
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.31.0", "version": "v1.32.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git", "url": "https://github.com/symfony/polyfill-ctype.git",
@ -2713,7 +2727,7 @@
"portable" "portable"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0"
}, },
"funding": [ "funding": [
{ {
@ -2733,19 +2747,20 @@
}, },
{ {
"name": "symfony/polyfill-mbstring", "name": "symfony/polyfill-mbstring",
"version": "v1.31.0", "version": "v1.32.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git", "url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-iconv": "*",
"php": ">=7.2" "php": ">=7.2"
}, },
"provide": { "provide": {
@ -2793,7 +2808,7 @@
"shim" "shim"
], ],
"support": { "support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0"
}, },
"funding": [ "funding": [
{ {
@ -2809,7 +2824,7 @@
"type": "tidelift" "type": "tidelift"
} }
], ],
"time": "2024-09-09T11:45:10+00:00" "time": "2024-12-23T08:48:59+00:00"
}, },
{ {
"name": "symfony/yaml", "name": "symfony/yaml",
@ -2939,7 +2954,11 @@
], ],
"aliases": [], "aliases": [],
"minimum-stability": "stable", "minimum-stability": "stable",
"stability-flags": [], "stability-flags": {
"nulib/php": 20,
"nulib/spout": 20,
"nulib/phpss": 20
},
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {

View File

@ -63,21 +63,14 @@ Application::run(new class extends Application {
$storage = new SqliteStorage($dbfile); $storage = new SqliteStorage($dbfile);
$db = $storage->db(); $db = $storage->db();
$haveChannels = $storage->tableExists("_channels");
$name = $this->name; $name = $this->name;
$channelClass = $this->channelClass; $channelClass = $this->channelClass;
$tableName = $this->tableName; $tableName = $this->tableName;
if ($name !== null) { if ($name !== null) {
$row = null; if (!$storage->channelExists($name, $row)) {
if ($haveChannels) { self::die("$name: nom de canal de données introuvable");
$row = $db->one([
"select from _channels",
"where" => ["name" => $name],
]);
} }
if ($row === null) self::die("$name: nom de canal de données introuvable"); if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"];
if ($row["class"] !== "class@anonymous") $channelClass = $row["class"];
else $tableName = $row["table_name"]; else $tableName = $row["table_name"];
} }
if ($channelClass !== null) { if ($channelClass !== null) {
@ -92,17 +85,12 @@ Application::run(new class extends Application {
}; };
} else { } else {
$found = false; $found = false;
if ($haveChannels) { foreach ($storage->getChannels() as $row) {
$rows = $db->all([ msg::print($row["name"]);
"select from _channels", $found = true;
]);
foreach ($rows as $row) {
msg::print($row["name"]);
$found = true;
}
} }
if (!$found) self::die("Vous devez spécifier le canal de données"); if ($found) self::exit();
else self::exit(); self::die("Vous devez spécifier le canal de données");
} }
$capacitor = new Capacitor($storage, $channel); $capacitor = new Capacitor($storage, $channel);

View File

@ -34,6 +34,12 @@
.left-gap { margin-left: 1em;} .left-gap { margin-left: 1em;}
.right-gap { margin-right: 1em;} .right-gap { margin-right: 1em;}
table.table-sticky tr th {
position: sticky;
top: 0;
background-color: white;
}
/* si un navbar-form contient des btn-sm ou des btn-xs, utiliser les classes ci-dessous. */ /* si un navbar-form contient des btn-sm ou des btn-xs, utiliser les classes ci-dessous. */
.navbar-form-sm { margin-top: 10px; margin-bottom: 10px; } .navbar-form-sm { margin-top: 10px; margin-bottom: 10px; }
.navbar-form-xs { margin-top: 14px; margin-bottom: 14px; } .navbar-form-xs { margin-top: 14px; margin-bottom: 14px; }

View File

@ -1,12 +1,22 @@
<?php <?php
namespace nur\b\authnz; namespace nur\b\authnz;
use nur\authz;
use nur\b\IllegalAccessException; use nur\b\IllegalAccessException;
use nur\b\ValueException; use nur\b\ValueException;
use nur\config; use nur\config;
use nur\cookie; use nur\cookie;
use nur\F;
use nur\msg;
use nur\P;
use nur\session; use nur\session;
use nur\v\fo;
use nur\v\ly;
use nur\v\page; use nur\v\page;
use nur\v\v;
use nur\v\vo;
use nur\v\vp\AInitAuthzPage;
use nur\v\vp\AInitPage;
/** /**
* Interface IAuthzManager: gestionnaire d'authentification et d'autorisation * Interface IAuthzManager: gestionnaire d'authentification et d'autorisation
@ -53,7 +63,7 @@ class AuthzManager {
function checkCookie(?string &$username=null, ?string &$authType=null): bool { function checkCookie(?string &$username=null, ?string &$authType=null): bool {
$value = cookie::get($this->getCookieKey(), false); $value = cookie::get($this->getCookieKey(), false);
if ($value === false) return false; if ($value === false) return false;
if (!preg_match('/^(cas|form|):(.*)$/', $value, $ms)) return false; if (!preg_match('/^(cas|form|ext|):(.*)$/', $value, $ms)) return false;
$authType = $ms[1]; $authType = $ms[1];
$username = $ms[2]; $username = $ms[2];
return true; return true;
@ -191,6 +201,22 @@ class AuthzManager {
page::redirect(page::bu($loginUrl, $params)); page::redirect(page::bu($loginUrl, $params));
} }
function extLogin(string $username): bool {
# l'utilisateur doit exister
$user = $this->getUserManager()->getAuthzUser($username, null);
# ce doit être un utilisateur valide
if ($user !== null && $user->isValid()) {
$this->setCookie($username, "ext");
$this->initSession(self::STATUS_INITIAL);
session::set(self::SESSION_KEY_USERNAME, $username);
$this->onAuthOk($username);
session::set(self::SESSION_KEY_USER, $user);
$this->onAuthzOk($user);
return true;
}
return false;
}
function formLogin(string $username, string $password): bool { function formLogin(string $username, string $password): bool {
# l'utilisateur doit exister # l'utilisateur doit exister
$user = $this->getUserManager()->getAuthzUser($username, null); $user = $this->getUserManager()->getAuthzUser($username, null);
@ -202,7 +228,7 @@ class AuthzManager {
if (config::is_devel() && !$password) $password = null; if (config::is_devel() && !$password) $password = null;
if ($password === null || $user->validatePassword($password)) { if ($password === null || $user->validatePassword($password)) {
# c'est bon # c'est bon
$this->initSession(self::STATUS_INITIAL, null); $this->initSession(self::STATUS_INITIAL);
session::set(self::SESSION_KEY_USERNAME, $username); session::set(self::SESSION_KEY_USERNAME, $username);
$this->onAuthOk($username); $this->onAuthOk($username);
session::set(self::SESSION_KEY_USER, $user); session::set(self::SESSION_KEY_USER, $user);
@ -221,14 +247,11 @@ class AuthzManager {
$this->onAuthOk($username); $this->onAuthOk($username);
# l'utilisateur doit exister # l'utilisateur doit exister
$user = $this->getUserManager()->getAuthzUser($username, $overrides); $user = $this->getUserManager()->getAuthzUser($username, $overrides);
if ($user !== null) { # ce doit être un utilisteur valide
# ce doit être un utilisteur valide if ($user !== null && $user->isValid()) {
if ($user->isValid()) { session::set(self::SESSION_KEY_USER, $user);
# c'est bon $this->onAuthzOk($user);
session::set(self::SESSION_KEY_USER, $user); return true;
$this->onAuthzOk($user);
return true;
}
} }
return false; return false;
} }
@ -317,4 +340,83 @@ class AuthzManager {
/** Traiter le cas où l'utilisateur a été autorisé avec succès. */ /** Traiter le cas où l'utilisateur a été autorisé avec succès. */
function onAuthzOk(IAuthzUser $authz): void { function onAuthzOk(IAuthzUser $authz): void {
} }
#############################################################################
# Page login
private $username = null;
private $authType = null;
function beforeSetup(AInitAuthzPage $page): void {
# initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
# setup() et afterSetup(), la session est disponible
$username = P::get("u");
$password = P::get("p");
$destPage = F::get("d", $page->getMainUrl());
$page->_ensureFormLoginAndRedirect($username, $password, $destPage);
$this->checkSession($this->username, $this->authType);
}
function print(AInitAuthzPage $page): void {
page::no_cache();
$username = P::get("u");
$password = P::get("p");
ly::row();
vo::h1(["class" => "text-center", q($page->TITLE())]);
ly::col(["sm" => 6, "sm-push" => 3]);
$status = $this->getStatus();
switch ($status) {
case authz::DISCONNECTED:
msg::warning("Vous avez été déconnecté. Veuillez vous reconnecter");
break;
case authz::UNAUTHORIZED:
msg::error(["user" => [
"Connecté en tant que ",
v::b($this->getAuth()),
", vous n'êtes pas autorisé à accéder à la page que vous avez demandé.",
]]);
break;
}
ly::panel("Connexion par identifiant/mot de passe");
fo::start([
"type" => "basic",
"action" => "",
"method" => "post",
]);
fo::text("Identifiant", "u", $username?: $this->username, [
"accesskey" => "q",
"placeholder" => "Votre identifiant",
]);
fo::password("Mot de passe", "p", $password, [
"placeholder" => "Votre mot de passe",
]);
if ($username || $password) {
msg::error("$username: Votre identifiant et/ou votre mot de passe sont incorrects");
} elseif ($username === "") {
msg::error("Vous devez saisir votre identifiant");
} elseif ($password === "") {
msg::error("Vous devez saisir votre mot de passe");
}
fo::submit(["Connexion", "accesskey" => "r"]);
if ($this->isAuth() && $this->authType === "form") {
if ($status != authz::UNAUTHORIZED) {
msg::warning(["user" => [
"Connecté en tant que ",
v::b($this->getAuth()),
", vous n'êtes pas autorisé à accéder à cette application.",
]]);
}
fo::submit([
"Vous déconnecter", "accesskey" => "z",
"formmethod" => "get", "formaction" => $page->getLogoutUrl(),
]);
}
fo::end();
ly::end();
}
} }

View File

@ -1,8 +1,18 @@
<?php <?php
namespace nur\b\authnz; namespace nur\b\authnz;
use nur\authz;
use nur\config; use nur\config;
use nur\F;
use nur\func; use nur\func;
use nur\msg;
use nur\v\fo;
use nur\v\icon;
use nur\v\ly;
use nur\v\page;
use nur\v\v;
use nur\v\vo;
use nur\v\vp\AInitAuthzPage;
/** /**
* Class CasAuthzManager: un utilisateur authentifié par CAS v3 * Class CasAuthzManager: un utilisateur authentifié par CAS v3
@ -28,4 +38,93 @@ class CasAuthzManager extends AuthzManager {
} }
return $this->userManager; return $this->userManager;
} }
private $destPage = null;
function beforeSetup(AInitAuthzPage $page): void {
# initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
# setup() et afterSetup(), la session est disponible
$this->destPage = F::get("d", $page->getMainUrl());
$this->checkSession($username, $authType);
if ($authType === "cas" && F::get("a")) {
# autologin
$casauthUrl = config::k("url")."/".$page->getCasauthUrl();
page::redirect(page::bu($page->getCasLoginUrl(), [
"service" => page::bu($casauthUrl, [
"r" => $page->getLoginUrl(),
"d" => $this->destPage,
])
]));
}
}
function print(AInitAuthzPage $page): void {
page::no_cache();
ly::row();
vo::h1(["class" => "text-center", q($page->TITLE())]);
ly::col(["sm" => 6, "sm-push" => 3]);
$status = $this->getStatus();
switch ($status) {
case authz::DISCONNECTED:
msg::warning("Vous avez été déconnecté. Veuillez vous reconnecter");
break;
case authz::UNAUTHORIZED:
msg::error(["user" => [
"Connecté en tant que ",
v::b($this->getAuth()),
", vous n'êtes pas autorisé à accéder à la page que vous avez demandé.",
]]);
break;
}
ly::panel("Connexion par CAS");
if ($page->isDevauthAllowed()) {
fo::start([
"type" => "basic",
"action" => $page->getCasauthUrl(),
"method" => "get",
]);
fo::hidden("r", $page->getLoginUrl());
fo::hidden("d", $this->destPage);
} else {
fo::start([
"type" => "basic",
"action" => $page->getCasLoginUrl(),
"method" => "get",
]);
$casauthUrl = config::k("url")."/".$page->getCasauthUrl();
fo::hidden("service", page::bu($casauthUrl, [
"r" => $page->getLoginUrl(),
"d" => $this->destPage,
]));
}
#fo::p("Si vous avez un compte à l'université, vous pouvez vous connecter via CAS");
vo::p("Si vous avez un compte à l'université, vous pouvez vous connecter via CAS");
if ($this->isAuth()) {
if ($status != authz::UNAUTHORIZED) {
msg::warning(["user" => [
"Connecté en tant que ",
v::b(authz::get_auth()),
", vous n'êtes pas autorisé à accéder à cette application.",
]]);
}
fo::submit([
icon::logout("Vous déconnecter"),
"formaction" => $page->getLogoutUrl(),
"accesskey" => "z",
]);
fo::hidden("renew", "true");
fo::submit([
icon::login("Changer de compte"),
"accesskey" => "r",
]);
} else {
fo::submit(["Connexion par CAS", "accesskey" => "r"]);
}
fo::end();
ly::end();
}
} }

View File

@ -0,0 +1,78 @@
<?php
namespace nur\b\authnz;
use nur\authz;
use nur\config;
use nur\F;
use nur\msg;
use nur\v\fo;
use nur\v\ly;
use nur\v\page;
use nur\v\v;
use nur\v\vo;
use nur\v\vp\AInitAuthzPage;
class ExtAuthzManager extends AuthzManager {
const USER_MANAGER_CLASS = ExtUserManager::class;
private $destPage = null;
function beforeSetup(AInitAuthzPage $page): void {
# initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
# setup() et afterSetup(), la session est disponible
$this->destPage = F::get("d", $page->getMainUrl());
$this->checkSession($username, $authType);
if ($authType === "ext" && F::get("a")) {
# autologin
$extauthUrl = config::k("url")."/".$page->getExtauthUrl();
page::redirect(page::bu($extauthUrl, [
"d" => $this->destPage,
]));
}
}
function print(AInitAuthzPage $page): void {
page::no_cache();
ly::row();
vo::h1(["class" => "text-center", q($page->TITLE())]);
ly::col(["sm" => 6, "sm-push" => 3]);
$status = $this->getStatus();
switch ($status) {
case authz::DISCONNECTED:
msg::warning("Vous avez été déconnecté. Veuillez vous reconnecter");
break;
case authz::UNAUTHORIZED:
msg::error(["user" => [
"Connecté en tant que ",
v::b($this->getAuth()),
", vous n'êtes pas autorisé à accéder à la page que vous avez demandé.",
]]);
break;
}
ly::panel("Connexion");
fo::start([
"type" => "basic",
"action" => $page->getExtauthUrl(),
"method" => "get",
]);
fo::hidden("d", $this->destPage);
vo::p("Si vous avez un compte à l'université, vous pouvez vous connecter");
if ($this->isAuth()) {
if ($status != authz::UNAUTHORIZED) {
msg::warning(["user" => [
"Connecté en tant que ",
v::b($this->getAuth()),
", vous n'êtes pas autorisé à accéder à cette application.",
]]);
}
} else {
fo::submit(["Connexion", "accesskey" => "r"]);
}
fo::end();
ly::end();
}
}

View File

@ -0,0 +1,8 @@
<?php
namespace nur\b\authnz;
class ExtUserManager extends SimpleUserManager {
function _getUser(string $username): ?array {
return ["username" => $username];
}
}

View File

@ -633,14 +633,15 @@ class CTable extends ComponentPrintable implements IParametrable {
/** @var string|int clé de la colonne courante */ /** @var string|int clé de la colonne courante */
protected $col; protected $col;
function colTd($value): array { function colTd($value): ?array {
$vs = $this->col($value); $vs = $this->col($value);
if ($this->colCtx !== null) { if ($this->colCtx !== null) {
$vs = func::_call($this->colCtx, [$vs, $value, $this->col, $this->index, $this->row, $this->rawRow]); $vs = func::_call($this->colCtx, [$vs, $value, $this->col, $this->index, $this->row, $this->rawRow]);
if ($vs === false) return null;
} else { } else {
$result = A::get($this->results, $this->col); $result = A::get($this->results, $this->col);
$valid = $result === null || $result["valid"]; $valid = $result === null || $result["valid"];
$vs= [ $vs = [
"class" => ["danger" => !$valid], "class" => ["danger" => !$valid],
$vs, $vs,
]; ];

View File

@ -64,7 +64,7 @@ class AInitAuthzPage extends AInitPage implements IBasicPage {
} }
} }
protected function ensureAuthOrRedirect(bool $requireAuth, bool $requireAuthz, $requireRole, $requirePerm): void { protected function _ensureAuthOrRedirect(bool $requireAuth, bool $requireAuthz, $requireRole, $requirePerm): void {
$am = authz::manager(); $am = authz::manager();
$loginUrl = $this->getLoginUrl(); $loginUrl = $this->getLoginUrl();
$destUrl = page::self(true); $destUrl = page::self(true);
@ -88,7 +88,7 @@ class AInitAuthzPage extends AInitPage implements IBasicPage {
if ($am->isAuth()) $am->setConnected(); if ($am->isAuth()) $am->setConnected();
} }
protected function ensureFormLoginAndRedirect(?string $username, ?string $password, string $destUrl): void { function _ensureFormLoginAndRedirect(?string $username, ?string $password, string $destUrl): void {
if ($username === null && $password === null) return; if ($username === null && $password === null) return;
if (authz::manager()->formLogin($username, $password)) { if (authz::manager()->formLogin($username, $password)) {
page::redirect($destUrl); page::redirect($destUrl);

View File

@ -40,6 +40,11 @@ abstract class AInitPage extends AbstractPage {
} }
} }
/** obtenir l'url d'authentification externe. cet url doit être *relatif* */
function getExtauthUrl(): string {
return page::bu(config::k("extauth_page", "_extauth.php"));
}
function getLogoutUrl(): string { function getLogoutUrl(): string {
return page::bu(config::k("logout_page","_logout.php")); return page::bu(config::k("logout_page","_logout.php"));
} }

View File

@ -0,0 +1,68 @@
<?php
namespace nur\v\vp;
use nur\A;
use nur\authz;
use nur\config;
use nur\F;
use nur\v\base\AbstractPage;
use nur\v\page;
class AppExtauthPage extends AbstractPage {
/**
* @var bool faut-il afficher les variables au lieu de rediriger vers
* $ret_url?
*/
const DEBUG = false;
function isDebug(): bool {
if (!config::is_devel()) return false;
return static::DEBUG || F::get("D");
}
/** @var string nom de l'utilisateur connecté */
private $user;
function setup(): void {
$destUrl = null;
$user = false;
if ($user === false) $user = A::get($_SERVER, "REMOTE_USER", false);
if ($user === false) $user = A::get($_SERVER, "HTTP_REMOTE_USER", false);
if ($user === false) $user = A::get($_SERVER, "HTTP_X_REMOTE_USER", false);
if ($user) {
if (authz::manager()->extLogin($user)) {
$destUrl = F::get("d");
if ($destUrl && !$this->isDebug()) page::redirect($destUrl);
}
} else {
$user = "NONE";
}
$this->user = $user;
$this->destUrl = $destUrl;
}
private $destUrl;
function print(): void {
page::content_type("text/plain");
page::no_cache();
if ($this->isDebug()) {
echo "destUrl: $this->destUrl\n";
echo "--- \$_SERVER\n\n";
foreach ($_SERVER as $name => $value) {
echo "$name: $value\n";
}
echo "\n--- \$_REQUEST\n\n";
foreach ($_REQUEST as $name => $value) {
echo "$name: $value\n";
}
echo "\n--- \$_SESSION\n\n";
foreach ($_SESSION as $name => $value) {
echo "$name: ".var_export($value, true)."\n";
}
} else {
echo $this->user;
}
}
}

View File

@ -13,7 +13,7 @@ class BasicPage extends AInitAuthzPage {
function afterConfig(): void { function afterConfig(): void {
# initialiser la session avant setup. ainsi, dans les fonction beforeSetup(), # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
# setup() et afterSetup(), la session est disponible # setup() et afterSetup(), la session est disponible
$this->ensureAuthOrRedirect(static::REQUIRE_AUTH, static::REQUIRE_AUTHZ, static::REQUIRE_ROLE, static::REQUIRE_PERM); $this->_ensureAuthOrRedirect(static::REQUIRE_AUTH, static::REQUIRE_AUTHZ, static::REQUIRE_ROLE, static::REQUIRE_PERM);
parent::afterConfig(); parent::afterConfig();
} }
} }

View File

@ -32,7 +32,7 @@ class NavigablePage extends AInitAuthzPage implements INavigablePage {
function afterConfig(): void { function afterConfig(): void {
# initialiser la session avant setup. ainsi, dans les fonction beforeSetup(), # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
# setup() et afterSetup(), la session est disponible # setup() et afterSetup(), la session est disponible
$this->ensureAuthOrRedirect(static::REQUIRE_AUTH, static::REQUIRE_AUTHZ, static::REQUIRE_ROLE, static::REQUIRE_PERM); $this->_ensureAuthOrRedirect(static::REQUIRE_AUTH, static::REQUIRE_AUTHZ, static::REQUIRE_ROLE, static::REQUIRE_PERM);
parent::afterConfig(); parent::afterConfig();
} }

View File

@ -0,0 +1,19 @@
<?php
namespace nur\v\vp;
use nur\authz;
trait TAuthzLoginPage {
function TLoginPage_beforeSetup(): void {
authz::manager()->beforeSetup($this);
}
function beforeSetup(): void {
$this->TLoginPage_beforeSetup();
parent::beforeSetup();
}
function print(): void {
authz::manager()->print($this);
}
}

View File

@ -17,7 +17,7 @@ trait TFormLoginPage {
$username = P::get("u"); $username = P::get("u");
$password = P::get("p"); $password = P::get("p");
$destPage = F::get("d", $this->getMainUrl()); $destPage = F::get("d", $this->getMainUrl());
$this->ensureFormLoginAndRedirect($username, $password, $destPage); $this->_ensureFormLoginAndRedirect($username, $password, $destPage);
authz::manager()->checkSession($flcUsername, $flcAuthType); authz::manager()->checkSession($flcUsername, $flcAuthType);
$this->flcUsername = $flcUsername; $this->flcUsername = $flcUsername;

View File

@ -2,6 +2,7 @@
namespace nulib\php\access; namespace nulib\php\access;
use ArrayAccess; use ArrayAccess;
use nulib\A;
use nulib\cl; use nulib\cl;
/** /**
@ -118,85 +119,18 @@ class KeyAccess extends AbstractAccess {
} }
function ensureAssoc(array $keys, ?array $params=null): void { function ensureAssoc(array $keys, ?array $params=null): void {
$dest =& $this->dest; A::ensure_assoc($this->dest, $keys, $params);
$prefix = $params["key_prefix"] ?? null;
$suffix = $params["key_suffix"] ?? null;
$index = 0;
foreach ($keys as $key) {
if ($prefix !== null || $suffix !== null) {
$destKey = "$prefix$key$suffix";
} else {
# préserver les clés numériques
$destKey = $key;
}
if ($dest !== null && array_key_exists($destKey, $dest)) continue;
while (in_array($index, $keys, true)) {
$index++;
}
if ($dest !== null && array_key_exists($index, $dest)) {
$dest[$destKey] = $dest[$index];
unset($dest[$index]);
$index++;
}
}
} }
function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void {
$dest =& $this->dest; A::ensure_keys($this->dest, $defaults, $missings, $params);
$keys = array_keys($defaults);
$prefix = $params["key_prefix"] ?? null;
$suffix = $params["key_suffix"] ?? null;
foreach ($keys as $key) {
$destKey = "$prefix$key$suffix";
$haveMissing = $missings !== null && array_key_exists($key, $missings);
if ($dest === null || !array_key_exists($destKey, $dest)) {
$dest[$destKey] = $defaults[$key];
} elseif ($haveMissing && $dest[$destKey] === $missings[$key]) {
$dest[$destKey] = $defaults[$key];
}
}
} }
function deleteMissings(array $missings, ?array $params=null): void { function deleteMissings(array $missings, ?array $params=null): void {
$dest =& $this->dest; A::delete_missings($this->dest, $missings, $params);
$prefix = $params["key_prefix"] ?? null;
$suffix = $params["key_suffix"] ?? null;
foreach ($missings as $key => $missing) {
$destKey = "$prefix$key$suffix";
if (array_key_exists($destKey, $dest) && $dest[$destKey] === $missing) {
unset($dest[$destKey]);
}
}
} }
function ensureOrder(array $keys, ?array $params=null): void { function ensureOrder(array $keys, ?array $params=null): void {
$dest =& $this->dest; A::ensure_order($this->dest, $keys, $params);
if ($dest === null) return;
$prefix = $params["key_prefix"] ?? null;
$suffix = $params["key_suffix"] ?? null;
if ($prefix !== null || $suffix !== null) {
foreach ($keys as &$key) {
$key = "$prefix$key$suffix";
}; unset($key);
}
$destKeys = array_keys($dest);
$keyCount = count($keys);
if (array_slice($destKeys, 0, $keyCount) === $keys) {
# si le tableau a déjà les bonnes clés dans le bon ordre, rien à faire
return;
}
$ordered = [];
foreach ($keys as $key) {
if (array_key_exists($key, $dest)) {
$ordered[$key] = $dest[$key];
unset($dest[$key]);
}
}
$preserveKeys = $params["preserve_keys"] ?? false;
if ($preserveKeys) $dest = cl::merge2($ordered, $dest);
else $dest = array_merge($ordered, $dest);
} }
} }

View File

@ -11,6 +11,10 @@ use Traversable;
/** /**
* Class Cursor: parcours des lignes itérable * Class Cursor: parcours des lignes itérable
* *
* XXX si on spécifie $cols ou $colsFunc, il y a une possibilité que les clés de
* $row ne soient pas dans le bon ordre, ou que les clés de $cols ne soient pas
* présentes dans $row. ajouter les paramètres ensure_keys et order_keys
*
* @property-read array|null $value alias pour $row * @property-read array|null $value alias pour $row
* @property-read iterable|null $rows la source des lignes * @property-read iterable|null $rows la source des lignes
*/ */
@ -18,80 +22,14 @@ class Cursor implements Iterator {
const PARAMS_SCHEMA = [ const PARAMS_SCHEMA = [
"rows" => ["?iterable"], "rows" => ["?iterable"],
"rows_func" => ["?callable"], "rows_func" => ["?callable"],
"cols" => ["?array"],
"cols_func" => ["?callable"],
"map" => ["?array"],
"map_func" => ["?callable"],
"filter" => ["?array"], "filter" => ["?array"],
"filter_func" => ["?callable"], "filter_func" => ["?callable"],
"map" => ["?array"],
"map_func" => ["?callable"],
"cols" => ["?array"],
"cols_func" => ["?callable"],
]; ];
/**
* mapper le tableau source $row selon les règles suivantes illustrées dans
* l'exemple suivant:
* si
* $map = ["a", "b" => "x", "c" => function() { return "y"; }, "d" => null]
* alors retourner le tableau
* ["a" => $row["a"], "b" => $row["x"], "c" => "y", "d" => null]
*/
protected static function map_row(array $row, ?array $map): array {
if ($map === null) return $row;
$index = 0;
$mapped = [];
foreach ($map as $key => $value) {
if ($key === $index) {
$index++;
if ($value === null) $mapped[] = null;
else $mapped[$value] = cl::get($row, $value);
} elseif (is_callable($value)) {
$func = func::with($value);
$value = cl::get($row, $key);
$mapped[$key] = $func->invoke([$value, $key, $row]);
} else {
if ($value === null) $mapped[$key] = null;
else $mapped[$key] = cl::get($row, $value);
}
}
return $mapped;
}
/**
* tester si $row satisfait les conditions de $filter
* - $filter est un scalaire, le transformer en [$filter]
* - sinon $filter doit être un tableau de scalaires
*
* les règles des conditions sont les suivantes:
* - une valeur séquentielle $key est équivalente à la valeur associative
* $key => true
* - une valeur associative $key => bool indique que la clé correspondante ne
* doit pas (resp. doit) exister selon que bool vaut false (resp. true)
* - une valeur associative $key => $value indique que la clé correspondante
* doit exiter avec la valeur spécifiée
*/
protected static function filter_row(array $row, $filter): bool {
if ($filter === null) return false;
if (!is_array($filter)) $filter = [$filter];
if (!$filter) return false;
$index = 0;
foreach ($filter as $key => $value) {
if ($key === $index) {
$index++;
if (!array_key_exists($value, $row)) return false;
} elseif (is_bool($value)) {
if ($value) {
if (!array_key_exists($key, $row)) return false;
} else {
if (array_key_exists($key, $row)) return false;
}
} else {
if (!array_key_exists($key, $row)) return false;
if ($row[$key] !== $value) return false;
}
}
return true;
}
function __construct(?iterable $rows=null, ?array $params=null) { function __construct(?iterable $rows=null, ?array $params=null) {
if ($rows !== null) $params["rows"] = $rows; if ($rows !== null) $params["rows"] = $rows;
@ -103,7 +41,7 @@ class Cursor implements Iterator {
$rowsGenerator = $rowsFunc; $rowsGenerator = $rowsFunc;
$rowsFunc = null; $rowsFunc = null;
} else { } else {
$rowsFunc = func::with($rowsFunc, [$rows]); $rowsFunc = func::with($rowsFunc, [$rows, $this]);
} }
} elseif ($rows instanceof Traversable) { } elseif ($rows instanceof Traversable) {
$rowsGenerator = $rows; $rowsGenerator = $rows;
@ -115,61 +53,95 @@ class Cursor implements Iterator {
$this->rowsGenerator = $rowsGenerator; $this->rowsGenerator = $rowsGenerator;
$this->rowsFunc = $rowsFunc; $this->rowsFunc = $rowsFunc;
$this->cols = $params["cols"] ?? null; $filter = $params["filter"] ?? null;
$colsFunc = $params["cols_func"] ?? null; $filterFunc = $params["filter_func"] ?? null;
if ($colsFunc !== null) $colsFunc = func::with($colsFunc); if ($filterFunc !== null) $this->setFilterFunc($filterFunc);
$this->colsFunc = $colsFunc; elseif ($filter !== null) $this->setFilter($filter);
$map = $params["map"] ?? null; $map = $params["map"] ?? null;
$mapFunc = $params["map_func"] ?? null; $mapFunc = $params["map_func"] ?? null;
if ($mapFunc !== null) { if ($mapFunc !== null) $this->setMapFunc($mapFunc);
$mapFunc = func::with($mapFunc); elseif ($map !== null) $this->setMap($map);
} elseif ($map !== null) {
$mapFunc = func::with(function(array $row) use ($map) {
return self::map_row($row, $map);
});
}
$this->mapFunc = $mapFunc;
$filter = $params["filter"] ?? null; $this->cols = $params["cols"] ?? null;
$filterFunc = $params["filter_func"] ?? null; $this->setColsFunc($params["cols_func"] ?? null);
if ($filterFunc !== null) {
$filterFunc = func::with($filterFunc);
} elseif ($filter !== null) {
$filterFunc = func::with(function(array $row) use ($filter) {
return self::filter_row($row, $filter);
});
}
$this->filterFunc = $filterFunc;
} }
/** un générateur de lignes */ /** un générateur de lignes */
private ?Traversable $rowsGenerator; private ?Traversable $rowsGenerator;
/** une fonction de signature <code>function(Cursor): ?iterable</code> */ /** une fonction de signature <code>function(mixed $rows, Cursor): ?iterable</code> */
private ?func $rowsFunc; private ?func $rowsFunc;
/** une fonction de signature <code>function(Cursor): ?array</code> */ /** une fonction de signature <code>function(?array $row, Cursor): bool</code> */
private ?func $colsFunc; private ?func $filterFunc = null;
/** une fonction de signature <code>function(Cursor): ?array</code> */ function setFilter(array $filter): self {
private ?func $mapFunc; $this->filterFunc = func::with(function(?array $row) use ($filter) {
return cl::filter($row, $filter);
});
return $this;
}
/** une fonction de signature <code>function(Cursor): bool</code> */ function setFilterFunc(?callable $func): self {
private ?func $filterFunc; if ($func === null) $this->filterFunc = null;
else $this->filterFunc = func::with($func)->bind($this);
return $this;
}
/** une fonction de signature <code>function(?array $row, Cursor): ?array</code> */
private ?func $mapFunc = null;
function setMap(array $map): self {
$this->mapFunc = func::with(function(?array $row) use ($map) {
return cl::map($row, $map);
});
return $this;
}
function setMapFunc(?callable $func): self {
if ($func === null) $this->mapFunc = null;
else $this->mapFunc = func::with($func)->bind($this);
return $this;
}
/** une fonction de signature <code>function(?array $row, Cursor): ?array</code> */
private ?func $colsFunc = null;
function setColsFunc(?callable $func): self {
$this->cols = null;
if ($func === null) $this->colsFunc = null;
else $this->colsFunc = func::with($func)->bind($this);
return $this;
}
/** @var iterable|null source des éléments */
protected ?iterable $rows; protected ?iterable $rows;
/** @var array|null listes des colonnes de chaque enregistrement */
public ?array $cols; public ?array $cols;
/**
* @var int index de l'enregistrement (en ne comptant pas les éléments filtrés)
*/
public int $index; public int $index;
/**
* @var int index original de l'enregistrement (en tenant compte des éléments
* filtrés)
*/
public int $origIndex; public int $origIndex;
/** @var string|int clé de l'enregistrement */
public $key; public $key;
/** @var mixed élément original récupéré depuis la source */
public $raw; public $raw;
/**
* @var array|null enregistrement après conversion en tableau et application
* du mapping
*/
public ?array $row; public ?array $row;
function __get($name) { function __get($name) {
@ -179,18 +151,22 @@ class Cursor implements Iterator {
return null; return null;
} }
protected function cols(): ?array { protected function convertToRow($raw): ?array {
return $this->row !== null? array_keys($this->row): null; return cl::withn($raw);
} }
protected function filter(): bool { protected function filterFunc(?array $row): bool {
return false; return false;
} }
protected function map(): ?array { protected function mapFunc(?array $row): ?array {
return $this->row; return $this->row;
} }
protected function colsFunc(?array $row): ?array {
return $this->row !== null? array_keys($this->row): null;
}
############################################################################# #############################################################################
# Iterator # Iterator
@ -212,26 +188,30 @@ class Cursor implements Iterator {
} }
function valid(): bool { function valid(): bool {
$cols = $this->colsFunc; $colsFunc = $this->colsFunc;
$filter = $this->filterFunc; $filterFunc = $this->filterFunc;
$map = $this->mapFunc; $mapFunc = $this->mapFunc;
while ($valid = iter::valid($this->rows)) { while ($valid = iter::valid($this->rows)) {
$this->raw = iter::current($this->rows, $this->key); $this->raw = iter::current($this->rows, $this->key);
# conversion en enregistrement
$this->key ??= $this->origIndex; $this->key ??= $this->origIndex;
$this->row = cl::withn($this->raw); $this->row = $this->convertToRow($this->raw);
if ($filter === null) $filtered = $this->filter(); # filtrage
else $filtered = $filter->invoke([$this]); if ($filterFunc === null) $filtered = $this->filterFunc($this->row);
if (!$filtered) { else $filtered = $filterFunc->invoke([$this->row, $this]);
if ($map === null) $this->row = $this->map(); if ($filtered) {
else $this->row = $map->invoke([$this]);
if ($this->cols === null) {
if ($cols === null) $this->cols = $this->cols();
else $this->cols = $cols->invoke([$this]);
}
break;
} else {
iter::next($this->rows); iter::next($this->rows);
$this->origIndex++; $this->origIndex++;
} else {
# l'enregistrement n'as pas été filtré: faire le mapping
if ($mapFunc === null) $this->row = $this->mapFunc($this->row);
else $this->row = $mapFunc->invoke([$this->row, $this]);
# calculer la liste des colonnes le cas échéant
if ($this->cols === null) {
if ($colsFunc === null) $this->cols = $this->colsFunc($this->row);
else $this->cols = $colsFunc->invoke([$this->row, $this]);
}
break;
} }
} }
if (!$valid) { if (!$valid) {

View File

@ -127,7 +127,6 @@ abstract class Schema implements ArrayAccess {
} }
} }
# type # type
$types = [];
$deftype = $definition["type"]; $deftype = $definition["type"];
$nullable = $definition["nullable"] ?? false; $nullable = $definition["nullable"] ?? false;
if ($deftype === null) { if ($deftype === null) {
@ -138,7 +137,17 @@ abstract class Schema implements ArrayAccess {
if (!is_string($deftype)) throw SchemaException::invalid_type($deftype); if (!is_string($deftype)) throw SchemaException::invalid_type($deftype);
$deftype = explode("|", $deftype); $deftype = explode("|", $deftype);
} }
foreach ($deftype as $type) { $types = [];
$unionTypes = [];
$index = 0;
foreach ($deftype as $key => $type) {
$args = null;
if ($key === $index) {
$index++;
} else {
$args = $type;
$type = $key;
}
if ($type !== null) $type = trim($type); if ($type !== null) $type = trim($type);
if ($type === null || $type === "null") { if ($type === null || $type === "null") {
$nullable = true; $nullable = true;
@ -151,10 +160,18 @@ abstract class Schema implements ArrayAccess {
} }
if ($type === "") throw SchemaException::invalid_type($type); if ($type === "") throw SchemaException::invalid_type($type);
$type = cl::get(ref_types::ALIASES, $type, $type); $type = cl::get(ref_types::ALIASES, $type, $type);
$types = array_merge($types, explode("|", $type)); if ($args === null) {
$unionTypes = array_merge($unionTypes, explode("|", $type));
} else {
$types = array_merge($types, [$type => $args]);
}
}
if (!$types && !$unionTypes) throw SchemaException::invalid_schema("scalar: type is required");
foreach ($unionTypes as $type) {
if (!array_key_exists($type, $types)) {
$types[] = $type;
}
} }
if (!$types) throw SchemaException::invalid_schema("scalar: type is required");
$types = array_keys(array_fill_keys($types, true));
} }
$definition["type"] = $types; $definition["type"] = $types;
$definition["nullable"] = $nullable; $definition["nullable"] = $nullable;
@ -224,7 +241,7 @@ abstract class Schema implements ArrayAccess {
$types = $definition["type"]; $types = $definition["type"];
$nullable = $definition["nullable"]; $nullable = $definition["nullable"];
# s'il n'y a qu'une seul type, l'instancier tout de suite # s'il n'y a qu'une seul type, l'instancier tout de suite
if (is_array($types) && count($types) == 1 && $types[0] !== null) { if (is_array($types) && count($types) == 1 && cl::first($types) !== null) {
foreach ($types as $key => $name) { foreach ($types as $key => $name) {
if ($key === 0) { if ($key === 0) {
$args = null; $args = null;

View File

@ -5,19 +5,45 @@ use nulib\cl;
use nulib\schema\_scalar\ScalarSchema; use nulib\schema\_scalar\ScalarSchema;
use nulib\schema\Result; use nulib\schema\Result;
use nulib\schema\Schema; use nulib\schema\Schema;
use nulib\str;
class tarray extends _tstring { class tarray extends _tstring {
const NAME = "array"; const NAME = "array";
const SPLIT_PATTERN = '/\s+/'; const TRIM = true;
const FORMAT = " "; const NORM_NL = true;
const SEP_MAP = [
"space" => "spaces",
"lines" => "line",
];
const STD_SEPS = [
"spaces" => [" ", '/\s+/', true],
"line" => ["\n", "\n", false],
];
const DEFAULT_SEP = self::STD_SEPS["spaces"];
const DEFAULT_PARSE_SEP = self::DEFAULT_SEP[1];
const DEFAULT_FORMAT_SEP = self::DEFAULT_SEP[0];
public static function get_params_from_definition(?array $definition): ?array { public static function get_params_from_definition(?array $definition): ?array {
$params = parent::get_params_from_definition($definition); $params = parent::get_params_from_definition($definition);
$splitPattern = $definition["split_pattern"] ?? null; $sep = $definition["sep"] ?? null;
if ($splitPattern !== null) $params["split_pattern"] = $splitPattern; if ($sep !== null) {
$format = $definition["format"] ?? null; if (!is_array($sep)) {
if ($format !== null) $params["format"] = $format; $sep = cl::get(self::SEP_MAP, $sep, $sep);
$sep = self::STD_SEPS[$sep] ?? [$sep, $sep, false];
}
$params["parse_sep"] = $sep[1] ?? $sep[0];
$params["format_sep"] = $sep[0];
$params["trim"] ??= $sep[2] ?? static::TRIM;
$params["norm_nl"] ??= $sep[3] ?? static::NORM_NL;
} else {
$parseSep = $definition["parse_sep"] ?? null;
if ($parseSep !== null) $params["parse_sep"] = $parseSep;
$formatSep = $definition["format_sep"] ?? null;
if ($formatSep !== null) $params["format_sep"] = $formatSep;
}
return $params; return $params;
} }
@ -43,8 +69,9 @@ class tarray extends _tstring {
} }
function parse(string $value) { function parse(string $value) {
$pattern = $this->params["split_pattern"] ?? static::SPLIT_PATTERN; $sep = $this->params["parse_sep"] ?? static::DEFAULT_PARSE_SEP;
return preg_split($pattern, $value); if ($sep !== false) $value = str::split($sep, $value);
return $value;
} }
/** /**
@ -63,7 +90,7 @@ class tarray extends _tstring {
function format($value, $format=null): string { function format($value, $format=null): string {
if ($value === null) return ""; if ($value === null) return "";
$format ??= $this->params["format"] ?? static::FORMAT; $format ??= $this->params["format"] ?? static::DEFAULT_FORMAT_SEP;
return implode($format, $value); return implode($format, $value);
} }
} }

View File

@ -16,7 +16,7 @@ class CursorTest extends TestCase {
function test_map_row() { function test_map_row() {
$cursor = new class extends Cursor { $cursor = new class extends Cursor {
function mapRow(array $row, ?array $map): array { function mapRow(array $row, ?array $map): array {
return self::map_row($row, $map); return cl::map($row, $map);
} }
}; };
$row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99]; $row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99];
@ -32,7 +32,7 @@ class CursorTest extends TestCase {
function test_filter_row() { function test_filter_row() {
$cursor = new class extends Cursor { $cursor = new class extends Cursor {
function filterRow(array $row, $filter): bool { function filterRow(array $row, $filter): bool {
return self::filter_row($row, $filter); return cl::filter($row, $filter);
} }
}; };
$row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99]; $row = ["a" => 1, "b" => 2, "c" => 3, "x" => 99];

View File

@ -0,0 +1,56 @@
<?php
namespace nulib\schema\types;
use nulib\schema\Schema;
use nur\t\TestCase;
class arrayTest extends TestCase {
function testSchema() {
$value = " first second ";
Schema::nw($value, null, "array");
self::assertSame(["first", "second"], $value);
$value = " first second ";
Schema::nw($value, null, [
"array", "sep" => "spaces",
]);
self::assertSame(["first", "second"], $value);
$value = " first second ";
Schema::nw($value, null, [
"array",
"sep" => "spaces", "trim" => false,
]);
self::assertSame(["", "first", "second", ""], $value);
$value = " first second ";
Schema::nw($value, null, [
"array",
"sep" => "line",
]);
self::assertSame([" first second "], $value);
$value = " first second ";
Schema::nw($value, null, [
"array",
"sep" => "line", "trim" => true,
]);
self::assertSame(["first second"], $value);
}
function testxxx() {
$value = " first second ";
Schema::nw($value, null, [
"array", "sep" => "spaces",
]);
self::assertSame(["first", "second"], $value);
# équivalent à...
$value = " first second ";
Schema::nw($value, null, [
"type" => ["array" => [["sep" => "spaces"]]],
"" => ["scalar"],
]);
self::assertSame(["first", "second"], $value);
}
}