<pman>Intégration de la branche rel82-0.5.1
This commit is contained in:
		
						commit
						452182a3ef
					
				
							
								
								
									
										14
									
								
								.idea/php.xml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14
									
								
								.idea/php.xml
									
									
									
										generated
									
									
									
								
							| @ -21,20 +21,16 @@ | ||||
|       <path value="$PROJECT_DIR$/vendor/nulib/phpss" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/sebastian/code-unit" /> | ||||
|       <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/sebastian/complexity" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/symfony/polyfill-ctype" /> | ||||
|       <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/myclabs/php-enum" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/sebastian/global-state" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/maennchen/zipstream-php" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/sebastian/cli-parser" /> | ||||
|       <path value="$PROJECT_DIR$/vendor/markbaker/complex" /> | ||||
|       <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/symfony/yaml" /> | ||||
|       <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/phpoffice/phpspreadsheet" /> | ||||
|       <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/phpunit/php-timer" /> | ||||
|       <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/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/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/spout" /> | ||||
|     </include_path> | ||||
|  | ||||
							
								
								
									
										12
									
								
								CHANGES.md
									
									
									
									
									
								
							
							
						
						
									
										12
									
								
								CHANGES.md
									
									
									
									
									
								
							| @ -1,3 +1,15 @@ | ||||
| ## 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 | ||||
|  | ||||
							
								
								
									
										48
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								README.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| # 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)" | ||||
| 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 | ||||
| @ -1 +1 @@ | ||||
| 0.5.0 | ||||
| 0.5.1 | ||||
|  | ||||
							
								
								
									
										44
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										44
									
								
								composer.lock
									
									
									
										generated
									
									
									
								
							| @ -391,11 +391,11 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "nulib/php", | ||||
|             "version": "0.5.0p82", | ||||
|             "version": "0.5.1p82", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://git.univ-reunion.fr/sda-php/nulib.git", | ||||
|                 "reference": "9f8b6545e68079728bb7349487b99024bd7d5090" | ||||
|                 "reference": "bd8cdcafbeee9a786200672a022493441582b177" | ||||
|             }, | ||||
|             "require": { | ||||
|                 "ext-json": "*", | ||||
| @ -435,7 +435,7 @@ | ||||
|                 } | ||||
|             ], | ||||
|             "description": "fonctions et classes essentielles", | ||||
|             "time": "2025-04-30T00:33:36+00:00" | ||||
|             "time": "2025-05-12T11:31:32+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "nulib/phpss", | ||||
| @ -1121,16 +1121,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "phpunit/phpunit", | ||||
|             "version": "10.5.45", | ||||
|             "version": "10.5.46", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/sebastianbergmann/phpunit.git", | ||||
|                 "reference": "bd68a781d8e30348bc297449f5234b3458267ae8" | ||||
|                 "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/bd68a781d8e30348bc297449f5234b3458267ae8", | ||||
|                 "reference": "bd68a781d8e30348bc297449f5234b3458267ae8", | ||||
|                 "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/8080be387a5be380dda48c6f41cee4a13aadab3d", | ||||
|                 "reference": "8080be387a5be380dda48c6f41cee4a13aadab3d", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @ -1140,7 +1140,7 @@ | ||||
|                 "ext-mbstring": "*", | ||||
|                 "ext-xml": "*", | ||||
|                 "ext-xmlwriter": "*", | ||||
|                 "myclabs/deep-copy": "^1.12.1", | ||||
|                 "myclabs/deep-copy": "^1.13.1", | ||||
|                 "phar-io/manifest": "^2.0.4", | ||||
|                 "phar-io/version": "^3.2.1", | ||||
|                 "php": ">=8.1", | ||||
| @ -1202,7 +1202,7 @@ | ||||
|             "support": { | ||||
|                 "issues": "https://github.com/sebastianbergmann/phpunit/issues", | ||||
|                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy", | ||||
|                 "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.45" | ||||
|                 "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.46" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @ -1213,12 +1213,20 @@ | ||||
|                     "url": "https://github.com/sebastianbergmann", | ||||
|                     "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", | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2025-02-06T16:08:12+00:00" | ||||
|             "time": "2025-05-02T06:46:24+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "psr/http-client", | ||||
| @ -2416,7 +2424,7 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/polyfill-ctype", | ||||
|             "version": "v1.31.0", | ||||
|             "version": "v1.32.0", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/polyfill-ctype.git", | ||||
| @ -2475,7 +2483,7 @@ | ||||
|                 "portable" | ||||
|             ], | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" | ||||
|                 "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @ -2495,16 +2503,16 @@ | ||||
|         }, | ||||
|         { | ||||
|             "name": "symfony/yaml", | ||||
|             "version": "v7.2.5", | ||||
|             "version": "v7.2.6", | ||||
|             "source": { | ||||
|                 "type": "git", | ||||
|                 "url": "https://github.com/symfony/yaml.git", | ||||
|                 "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912" | ||||
|                 "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23" | ||||
|             }, | ||||
|             "dist": { | ||||
|                 "type": "zip", | ||||
|                 "url": "https://api.github.com/repos/symfony/yaml/zipball/4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", | ||||
|                 "reference": "4c4b6f4cfcd7e52053f0c8bfad0f7f30fb924912", | ||||
|                 "url": "https://api.github.com/repos/symfony/yaml/zipball/0feafffb843860624ddfd13478f481f4c3cd8b23", | ||||
|                 "reference": "0feafffb843860624ddfd13478f481f4c3cd8b23", | ||||
|                 "shasum": "" | ||||
|             }, | ||||
|             "require": { | ||||
| @ -2547,7 +2555,7 @@ | ||||
|             "description": "Loads and dumps YAML files", | ||||
|             "homepage": "https://symfony.com", | ||||
|             "support": { | ||||
|                 "source": "https://github.com/symfony/yaml/tree/v7.2.5" | ||||
|                 "source": "https://github.com/symfony/yaml/tree/v7.2.6" | ||||
|             }, | ||||
|             "funding": [ | ||||
|                 { | ||||
| @ -2563,7 +2571,7 @@ | ||||
|                     "type": "tidelift" | ||||
|                 } | ||||
|             ], | ||||
|             "time": "2025-03-03T07:12:39+00:00" | ||||
|             "time": "2025-04-04T10:10:11+00:00" | ||||
|         }, | ||||
|         { | ||||
|             "name": "theseer/tokenizer", | ||||
|  | ||||
| @ -63,21 +63,14 @@ Application::run(new class extends Application { | ||||
|     $storage = new SqliteStorage($dbfile); | ||||
|     $db = $storage->db(); | ||||
| 
 | ||||
|     $haveChannels = $storage->tableExists("_channels"); | ||||
| 
 | ||||
|     $name = $this->name; | ||||
|     $channelClass = $this->channelClass; | ||||
|     $tableName = $this->tableName; | ||||
|     if ($name !== null) { | ||||
|       $row = null; | ||||
|       if ($haveChannels) { | ||||
|         $row = $db->one([ | ||||
|           "select from _channels", | ||||
|           "where" => ["name" => $name], | ||||
|         ]); | ||||
|       if (!$storage->channelExists($name, $row)) { | ||||
|         self::die("$name: nom de canal de données introuvable"); | ||||
|       } | ||||
|       if ($row === null) self::die("$name: nom de canal de données introuvable"); | ||||
|       if ($row["class"] !== "class@anonymous") $channelClass = $row["class"]; | ||||
|       if ($row["class_name"] !== "class@anonymous") $channelClass = $row["class_name"]; | ||||
|       else $tableName = $row["table_name"]; | ||||
|     } | ||||
|     if ($channelClass !== null) { | ||||
| @ -92,17 +85,12 @@ Application::run(new class extends Application { | ||||
|       }; | ||||
|     } else { | ||||
|       $found = false; | ||||
|       if ($haveChannels) { | ||||
|         $rows = $db->all([ | ||||
|           "select from _channels", | ||||
|         ]); | ||||
|         foreach ($rows as $row) { | ||||
|           msg::print($row["name"]); | ||||
|           $found = true; | ||||
|         } | ||||
|       foreach ($storage->getChannels() as $row) { | ||||
|         msg::print($row["name"]); | ||||
|         $found = true; | ||||
|       } | ||||
|       if (!$found) self::die("Vous devez spécifier le canal de données"); | ||||
|       else self::exit(); | ||||
|       if ($found) self::exit(); | ||||
|       self::die("Vous devez spécifier le canal de données"); | ||||
|     } | ||||
|     $capacitor = new Capacitor($storage, $channel); | ||||
| 
 | ||||
|  | ||||
| @ -34,6 +34,12 @@ | ||||
| .left-gap { margin-left: 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. */ | ||||
| .navbar-form-sm { margin-top: 10px; margin-bottom: 10px; } | ||||
| .navbar-form-xs { margin-top: 14px; margin-bottom: 14px; } | ||||
|  | ||||
| @ -1,12 +1,22 @@ | ||||
| <?php | ||||
| namespace nur\b\authnz; | ||||
| 
 | ||||
| use nur\authz; | ||||
| use nur\b\IllegalAccessException; | ||||
| use nur\b\ValueException; | ||||
| use nur\config; | ||||
| use nur\cookie; | ||||
| use nur\F; | ||||
| use nur\msg; | ||||
| use nur\P; | ||||
| use nur\session; | ||||
| 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; | ||||
| use nur\v\vp\AInitPage; | ||||
| 
 | ||||
| /** | ||||
|  * Interface IAuthzManager: gestionnaire d'authentification et d'autorisation | ||||
| @ -53,7 +63,7 @@ class AuthzManager { | ||||
|   function checkCookie(?string &$username=null, ?string &$authType=null): bool { | ||||
|     $value = cookie::get($this->getCookieKey(), 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]; | ||||
|     $username = $ms[2]; | ||||
|     return true; | ||||
| @ -191,6 +201,22 @@ class AuthzManager { | ||||
|     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 { | ||||
|     # l'utilisateur doit exister
 | ||||
|     $user = $this->getUserManager()->getAuthzUser($username, null); | ||||
| @ -202,7 +228,7 @@ class AuthzManager { | ||||
|         if (config::is_devel() && !$password) $password = null; | ||||
|         if ($password === null || $user->validatePassword($password)) { | ||||
|           # c'est bon
 | ||||
|           $this->initSession(self::STATUS_INITIAL, null); | ||||
|           $this->initSession(self::STATUS_INITIAL); | ||||
|           session::set(self::SESSION_KEY_USERNAME, $username); | ||||
|           $this->onAuthOk($username); | ||||
|           session::set(self::SESSION_KEY_USER, $user); | ||||
| @ -221,14 +247,11 @@ class AuthzManager { | ||||
|     $this->onAuthOk($username); | ||||
|     # l'utilisateur doit exister
 | ||||
|     $user = $this->getUserManager()->getAuthzUser($username, $overrides); | ||||
|     if ($user !== null) { | ||||
|       # ce doit être un utilisteur valide
 | ||||
|       if ($user->isValid()) { | ||||
|         # c'est bon
 | ||||
|         session::set(self::SESSION_KEY_USER, $user); | ||||
|         $this->onAuthzOk($user); | ||||
|         return true; | ||||
|       } | ||||
|     # ce doit être un utilisteur valide
 | ||||
|     if ($user !== null && $user->isValid()) { | ||||
|       session::set(self::SESSION_KEY_USER, $user); | ||||
|       $this->onAuthzOk($user); | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| @ -317,4 +340,83 @@ class AuthzManager { | ||||
|   /** Traiter le cas où l'utilisateur a été autorisé avec succès. */ | ||||
|   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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -1,8 +1,18 @@ | ||||
| <?php | ||||
| namespace nur\b\authnz; | ||||
| 
 | ||||
| use nur\authz; | ||||
| use nur\config; | ||||
| use nur\F; | ||||
| 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 | ||||
| @ -28,4 +38,93 @@ class CasAuthzManager extends AuthzManager { | ||||
|     } | ||||
|     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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
							
								
								
									
										78
									
								
								nur_src/b/authnz/ExtAuthzManager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								nur_src/b/authnz/ExtAuthzManager.php
									
									
									
									
									
										Normal 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(); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										8
									
								
								nur_src/b/authnz/ExtUserManager.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								nur_src/b/authnz/ExtUserManager.php
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| <?php | ||||
| namespace nur\b\authnz; | ||||
| 
 | ||||
| class ExtUserManager extends SimpleUserManager { | ||||
|   function _getUser(string $username): ?array { | ||||
|     return ["username" => $username]; | ||||
|   } | ||||
| } | ||||
| @ -633,14 +633,15 @@ class CTable extends ComponentPrintable implements IParametrable { | ||||
|   /** @var string|int clé de la colonne courante */ | ||||
|   protected $col; | ||||
| 
 | ||||
|   function colTd($value): array { | ||||
|   function colTd($value): ?array { | ||||
|     $vs = $this->col($value); | ||||
|     if ($this->colCtx !== null) { | ||||
|       $vs = func::_call($this->colCtx, [$vs, $value, $this->col, $this->index, $this->row, $this->rawRow]); | ||||
|       if ($vs === false) return null; | ||||
|     } else { | ||||
|       $result = A::get($this->results, $this->col); | ||||
|       $valid = $result === null || $result["valid"]; | ||||
|       $vs= [ | ||||
|       $vs = [ | ||||
|         "class" => ["danger" => !$valid], | ||||
|         $vs, | ||||
|       ]; | ||||
|  | ||||
| @ -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(); | ||||
|     $loginUrl = $this->getLoginUrl(); | ||||
|     $destUrl = page::self(true); | ||||
| @ -88,7 +88,7 @@ class AInitAuthzPage extends AInitPage implements IBasicPage { | ||||
|     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 (authz::manager()->formLogin($username, $password)) { | ||||
|       page::redirect($destUrl); | ||||
|  | ||||
| @ -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 { | ||||
|     return page::bu(config::k("logout_page","_logout.php")); | ||||
|   } | ||||
|  | ||||
							
								
								
									
										68
									
								
								nur_src/v/vp/AppExtauthPage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								nur_src/v/vp/AppExtauthPage.php
									
									
									
									
									
										Normal 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; | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @ -13,7 +13,7 @@ class BasicPage extends AInitAuthzPage { | ||||
|   function afterConfig(): void { | ||||
|     # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
 | ||||
|     # 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(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -32,7 +32,7 @@ class NavigablePage extends AInitAuthzPage implements INavigablePage { | ||||
|   function afterConfig(): void { | ||||
|     # initialiser la session avant setup. ainsi, dans les fonction beforeSetup(),
 | ||||
|     # 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(); | ||||
|   } | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										19
									
								
								nur_src/v/vp/TAuthzLoginPage.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								nur_src/v/vp/TAuthzLoginPage.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
| @ -17,7 +17,7 @@ trait TFormLoginPage { | ||||
|     $username = P::get("u"); | ||||
|     $password = P::get("p"); | ||||
|     $destPage = F::get("d", $this->getMainUrl()); | ||||
|     $this->ensureFormLoginAndRedirect($username, $password, $destPage); | ||||
|     $this->_ensureFormLoginAndRedirect($username, $password, $destPage); | ||||
| 
 | ||||
|     authz::manager()->checkSession($flcUsername, $flcAuthType); | ||||
|     $this->flcUsername = $flcUsername; | ||||
|  | ||||
| @ -2,6 +2,7 @@ | ||||
| namespace nulib\php\access; | ||||
| 
 | ||||
| use ArrayAccess; | ||||
| use nulib\A; | ||||
| use nulib\cl; | ||||
| 
 | ||||
| /** | ||||
| @ -118,85 +119,18 @@ class KeyAccess extends AbstractAccess { | ||||
|   } | ||||
| 
 | ||||
|   function ensureAssoc(array $keys, ?array $params=null): void { | ||||
|     $dest =& $this->dest; | ||||
|     $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++; | ||||
|       } | ||||
|     } | ||||
|     A::ensure_assoc($this->dest, $keys, $params); | ||||
|   } | ||||
| 
 | ||||
|   function ensureKeys(array $defaults, ?array $missings, ?array $params=null): void { | ||||
|     $dest =& $this->dest; | ||||
|     $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]; | ||||
|       } | ||||
|     } | ||||
|     A::ensure_keys($this->dest, $defaults, $missings, $params); | ||||
|   } | ||||
| 
 | ||||
|   function deleteMissings(array $missings, ?array $params=null): void { | ||||
|     $dest =& $this->dest; | ||||
|     $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]); | ||||
|       } | ||||
|     } | ||||
|     A::delete_missings($this->dest, $missings, $params); | ||||
|   } | ||||
| 
 | ||||
|   function ensureOrder(array $keys, ?array $params=null): void { | ||||
|     $dest =& $this->dest; | ||||
|     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); | ||||
|     A::ensure_order($this->dest, $keys, $params); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -11,6 +11,10 @@ use Traversable; | ||||
| /** | ||||
|  * 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 iterable|null $rows la source des lignes | ||||
|  */ | ||||
| @ -18,80 +22,14 @@ class Cursor implements Iterator { | ||||
|   const PARAMS_SCHEMA = [ | ||||
|     "rows" => ["?iterable"], | ||||
|     "rows_func" => ["?callable"], | ||||
|     "cols" => ["?array"], | ||||
|     "cols_func" => ["?callable"], | ||||
|     "map" => ["?array"], | ||||
|     "map_func" => ["?callable"], | ||||
|     "filter" => ["?array"], | ||||
|     "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) { | ||||
|     if ($rows !== null) $params["rows"] = $rows; | ||||
| 
 | ||||
| @ -103,7 +41,7 @@ class Cursor implements Iterator { | ||||
|         $rowsGenerator = $rowsFunc; | ||||
|         $rowsFunc = null; | ||||
|       } else { | ||||
|         $rowsFunc = func::with($rowsFunc, [$rows]); | ||||
|         $rowsFunc = func::with($rowsFunc, [$rows, $this]); | ||||
|       } | ||||
|     } elseif ($rows instanceof Traversable) { | ||||
|       $rowsGenerator = $rows; | ||||
| @ -115,61 +53,95 @@ class Cursor implements Iterator { | ||||
|     $this->rowsGenerator = $rowsGenerator; | ||||
|     $this->rowsFunc = $rowsFunc; | ||||
| 
 | ||||
|     $this->cols = $params["cols"] ?? null; | ||||
|     $colsFunc = $params["cols_func"] ?? null; | ||||
|     if ($colsFunc !== null) $colsFunc = func::with($colsFunc); | ||||
|     $this->colsFunc = $colsFunc; | ||||
|     $filter = $params["filter"] ?? null; | ||||
|     $filterFunc = $params["filter_func"] ?? null; | ||||
|     if ($filterFunc !== null) $this->setFilterFunc($filterFunc); | ||||
|     elseif ($filter !== null) $this->setFilter($filter); | ||||
| 
 | ||||
|     $map = $params["map"] ?? null; | ||||
|     $mapFunc = $params["map_func"] ?? null; | ||||
|     if ($mapFunc !== null) { | ||||
|       $mapFunc = func::with($mapFunc); | ||||
|     } elseif ($map !== null) { | ||||
|       $mapFunc = func::with(function(array $row) use ($map) { | ||||
|         return self::map_row($row, $map); | ||||
|       }); | ||||
|     } | ||||
|     $this->mapFunc = $mapFunc; | ||||
|     if ($mapFunc !== null) $this->setMapFunc($mapFunc); | ||||
|     elseif ($map !== null) $this->setMap($map); | ||||
| 
 | ||||
|     $filter = $params["filter"] ?? null; | ||||
|     $filterFunc = $params["filter_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; | ||||
|     $this->cols = $params["cols"] ?? null; | ||||
|     $this->setColsFunc($params["cols_func"] ?? null); | ||||
|   } | ||||
| 
 | ||||
|   /** un générateur de lignes */ | ||||
|   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; | ||||
| 
 | ||||
|   /** une fonction de signature <code>function(Cursor): ?array</code> */ | ||||
|   private ?func $colsFunc; | ||||
|   /** une fonction de signature <code>function(?array $row, Cursor): bool</code> */ | ||||
|   private ?func $filterFunc = null; | ||||
| 
 | ||||
|   /** une fonction de signature <code>function(Cursor): ?array</code> */ | ||||
|   private ?func $mapFunc; | ||||
|   function setFilter(array $filter): self { | ||||
|     $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> */ | ||||
|   private ?func $filterFunc; | ||||
|   function setFilterFunc(?callable $func): self { | ||||
|     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; | ||||
| 
 | ||||
|   /** @var array|null listes des colonnes de chaque enregistrement */ | ||||
|   public ?array $cols; | ||||
| 
 | ||||
|   /** | ||||
|    * @var int index de l'enregistrement (en ne comptant pas les éléments filtrés) | ||||
|    */ | ||||
|   public int $index; | ||||
| 
 | ||||
|   /** | ||||
|    * @var int index original de l'enregistrement (en tenant compte des éléments | ||||
|    * filtrés) | ||||
|    */ | ||||
|   public int $origIndex; | ||||
| 
 | ||||
|   /** @var string|int clé de l'enregistrement */ | ||||
|   public $key; | ||||
| 
 | ||||
|   /** @var mixed élément original récupéré depuis la source */ | ||||
|   public $raw; | ||||
| 
 | ||||
|   /** | ||||
|    * @var array|null enregistrement après conversion en tableau et application | ||||
|    * du mapping | ||||
|    */ | ||||
|   public ?array $row; | ||||
| 
 | ||||
|   function __get($name) { | ||||
| @ -179,18 +151,22 @@ class Cursor implements Iterator { | ||||
|     return null; | ||||
|   } | ||||
| 
 | ||||
|   protected function cols(): ?array { | ||||
|     return $this->row !== null? array_keys($this->row): null; | ||||
|   protected function convertToRow($raw): ?array { | ||||
|     return cl::withn($raw); | ||||
|   } | ||||
| 
 | ||||
|   protected function filter(): bool { | ||||
|   protected function filterFunc(?array $row): bool { | ||||
|     return false; | ||||
|   } | ||||
| 
 | ||||
|   protected function map(): ?array { | ||||
|   protected function mapFunc(?array $row): ?array { | ||||
|     return $this->row; | ||||
|   } | ||||
| 
 | ||||
|   protected function colsFunc(?array $row): ?array { | ||||
|     return $this->row !== null? array_keys($this->row): null; | ||||
|   } | ||||
| 
 | ||||
|   #############################################################################
 | ||||
|   # Iterator
 | ||||
| 
 | ||||
| @ -212,26 +188,30 @@ class Cursor implements Iterator { | ||||
|   } | ||||
| 
 | ||||
|   function valid(): bool { | ||||
|     $cols = $this->colsFunc; | ||||
|     $filter = $this->filterFunc; | ||||
|     $map = $this->mapFunc; | ||||
|     $colsFunc = $this->colsFunc; | ||||
|     $filterFunc = $this->filterFunc; | ||||
|     $mapFunc = $this->mapFunc; | ||||
|     while ($valid = iter::valid($this->rows)) { | ||||
|       $this->raw = iter::current($this->rows, $this->key); | ||||
|       # conversion en enregistrement
 | ||||
|       $this->key ??= $this->origIndex; | ||||
|       $this->row = cl::withn($this->raw); | ||||
|       if ($filter === null) $filtered = $this->filter(); | ||||
|       else $filtered = $filter->invoke([$this]); | ||||
|       if (!$filtered) { | ||||
|         if ($map === null) $this->row = $this->map(); | ||||
|         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 { | ||||
|       $this->row = $this->convertToRow($this->raw); | ||||
|       # filtrage
 | ||||
|       if ($filterFunc === null) $filtered = $this->filterFunc($this->row); | ||||
|       else $filtered = $filterFunc->invoke([$this->row, $this]); | ||||
|       if ($filtered) { | ||||
|         iter::next($this->rows); | ||||
|         $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) { | ||||
|  | ||||
| @ -127,7 +127,6 @@ abstract class Schema implements ArrayAccess { | ||||
|       } | ||||
|     } | ||||
|     # type
 | ||||
|     $types = []; | ||||
|     $deftype = $definition["type"]; | ||||
|     $nullable = $definition["nullable"] ?? false; | ||||
|     if ($deftype === null) { | ||||
| @ -138,7 +137,17 @@ abstract class Schema implements ArrayAccess { | ||||
|         if (!is_string($deftype)) throw SchemaException::invalid_type($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 === "null") { | ||||
|           $nullable = true; | ||||
| @ -151,10 +160,18 @@ abstract class Schema implements ArrayAccess { | ||||
|         } | ||||
|         if ($type === "") throw SchemaException::invalid_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["nullable"] = $nullable; | ||||
| @ -224,7 +241,7 @@ abstract class Schema implements ArrayAccess { | ||||
|     $types = $definition["type"]; | ||||
|     $nullable = $definition["nullable"]; | ||||
|     # 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) { | ||||
|         if ($key === 0) { | ||||
|           $args = null; | ||||
|  | ||||
| @ -5,19 +5,45 @@ use nulib\cl; | ||||
| use nulib\schema\_scalar\ScalarSchema; | ||||
| use nulib\schema\Result; | ||||
| use nulib\schema\Schema; | ||||
| use nulib\str; | ||||
| 
 | ||||
| class tarray extends _tstring { | ||||
|   const NAME = "array"; | ||||
| 
 | ||||
|   const SPLIT_PATTERN = '/\s+/'; | ||||
|   const FORMAT = " "; | ||||
|   const TRIM = true; | ||||
|   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 { | ||||
|     $params = parent::get_params_from_definition($definition); | ||||
|     $splitPattern = $definition["split_pattern"] ?? null; | ||||
|     if ($splitPattern !== null) $params["split_pattern"] = $splitPattern; | ||||
|     $format = $definition["format"] ?? null; | ||||
|     if ($format !== null) $params["format"] = $format; | ||||
|     $sep = $definition["sep"] ?? null; | ||||
|     if ($sep !== null) { | ||||
|       if (!is_array($sep)) { | ||||
|         $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; | ||||
|   } | ||||
| 
 | ||||
| @ -43,8 +69,9 @@ class tarray extends _tstring { | ||||
|   } | ||||
| 
 | ||||
|   function parse(string $value) { | ||||
|     $pattern = $this->params["split_pattern"] ?? static::SPLIT_PATTERN; | ||||
|     return preg_split($pattern, $value); | ||||
|     $sep = $this->params["parse_sep"] ?? static::DEFAULT_PARSE_SEP; | ||||
|     if ($sep !== false) $value = str::split($sep, $value); | ||||
|     return $value; | ||||
|   } | ||||
| 
 | ||||
|   /** | ||||
| @ -63,7 +90,7 @@ class tarray extends _tstring { | ||||
| 
 | ||||
|   function format($value, $format=null): string { | ||||
|     if ($value === null) return ""; | ||||
|     $format ??= $this->params["format"] ?? static::FORMAT; | ||||
|     $format ??= $this->params["format"] ?? static::DEFAULT_FORMAT_SEP; | ||||
|     return implode($format, $value); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -16,7 +16,7 @@ class CursorTest extends TestCase { | ||||
|   function test_map_row() { | ||||
|     $cursor = new class extends Cursor { | ||||
|       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]; | ||||
| @ -32,7 +32,7 @@ class CursorTest extends TestCase { | ||||
|   function test_filter_row() { | ||||
|     $cursor = new class extends Cursor { | ||||
|       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]; | ||||
|  | ||||
							
								
								
									
										56
									
								
								tests/schema/types/arrayTest.php
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								tests/schema/types/arrayTest.php
									
									
									
									
									
										Normal 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); | ||||
|   } | ||||
| } | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user