From c45ed5804dcb0abf18145c7164d0b63336f22620 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 09:08:13 -0500 Subject: [PATCH 001/117] Agent enrollment and authenticated channel for fog-agent Server side of the fog-agent replacement for the FOG client. Testing only at this stage; nothing here is reachable from the existing client. Enrollment (POST /agent/v1/enroll, unauthenticated): - agentEnrollment / agentEnrollToken tables and four hostAgent* columns (schema step 416, manifest, FK map group 12, route-column contract). - FOG\Agent\Enrollment matches the SMBIOS identity and CSR key against existing hosts, pends unknown or rebinding machines for an admin, and auto-approves via a minted token or an active deploy task. - fog-sign-node-cert gains an `agent` type: clientAuth-only leaf signed by a new agent intermediate CA, CN carries the host id, no names. - Admin routes: GET /agent/enrollments, POST /agent/enrollment/{id}/{action}. Authenticated channel (client certificate): - FOG\Agent\Principal re-verifies the presented certificate in PHP against management/other/agent-ca-bundle.pem (X509_PURPOSE_SSL_CLIENT) and binds it to a host by SPKI fingerprint with a direct prepared statement. Route::getIds() cannot be used here: it adds the calling user's site scope to the WHERE, and with no user that is `1=0`. - Route gates every /agent/v1/* path except enroll on that principal (401 JSON otherwise); POST /agent/v1/poll records version and check-in. - Installer: agent CA bundle, nginx ssl_verify_client optional with SSL_CLIENT_VERIFY / SSL_CLIENT_CERT params, Apache SSLVerifyClient optional with ExportCertData. Apache path is untested (lab is nginx). Fixes found on the way: - Host::addMAC() before save() wrote hostMAC rows with an empty hostID; reordered in Enrollment and both new-host sites in Boot\Registration. - BootFileManager->find() in FOGPage::_bootFileRow (a 1.5 API, swallowed by a catch) replaced with getIds(). - PHPStan extension build/phpstan/GetClassReturnTypeExtension.php types getClass('Name') so a wrong method name on a manager is a finding; the 16 pre-existing findings it surfaced are baselined for a later pass. Proven on the lab 2026-09-03: enroll -> pending -> approve -> issued -> poll with the certificate updates the host row. tests/agent-principal .test.php and the existing suites pass (252/252); phpstan clean. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- build/phpstan/GetClassReturnTypeExtension.php | 135 + composer.json | 5 + ...l-integrity-is-declared-in-the-database.md | 2 +- docs/development/foreign-keys.md | 2 +- lib/common/functions.sh | 142 +- packages/pki/fog-sign-node-cert | 57 +- packages/web/commons/schema-constraints.php | 12 + packages/web/commons/schema-expected.php | 41 +- packages/web/commons/schema.php | 91 + .../de_DE.UTF-8/LC_MESSAGES/messages.po | 88 +- .../en_US.UTF-8/LC_MESSAGES/messages.po | 88 +- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 86 +- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 88 +- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 88 +- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 88 +- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 88 +- .../web/management/languages/messages.pot | 82 +- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 88 +- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 88 +- packages/web/src/Agent/Enrollment.php | 673 +++ packages/web/src/Agent/Principal.php | 134 + packages/web/src/Auth/Authorization.php | 8 + packages/web/src/Base/FOGPage.php | 11 +- packages/web/src/Base/System.php | 2 +- packages/web/src/Boot/Registration.php | 18 +- packages/web/src/Items/AgentEnrollToken.php | 60 + packages/web/src/Items/AgentEnrollment.php | 76 + packages/web/src/Items/Host.php | 10 +- .../src/Managers/AgentEnrollTokenManager.php | 29 + .../src/Managers/AgentEnrollmentManager.php | 29 + packages/web/src/Router/OpenAPI.php | 184 + packages/web/src/Router/Route.php | 236 +- phpstan-baseline.neon | 4680 ++++++++--------- phpstan.neon | 9 + tests/agent-principal.test.php | 129 + tests/fixtures/route-column-contract.txt | 14 +- tests/foreign-key-map.test.php | 1 + 37 files changed, 5104 insertions(+), 2558 deletions(-) create mode 100644 build/phpstan/GetClassReturnTypeExtension.php create mode 100644 packages/web/src/Agent/Enrollment.php create mode 100644 packages/web/src/Agent/Principal.php create mode 100644 packages/web/src/Items/AgentEnrollToken.php create mode 100644 packages/web/src/Items/AgentEnrollment.php create mode 100644 packages/web/src/Managers/AgentEnrollTokenManager.php create mode 100644 packages/web/src/Managers/AgentEnrollmentManager.php create mode 100644 tests/agent-principal.test.php diff --git a/build/phpstan/GetClassReturnTypeExtension.php b/build/phpstan/GetClassReturnTypeExtension.php new file mode 100644 index 0000000000..9ef9771f14 --- /dev/null +++ b/build/phpstan/GetClassReturnTypeExtension.php @@ -0,0 +1,135 @@ +find()` -- the 1.5 + * API, gone from 1.6's FOGManagerController -- analysed clean and died on a + * live server with "Call to undefined method" (fog-agent poll, 2026-09-03). + * Roughly a hundred call sites in packages/web/src have that shape, so the + * gap is not one line, it is the whole factory. + * + * Resolution mirrors Initiator::srcClassMap() rather than calling it: + * lowercase basename of every packages/web/src//.php maps to + * FOG\\. Plugins (FOG\Plugins\...) are not resolved here -- they + * are not in this repo's analysed paths -- and a name that maps to nothing + * falls through to PHPStan's default, exactly as getClass('DateTime') does at + * runtime. + * + * Registered in phpstan.neon under `services`, autoloaded through the root + * composer.json's autoload-dev (the repo root is never deployed, so nothing + * of this reaches a server). + * + * PHP version 7.4+ + * + * @category GetClassReturnTypeExtension + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +namespace FOG\Build\PhpStan; + +use PhpParser\Node\Expr\StaticCall; +use PHPStan\Analyser\Scope; +use PHPStan\Reflection\MethodReflection; +use PHPStan\Reflection\ReflectionProvider; +use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; +use PHPStan\Type\ObjectType; +use PHPStan\Type\Type; + +/** + * Resolves getClass('Name') to FOG\\Name for PHPStan. + * + * @category GetClassReturnTypeExtension + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class GetClassReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension +{ + /** @var array lowercase short name => FQCN */ + private $map = []; + + /** @var ReflectionProvider */ + private $reflectionProvider; + + /** + * Builds the short-name map once, from the tree on disk. + * + * @param ReflectionProvider $reflectionProvider PHPStan's class registry + */ + public function __construct(ReflectionProvider $reflectionProvider) + { + $this->reflectionProvider = $reflectionProvider; + $src = dirname(__DIR__, 2) . '/packages/web/src'; + foreach (glob($src . '/*/*.php') ?: [] as $path) { + $short = strtolower(basename($path, '.php')); + $this->map[$short] = 'FOG\\' . basename(dirname($path)) . '\\' . basename($path, '.php'); + } + } + + /** + * The class whose static method this extension answers for. Subclasses + * calling self::getClass() resolve to this declaring class, so one + * registration covers every FOGBase descendant. + * + * @return string + */ + public function getClass(): string + { + return \FOG\Base\FOGBase::class; + } + + /** + * @param MethodReflection $methodReflection the method being called + * + * @return bool + */ + public function isStaticMethodSupported(MethodReflection $methodReflection): bool + { + return 'getClass' === $methodReflection->getName(); + } + + /** + * The precise type when the name is a literal and the call is not the + * `$props === true` form (which returns an array); null otherwise, which + * hands back to PHPStan's default. + * + * @param MethodReflection $methodReflection the method being called + * @param StaticCall $methodCall the call node + * @param Scope $scope the analysis scope + * + * @return Type|null + */ + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): ?Type { + $args = $methodCall->getArgs(); + if (count($args) < 1) { + return null; + } + if (isset($args[2])) { + $props = $scope->getType($args[2]->value); + if (!$props->isFalse()->yes()) { + return null; + } + } + $names = $scope->getType($args[0]->value)->getConstantStrings(); + if (1 !== count($names)) { + return null; + } + $short = strtolower(trim($names[0]->getValue())); + if (!isset($this->map[$short])) { + return null; + } + $fqcn = $this->map[$short]; + if (!$this->reflectionProvider->hasClass($fqcn)) { + return null; + } + return new ObjectType($fqcn); + } +} diff --git a/composer.json b/composer.json index 9c528e1444..2cbbe45c6b 100644 --- a/composer.json +++ b/composer.json @@ -13,5 +13,10 @@ "config": { "optimize-autoloader": false, "sort-packages": true + }, + "autoload-dev": { + "psr-4": { + "FOG\\Build\\PhpStan\\": "build/phpstan/" + } } } diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index 94396e9e59..1a5e293b7d 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**108 of the map's 123 relationships are declared.** The other 15 are not +**109 of the map's 124 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index 48631511e3..c092333294 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 123 relationships live in them. +the survey and 25 of the map's 124 relationships live in them. ### Direction is the whole rule diff --git a/lib/common/functions.sh b/lib/common/functions.sh index 71f81089a7..389298b64c 100755 --- a/lib/common/functions.sh +++ b/lib/common/functions.sh @@ -5749,6 +5749,14 @@ _installNodeCertSigner() { echo "PKI_SB_CA_CERT=${sbca}" echo "PKI_SB_CA_KEY=$(_pkiZoneDir secureboot)/ca/.fogSBCA.key" fi + # The agent zone, when it exists (createAgentIntermediateCA runs + # from createSSLCA, so a master that has run this installer once has + # it). Same gate as the Secure Boot pair: a path that is not there is + # not a capability. + if [[ -f "$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" ]]; then + echo "PKI_AGENT_CA_CERT=$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" + echo "PKI_AGENT_CA_KEY=$(_pkiZoneDir agent)/ca/.fogAgentCA.key" + fi echo "PKI_STAGING=${stagedir}" } > "$conf" chown root:root "$conf" >>$error_log 2>&1 @@ -5892,6 +5900,14 @@ _installPkiAdminHelper() { echo "PKI_SB_CA_KEY=$(_pkiZoneDir secureboot)/ca/.fogSBCA.key" fi echo "PKI_SETTINGS=${fogprogramdir}/.fogsettings" + # The agent zone, when it exists (createAgentIntermediateCA runs + # from createSSLCA, so a master that has run this installer once has + # it). Same gate as the Secure Boot pair: a path that is not there is + # not a capability. + if [[ -f "$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" ]]; then + echo "PKI_AGENT_CA_CERT=$(_pkiZoneDir agent)/ca/.fogAgentCA.pem" + echo "PKI_AGENT_CA_KEY=$(_pkiZoneDir agent)/ca/.fogAgentCA.key" + fi echo "PKI_STAGING=${stagedir}" } > "$conf" chown root:root "$conf" >>$error_log 2>&1 @@ -8132,6 +8148,12 @@ emitNginxPhpBody() { # PHP_AUTH_USER/PHP_AUTH_PW were never populated and basic auth # could not succeed. echo " fastcgi_param HTTP_AUTHORIZATION \$http_authorization;" >> "$1" + # fog-agent authenticates with a client certificate; PHP needs the + # verdict and the certificate itself (URL-escaped: the raw form has + # newlines, which a fastcgi param cannot carry). Empty for everyone + # else, and Agent\Principal treats empty as "no certificate". + echo " fastcgi_param SSL_CLIENT_VERIFY \$ssl_client_verify;" >> "$1" + echo " fastcgi_param SSL_CLIENT_CERT \$ssl_client_escaped_cert;" >> "$1" echo " fastcgi_buffers 16 16k;" >> "$1" echo " fastcgi_buffer_size 32k;" >> "$1" } @@ -8589,7 +8611,7 @@ _customPkiPair() { _pkiZoneDir() { local root case "$1" in - root|web|client|secureboot) root="$(_pkiRootDir)" ;; + root|web|client|secureboot|agent) root="$(_pkiRootDir)" ;; *) return 0 ;; esac echo "${root}/$1" @@ -9427,6 +9449,39 @@ EOF chmod 0644 "${outdir}/${certfile}" >>$error_log 2>&1 return $st } +# The agent zone: an intermediate that issues CLIENT certificates to +# fog-agent installs, through fog-sign-node-cert's agent type. Its own zone +# rather than a use of the Web CA for two reasons the web zone's own notes +# make clear: the Web CA may be one the admin brought (a public CA issues no +# client certificates at all), and an EKU on a CA bounds what it can issue, +# so clientAuth here means nothing from this zone can ever pose as a server +# however its leaf is written. Always minted under the FOG root -- it is what +# the vhost will be told to trust for client certificates, and it must be +# something this server holds. +createAgentIntermediateCA() { + local agentdir cadir + agentdir="$(_pkiZoneDir agent)" + cadir="${agentdir}/ca" + mkdir -p "$cadir" >>$error_log 2>&1 + chmod 0700 "$cadir" >>$error_log 2>&1 + PKI_agent_ca_key="${cadir}/.fogAgentCA.key" + PKI_agent_ca_cert="${cadir}/.fogAgentCA.pem" + if [[ ! -f ${PKI_agent_ca_cert} ]]; then + dots "Creating FOG Agent CA" + _issueIntermediateCA "FOG Agent CA" "$cadir" ".fogAgentCA.key" ".fogAgentCA.pem" \ + "extendedKeyUsage = clientAuth" "FOG Agent" + errorStat $? + fi + # The trust file for VERIFYING agent certificates: the agent CA and the + # root it chains to, public halves only, world-readable. Three readers: + # the vhost (ssl_client_certificate / SSLCACertificateFile), PHP + # (Agent\Principal re-verifies against it, see that class for why), and + # the copy published under management/other for the same reason + # ca.cert.pem is. Rewritten every run so it follows a re-minted CA. + PKI_agent_ca_bundle="${agentdir}/agent-ca-bundle.pem" + cat "${PKI_agent_ca_cert}" "${PKI_root_ca_cert}" > "${PKI_agent_ca_bundle}" 2>>$error_log + chmod 0644 "${PKI_agent_ca_bundle}" >>$error_log 2>&1 +} # Did ${PKI_root_ca_cert} actually issue ${PKI_web_ca_cert}? # # The one question that separates a FOG-generated Web CA from one imported with @@ -10731,6 +10786,10 @@ EOF PKI_web_trust_chain="${PKI_root_ca_cert}" fi fi + # Outside the web-zone branch on purpose: an install that brought its own + # Web CA still needs an agent CA, and it is issued by the FOG root, which + # every master holds whatever signs its web leaf. + createAgentIntermediateCA _resolveWebLeafPaths _createWebLeaf _writeWebChainFiles @@ -10770,6 +10829,13 @@ EOF # certificate. cp -f "${PKI_root_ca_cert}" $webdirdest/management/other/ca.cert.pem >>$error_log 2>&1 openssl x509 -outform der -in $webdirdest/management/other/ca.cert.pem -out $webdirdest/management/other/ca.cert.der >>$error_log 2>&1 + # What Agent\Principal verifies client certificates against (see + # createAgentIntermediateCA). A storage node mints no agent CA and + # publishes nothing here, and the router's agent gate then refuses + # every certificate, which is right: a node is not an agent server. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + cp -f "${PKI_agent_ca_bundle}" $webdirdest/management/other/agent-ca-bundle.pem >>$error_log 2>&1 + fi errorStat $? dots "Resetting SSL Permissions" chown -R $apacheuser:$apacheuser $webdirdest/management/other >>$error_log 2>&1 @@ -10909,6 +10975,21 @@ EOF # it and the root. echo " ssl_certificate ${sslfullchain:-${PKI_web_vhost_cert}};" >> "$etcconf" echo " ssl_certificate_key ${PKI_web_vhost_key};" >> "$etcconf" + # fog-agent client certificates. `optional`, and at + # server scope because nginx allows nothing finer: + # a browser is never asked for one it does not have + # -- the request names only the FOG Agent CA, which + # no browser holds a certificate from -- and a + # request with no certificate reaches PHP with an + # empty verdict, where the router's agent gate + # decides. Verification against the agent bundle + # (agent CA + root) and depth 2 for exactly that + # chain. Absent on a node, which mints no agent CA. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " ssl_client_certificate ${PKI_agent_ca_bundle};" >> "$etcconf" + echo " ssl_verify_client optional;" >> "$etcconf" + echo " ssl_verify_depth 2;" >> "$etcconf" + fi echo " ssl_session_timeout 1d;" >> "$etcconf" # Zone name is FOG-specific on purpose. Alpine's stock # nginx.conf already declares `shared:SSL:2m` in the @@ -11086,6 +11167,21 @@ EOF # it and the root. echo " ssl_certificate ${sslfullchain:-${PKI_web_vhost_cert}};" >> "$etcconf" echo " ssl_certificate_key ${PKI_web_vhost_key};" >> "$etcconf" + # fog-agent client certificates. `optional`, and at + # server scope because nginx allows nothing finer: + # a browser is never asked for one it does not have + # -- the request names only the FOG Agent CA, which + # no browser holds a certificate from -- and a + # request with no certificate reaches PHP with an + # empty verdict, where the router's agent gate + # decides. Verification against the agent bundle + # (agent CA + root) and depth 2 for exactly that + # chain. Absent on a node, which mints no agent CA. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " ssl_client_certificate ${PKI_agent_ca_bundle};" >> "$etcconf" + echo " ssl_verify_client optional;" >> "$etcconf" + echo " ssl_verify_depth 2;" >> "$etcconf" + fi echo " ssl_session_timeout 1d;" >> "$etcconf" # Zone name is FOG-specific on purpose. Alpine's stock # nginx.conf already declares `shared:SSL:2m` in the @@ -11311,7 +11407,27 @@ EOF # supports 2.4.6, which would silently serve only the first # certificate -- the exact failure this is here to fix. [[ -n $sslchainonly ]] && echo " SSLCertificateChainFile $sslchainonly" >> "$etcconf" - echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + # fog-agent client certificates. Apache verifies them + # against SSLCACertificateFile, so with an agent CA + # present that file is the agent bundle (agent CA + + # root) rather than the web trust chain -- the + # directive governs client verification only, the + # server's own chain is SSLCertificateChainFile above. + # `optional` at vhost scope: a Location-scoped + # requirement means renegotiation, which TLS 1.3 has + # not got and Go's client does not do. The env vars + # are exported only under /agent/, the one place PHP + # reads them. Agent\Principal re-verifies regardless. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " SSLCACertificateFile ${PKI_agent_ca_bundle}" >> "$etcconf" + echo " SSLVerifyClient optional" >> "$etcconf" + echo " SSLVerifyDepth 2" >> "$etcconf" + echo " " >> "$etcconf" + echo " SSLOptions +StdEnvVars +ExportCertData" >> "$etcconf" + echo " " >> "$etcconf" + else + echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + fi echo " " >> "$etcconf" echo " Protocols h2 http/1.1" >> "$etcconf" echo " " >> "$etcconf" @@ -11438,7 +11554,27 @@ EOF # supports 2.4.6, which would silently serve only the first # certificate -- the exact failure this is here to fix. [[ -n $sslchainonly ]] && echo " SSLCertificateChainFile $sslchainonly" >> "$etcconf" - echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + # fog-agent client certificates. Apache verifies them + # against SSLCACertificateFile, so with an agent CA + # present that file is the agent bundle (agent CA + + # root) rather than the web trust chain -- the + # directive governs client verification only, the + # server's own chain is SSLCertificateChainFile above. + # `optional` at vhost scope: a Location-scoped + # requirement means renegotiation, which TLS 1.3 has + # not got and Go's client does not do. The env vars + # are exported only under /agent/, the one place PHP + # reads them. Agent\Principal re-verifies regardless. + if [[ -n ${PKI_agent_ca_bundle:-} && -f ${PKI_agent_ca_bundle} ]]; then + echo " SSLCACertificateFile ${PKI_agent_ca_bundle}" >> "$etcconf" + echo " SSLVerifyClient optional" >> "$etcconf" + echo " SSLVerifyDepth 2" >> "$etcconf" + echo " " >> "$etcconf" + echo " SSLOptions +StdEnvVars +ExportCertData" >> "$etcconf" + echo " " >> "$etcconf" + else + echo " SSLCACertificateFile ${PKI_web_trust_chain}" >> "$etcconf" + fi echo " " >> "$etcconf" echo " Protocols h2 http/1.1" >> "$etcconf" echo " " >> "$etcconf" diff --git a/packages/pki/fog-sign-node-cert b/packages/pki/fog-sign-node-cert index cd2750d5d9..4f1626db37 100755 --- a/packages/pki/fog-sign-node-cert +++ b/packages/pki/fog-sign-node-cert @@ -45,14 +45,14 @@ CONF="/opt/fog/.fog-pki" die() { echo "$*" >&2; exit 1; } [[ $EUID -eq 0 ]] || die "fog-sign-node-cert must run as root" -[[ $# -eq 2 ]] || die "usage: fog-sign-node-cert " +[[ $# -eq 2 ]] || die "usage: fog-sign-node-cert " type="$1" reqid="$2" # Validated before use, not after. A request id reaching the filesystem # unchecked is a path traversal into whatever the web user can write. -[[ $type =~ ^(web|signing)$ ]] || die "unknown certificate type" +[[ $type =~ ^(web|signing|agent)$ ]] || die "unknown certificate type" [[ $reqid =~ ^[a-f0-9]{32}$ ]] || die "malformed request id" [[ -r $CONF ]] || die "missing $CONF -- re-run the FOG installer" @@ -68,6 +68,59 @@ out="${PKI_STAGING}/${reqid}.pem" chainout="${PKI_STAGING}/${reqid}.chain" [[ -f $csr ]] || die "no request at ${csr}" + +# The agent zone issues CLIENT certificates to fog-agent installs +# (FOG\Agent\Enrollment stages the request). No names: the certificate +# identifies a host record, not an address, and the only thing the endpoint +# hands over is the host id, in its own file, matched against a fixed +# pattern here before it goes anywhere near a subject line. The key it is +# signed with is the agent intermediate, never the web CA -- the web CA may +# be one the admin brought, and a public CA does not issue client +# certificates -- and clientAuth alone on the EKU means nothing issued here +# can ever pose as a server. +if [[ $type == agent ]]; then + hostfile="${PKI_STAGING}/${reqid}.agent" + [[ -f $hostfile ]] || die "no host id at ${hostfile}" + hostid=$(head -n1 "$hostfile" | tr -d '[:space:]') + [[ $hostid =~ ^[0-9]{1,10}$ ]] || die "malformed host id" + cacert="${PKI_AGENT_CA_CERT:-}" + cakey="${PKI_AGENT_CA_KEY:-}" + [[ -n $cacert && -f $cacert ]] || die "the agent CA is not present on this server -- re-run the FOG installer" + [[ -n $cakey && -f $cakey ]] || die "the agent CA private key is not on this server (expected at ${cakey:-})" + tmpext=$(mktemp) || die "could not create a temporary file" + trap 'rm -f "$tmpext"' EXIT + { + echo "[ v3_agent ]" + echo "basicConstraints = critical,CA:FALSE" + echo "keyUsage = critical,digitalSignature" + echo "extendedKeyUsage = clientAuth" + echo "subjectKeyIdentifier = hash" + echo "authorityKeyIdentifier = keyid" + } > "$tmpext" + rm -f "$out" "$chainout" + # One year. The agent renews over its own mTLS session well before then; + # a shorter life bounds what a copied key is worth. + if ! openssl x509 -req -in "$csr" -CA "$cacert" -CAkey "$cakey" \ + -CAcreateserial -sha256 -days 365 \ + -extensions v3_agent -extfile "$tmpext" \ + -subj "/CN=fog-agent host ${hostid}/O=FOG Project/OU=FOG Agent" \ + -out "$out" 2>/dev/null; then + die "signing failed" + fi + # What the agent presents beneath its leaf: the intermediate. The root + # is what the server's vhost trusts and need not travel. + cat "$cacert" > "$chainout" + anchor="${PKI_ROOT_CERT:-}" + [[ -n $anchor && -f $anchor ]] || anchor="$cacert" + if ! openssl verify -purpose sslclient -CAfile "$anchor" -untrusted "$cacert" "$out" >/dev/null 2>&1; then + rm -f "$out" "$chainout" + die "the issued certificate does not verify against ${anchor}" + fi + chmod 0644 "$out" "$chainout" + echo "OK" + exit 0 +fi + [[ -f $san ]] || die "no name list at ${san}" # anchor: what the issued certificate must verify against, and what the node is diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index d721ab40ce..ad4bbf6127 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -321,6 +321,18 @@ // against a reused group id would silently start shutting down every // host that inherited the number. ['child' => 'groupPowerManagement', 'column' => 'gpmGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 11], + // FOG Agent enrollment. Group 12, created empty by step 416 so there is + // nothing to sweep before the flip. + // + // `satellite`: an enrollment row is the agent's standing with ONE host -- + // pending, issued or denied -- and means nothing once that host is gone. + // Every row gets a host, because an unknown machine is given a pending + // host at enrollment time, the same way iPXE registration does it. + // CASCADE rather than RESTRICT because deleting the pending host IS how + // an admin forgets a machine; the agent then comes back as unknown and + // waits for a fresh decision. A denied row going with its host is the + // same outcome, and the decision itself is in auditLog. + ['child' => 'agentEnrollment', 'column' => 'aeHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 12], ['child' => 'ldapUserGrant', 'column' => 'lugTargetID', 'parent' => '(lugTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ['child' => 'oidcUserGrant', 'column' => 'ougTargetID', 'parent' => '(ougTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ]; diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 036c6f9eb7..f1c25d052a 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -82,6 +82,41 @@ ], ], 'tables' => [ + 'agentEnrollment' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentEnrollment` ( `aeID` int(11) NOT NULL AUTO_INCREMENT, `aeHostID` int(11) NOT NULL DEFAULT 0, `aeFingerprint` varchar(64) NOT NULL DEFAULT \'\', `aeCSR` text NOT NULL, `aeIdentity` text NOT NULL DEFAULT \'\', `aeHostname` varchar(191) NOT NULL DEFAULT \'\', `aeOS` varchar(20) NOT NULL DEFAULT \'\', `aeArch` varchar(20) NOT NULL DEFAULT \'\', `aeAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `aeRemoteIP` varchar(45) NOT NULL DEFAULT \'\', `aeReason` varchar(32) NOT NULL DEFAULT \'\', `aeState` varchar(16) NOT NULL DEFAULT \'pending\', `aeCert` text NOT NULL DEFAULT \'\', `aeCreated` datetime DEFAULT NULL, `aeUpdated` datetime DEFAULT NULL, `aeDecided` datetime DEFAULT NULL, `aeDecidedBy` varchar(191) NOT NULL DEFAULT \'\', `aeDecidedVia` varchar(16) NOT NULL DEFAULT \'\', PRIMARY KEY (`aeID`), UNIQUE KEY `aeFingerprint` (`aeFingerprint`), KEY `aeState` (`aeState`), KEY `aeHostID` (`aeHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'aeID' => 'int(11) NOT NULL', + 'aeHostID' => 'int(11) NOT NULL DEFAULT 0', + 'aeFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'aeCSR' => 'text NOT NULL', + 'aeIdentity' => 'text NOT NULL DEFAULT \'\'', + 'aeHostname' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'aeOS' => 'varchar(20) NOT NULL DEFAULT \'\'', + 'aeArch' => 'varchar(20) NOT NULL DEFAULT \'\'', + 'aeAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', + 'aeRemoteIP' => 'varchar(45) NOT NULL DEFAULT \'\'', + 'aeReason' => 'varchar(32) NOT NULL DEFAULT \'\'', + 'aeState' => 'varchar(16) NOT NULL DEFAULT \'pending\'', + 'aeCert' => 'text NOT NULL DEFAULT \'\'', + 'aeCreated' => 'datetime DEFAULT NULL', + 'aeUpdated' => 'datetime DEFAULT NULL', + 'aeDecided' => 'datetime DEFAULT NULL', + 'aeDecidedBy' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'aeDecidedVia' => 'varchar(16) NOT NULL DEFAULT \'\'', + ], + ], + 'agentEnrollToken' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentEnrollToken` ( `atID` int(11) NOT NULL AUTO_INCREMENT, `atName` varchar(191) NOT NULL DEFAULT \'\', `atHash` varchar(64) NOT NULL DEFAULT \'\', `atUses` int(11) NOT NULL DEFAULT 1, `atExpires` datetime DEFAULT NULL, `atCreatedBy` varchar(191) NOT NULL DEFAULT \'\', `atCreated` datetime DEFAULT NULL, PRIMARY KEY (`atID`), UNIQUE KEY `atHash` (`atHash`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'atID' => 'int(11) NOT NULL', + 'atName' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'atHash' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'atUses' => 'int(11) NOT NULL DEFAULT 1', + 'atExpires' => 'datetime DEFAULT NULL', + 'atCreatedBy' => 'varchar(191) NOT NULL DEFAULT \'\'', + 'atCreated' => 'datetime DEFAULT NULL', + ], + ], 'apiTokens' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `apiTokens` ( `atID` int(11) NOT NULL AUTO_INCREMENT, `atUserID` int(11) NOT NULL DEFAULT 0, `atName` varchar(255) NOT NULL DEFAULT \'\', `atHash` char(64) NOT NULL DEFAULT \'\', `atEnabled` tinyint(1) NOT NULL DEFAULT 1, `atCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `atCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `atLastUsed` datetime DEFAULT NULL, PRIMARY KEY (`atID`), UNIQUE KEY `atHash` (`atHash`), KEY `atUserID` (`atUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -319,7 +354,7 @@ ], ], 'hosts' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT \'\', `hostAgentNotAfter` datetime DEFAULT NULL, `hostAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `hostAgentCheckin` datetime DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`), KEY `hostAgentFingerprint` (`hostAgentFingerprint`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'hostID' => 'int(11) NOT NULL', 'hostName' => 'varchar(16) NOT NULL', @@ -351,6 +386,10 @@ 'hostExitEfi' => 'longtext DEFAULT NULL', 'hostEnforce' => 'tinyint(1) NOT NULL DEFAULT 1', 'hostInfoKey' => 'varchar(255) DEFAULT NULL', + 'hostAgentFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'hostAgentNotAfter' => 'datetime DEFAULT NULL', + 'hostAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', + 'hostAgentCheckin' => 'datetime DEFAULT NULL', 'hostInfoLock' => 'tinyint(1) DEFAULT 0', 'hostSecTokenPrev' => 'longtext NOT NULL DEFAULT \'\'', 'hostLastPing' => 'datetime DEFAULT NULL', diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index a732b3e698..8ec517c0c9 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -10770,3 +10770,94 @@ function () { . "WHERE `settingKey`='FOG_MEMTEST_KERNEL' " . "AND `settingValue`='memtest.bin'", ]; + +// 416 +$this->schema[] = [ + // fog-agent enrollment (docs/design in the fog-agent repo, section 4; + // wire contract in its docs/design/protocol-v1.md). + // + // An agent that has no certificate yet presents its firmware identity, + // its MACs and a CSR. Nothing is issued until one of three approvals + // happens: an admin clicks Approve, a valid enrollment token was + // presented, or this server itself imaged the host within + // FOG_AGENT_ENROLL_DEPLOY_WINDOW hours. Until then the request waits + // here, verbatim, so that what gets signed on approval is exactly what + // was presented and not a re-read of anything the agent could change in + // between. + // + // One row per KEY (aeFingerprint is the sha256 of the SubjectPublicKeyInfo), + // not per request: an agent repeats the identical request every few + // minutes while it waits, and the repeat refreshes the row rather than + // adding one. A denied key stays denied across repeats for the same + // reason. + // + // aeHostID 0 until the request is bound to a host. Set at approval, + // or immediately when the identity resolved to a host and + // the request is merely waiting for a click. + // aeIdentity the SMBIOS tuple, smbios version and MAC list as the + // agent sent them, JSON. Kept raw: canonicalization is + // SmbiosIdentity's job at read time, the same as for boot. + // aeReason why it is waiting: unknown-host, known-host-no-agent, + // rebind, identity-conflict. Shown to the admin. + // aeState pending, issued, denied. + // aeCert the issued leaf plus its chain, PEM. Filled at approval + // so the agent's next poll can collect it; cleared once + // collected so a database read does not hand out a + // certificate twice. + "CREATE TABLE IF NOT EXISTS `agentEnrollment` ( " + . "`aeID` int(11) NOT NULL AUTO_INCREMENT, " + . "`aeHostID` int(11) NOT NULL DEFAULT 0, " + . "`aeFingerprint` varchar(64) NOT NULL DEFAULT '', " + . "`aeCSR` text NOT NULL, " + . "`aeIdentity` text NOT NULL DEFAULT '', " + . "`aeHostname` varchar(191) NOT NULL DEFAULT '', " + . "`aeOS` varchar(20) NOT NULL DEFAULT '', " + . "`aeArch` varchar(20) NOT NULL DEFAULT '', " + . "`aeAgentVersion` varchar(50) NOT NULL DEFAULT '', " + . "`aeRemoteIP` varchar(45) NOT NULL DEFAULT '', " + . "`aeReason` varchar(32) NOT NULL DEFAULT '', " + . "`aeState` varchar(16) NOT NULL DEFAULT 'pending', " + . "`aeCert` text NOT NULL DEFAULT '', " + . "`aeCreated` datetime DEFAULT NULL, " + . "`aeUpdated` datetime DEFAULT NULL, " + . "`aeDecided` datetime DEFAULT NULL, " + . "`aeDecidedBy` varchar(191) NOT NULL DEFAULT '', " + . "`aeDecidedVia` varchar(16) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`aeID`), " + . "UNIQUE KEY `aeFingerprint` (`aeFingerprint`), " + . "KEY `aeState` (`aeState`), " + . "KEY `aeHostID` (`aeHostID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Enrollment tokens: an admin's pre-approval, minted in the UI and baked + // into an installer or an image's bootstrap file. Only the sha256 of the + // token is stored, so a database read does not yield a usable token. + // atUses counts down; -1 means unlimited until atExpires. + "CREATE TABLE IF NOT EXISTS `agentEnrollToken` ( " + . "`atID` int(11) NOT NULL AUTO_INCREMENT, " + . "`atName` varchar(191) NOT NULL DEFAULT '', " + . "`atHash` varchar(64) NOT NULL DEFAULT '', " + . "`atUses` int(11) NOT NULL DEFAULT 1, " + . "`atExpires` datetime DEFAULT NULL, " + . "`atCreatedBy` varchar(191) NOT NULL DEFAULT '', " + . "`atCreated` datetime DEFAULT NULL, " + . "PRIMARY KEY (`atID`), " + . "UNIQUE KEY `atHash` (`atHash`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // What the host knows about its agent. The fingerprint is the binding: + // a client certificate whose key does not hash to this value is not this + // host's agent, whatever its subject says. Same shape as the Secure Boot + // enrollment columns above it in the host table. + "ALTER TABLE `hosts` " + . "ADD COLUMN `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT '', " + . "ADD COLUMN `hostAgentNotAfter` datetime DEFAULT NULL, " + . "ADD COLUMN `hostAgentVersion` varchar(50) NOT NULL DEFAULT '', " + . "ADD COLUMN `hostAgentCheckin` datetime DEFAULT NULL, " + . "ADD KEY `hostAgentFingerprint` (`hostAgentFingerprint`)", + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_AGENT_ENROLL_DEPLOY_WINDOW','Hours after this server completes " + . "a deploy to a host during which an agent presenting that host''s " + . "firmware identity is enrolled without an admin approving it. The " + . "deploy was the approval. 0 disables the shortcut and every " + . "enrollment waits for a click or a token.','24','General Settings')", +]; diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 538abb9f48..e430ff298d 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -1131,6 +1131,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Dieser Benutzername ist bereits vorhanden!" @@ -1220,10 +1223,16 @@ msgstr "freigeben" msgid "Approve Pending Hosts" msgstr "Ausstehende Hosts" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Ausgewählten MAcs freigeben" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Ausgewählten MAcs freigeben" @@ -1307,6 +1316,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2451,6 +2463,9 @@ msgstr "Debug Optionen" msgid "Debug Task" msgstr "Debug" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Standard" @@ -2551,6 +2566,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Remotedatei wird gelöscht" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "Verteilung" @@ -2933,6 +2951,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Keine Datei wurde hochgeladen" @@ -3035,6 +3056,13 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "wurde abgebrochen" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG-Client-Wiki" @@ -5009,6 +5037,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6348,6 +6379,9 @@ msgstr "Ein Host mit diesem Namen ist bereits vorhanden!" msgid "No storagegroups assigned to this snapin" msgstr "Die zugewiesene Image Speichergruppe ist nicht gültig." +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6392,6 +6426,9 @@ msgstr "" msgid "No values passed" msgstr "Keine Werte übergeben" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "Keine brauchbaren MACS" @@ -6869,9 +6906,23 @@ msgstr "Ausstehende MACs" msgid "Pending Registered Hosts" msgstr "Ausstehende registrierte Hosts" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "Ausstehende registrierte Hosts" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "Ausstehende Hosts" @@ -7901,6 +7952,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "SQL-Fehler" @@ -9495,6 +9549,9 @@ msgstr "Es gibt keine Gruppen auf diesem Server." msgid "The CA private key is on this server" msgstr "Es gibt keine Gruppen auf diesem Server." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9528,6 +9585,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9612,6 +9672,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr "läuft nicht mehr" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9639,6 +9703,10 @@ msgstr "" msgid "The grid key." msgstr "Fehler beim Erstellen eines Tasks" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "Es gibt keine Gruppen auf diesem Server." + #, fuzzy msgid "The identity provider could not be reached" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9696,6 +9764,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "Der Schlüssel wird registrierten Hosts zugewiesen, wenn" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9814,6 +9885,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9830,7 +9904,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10378,6 +10451,9 @@ msgstr "Benutzername Attribut" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "Authentifizieren nicht möglich" @@ -10970,6 +11046,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11219,6 +11298,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -12085,6 +12167,10 @@ msgstr "nicht verfügbar" msgid "unchanged for" msgstr "Imaged für" +#, fuzzy +msgid "unknown action" +msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" + #, fuzzy msgid "unrecorded" msgstr "(empfohlen)" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 1ee9199652..fe570c2e1c 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -1135,6 +1135,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Unknown upload error occurred. Return code: " +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "An image already exists with this name!" @@ -1224,10 +1227,16 @@ msgstr "Host approved" msgid "Approve Pending Hosts" msgstr "Pending Hosts" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Approve selected Hosts" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approve selected Hosts" @@ -1311,6 +1320,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2454,6 +2466,9 @@ msgstr "Debug Options" msgid "Debug Task" msgstr "Debug" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Default" @@ -2554,6 +2569,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Menu create failed" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "Deploy" @@ -2935,6 +2953,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "No file was uploaded" @@ -3037,6 +3058,13 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "has been successfully updated" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG Client Wiki" @@ -5011,6 +5039,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6360,6 +6391,9 @@ msgstr "An image already exists with this name!" msgid "No storagegroups assigned to this snapin" msgstr "The image storage group assigned is not valid" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6404,6 +6438,9 @@ msgstr "" msgid "No values passed" msgstr "No values passed" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "Not able to update" @@ -6880,9 +6917,23 @@ msgstr "Pending MACs" msgid "Pending Registered Hosts" msgstr "Pending Registered Hosts" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "Pending Registration created by FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "Pending Registration created by FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "Pending Registered Hosts" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "Pending hosts" @@ -7912,6 +7963,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "SQL Error:" @@ -9504,6 +9558,9 @@ msgstr "There are no groups on this server." msgid "The CA private key is on this server" msgstr "There are no groups on this server." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9537,6 +9594,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9621,6 +9681,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr " no longer exists" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9648,6 +9712,10 @@ msgstr "" msgid "The grid key." msgstr "Failed to create task" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "There are no groups on this server." + #, fuzzy msgid "The identity provider could not be reached" msgstr "Could not read temp file" @@ -9705,6 +9773,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9823,6 +9894,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Could not read temp file" @@ -9839,7 +9913,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10387,6 +10460,9 @@ msgstr "User Name" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "Error contacting server" @@ -10977,6 +11053,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11225,6 +11304,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Could not read temp file" @@ -12089,6 +12171,10 @@ msgstr "Not Available" msgid "unchanged for" msgstr "Imaged" +#, fuzzy +msgid "unknown action" +msgstr "Unknown upload error occurred. Return code: " + msgid "unrecorded" msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 617ae1cf51..f29a312e24 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -1149,6 +1149,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Se produjo un error de carga desconocida. Código de retorno: " +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Una imagen ya existe con este nombre!" @@ -1238,10 +1241,16 @@ msgstr "Creado" msgid "Approve Pending Hosts" msgstr "anfitriones pendientes" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "retirar" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "retirar" @@ -1324,6 +1333,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2478,6 +2490,9 @@ msgstr "Opciones de arranque:" msgid "Debug Task" msgstr "Opciones de arranque:" +msgid "Decided." +msgstr "" + #, fuzzy msgid "Default" msgstr "Defecto" @@ -2580,6 +2595,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Menú Error de creación" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + #, fuzzy msgid "Deploy" msgstr "última Desplegado" @@ -2969,6 +2987,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Ningún archivo fue subido" @@ -3073,6 +3094,13 @@ msgstr "" msgid "FOG" msgstr "En " +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "se ha actualizado correctamente" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG Wiki Cliente" @@ -5099,6 +5127,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6469,6 +6500,9 @@ msgstr "Una imagen ya existe con este nombre!" msgid "No storagegroups assigned to this snapin" msgstr "Marque aquí para ver los grupos no asignado esta imagen" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6513,6 +6547,9 @@ msgstr "" msgid "No values passed" msgstr "No hay valores pasados" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "No es capaz de actualizar" @@ -6995,9 +7032,22 @@ msgstr "macs pendientes" msgid "Pending Registered Hosts" msgstr "anfitriones pendientes" +msgid "Pending Registration created by FOG_AGENT" +msgstr "" + msgid "Pending Registration created by FOG_CLIENT" msgstr "" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "anfitriones pendientes" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "anfitriones pendientes" @@ -8039,6 +8089,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "Error" @@ -9667,6 +9720,9 @@ msgstr "No hay grupos en este servidor." msgid "The CA private key is on this server" msgstr "No hay grupos en este servidor." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9699,6 +9755,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9783,6 +9842,9 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +msgid "The enrollment is no longer pending." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9810,6 +9872,10 @@ msgstr "" msgid "The grid key." msgstr "No se pudo crear la tarea" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "No hay grupos en este servidor." + #, fuzzy msgid "The identity provider could not be reached" msgstr "No se pudo leer el archivo temporal" @@ -9868,6 +9934,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9986,6 +10055,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "No se pudo leer el archivo temporal" @@ -10002,7 +10074,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10546,6 +10617,9 @@ msgstr "Nombre de usuario" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "servidor de ponerse en contacto con el error" @@ -11142,6 +11216,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11390,6 +11467,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "No se pudo leer el archivo temporal" @@ -12251,6 +12331,10 @@ msgstr "No se dispone de hash" msgid "unchanged for" msgstr "Imagen" +#, fuzzy +msgid "unknown action" +msgstr "Se produjo un error de carga desconocida. Código de retorno: " + msgid "unrecorded" msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 767df64d75..db4d2d09a5 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -1131,6 +1131,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Dieser Benutzername ist bereits vorhanden!" @@ -1220,10 +1223,16 @@ msgstr "freigeben" msgid "Approve Pending Hosts" msgstr "Ausstehende Hosts" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Ausgewählten MAcs freigeben" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Ausgewählten MAcs freigeben" @@ -1307,6 +1316,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2451,6 +2463,9 @@ msgstr "Debug Optionen" msgid "Debug Task" msgstr "Debug" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Standard" @@ -2551,6 +2566,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Remotedatei wird gelöscht" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "Verteilung" @@ -2933,6 +2951,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Keine Datei wurde hochgeladen" @@ -3035,6 +3056,13 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "wurde abgebrochen" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG-Client-Wiki" @@ -5010,6 +5038,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6349,6 +6380,9 @@ msgstr "Ein Host mit diesem Namen ist bereits vorhanden!" msgid "No storagegroups assigned to this snapin" msgstr "Die zugewiesene Image Speichergruppe ist nicht gültig." +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6393,6 +6427,9 @@ msgstr "" msgid "No values passed" msgstr "Keine Werte übergeben" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "Keine brauchbaren MACS" @@ -6870,9 +6907,23 @@ msgstr "Ausstehende MACs" msgid "Pending Registered Hosts" msgstr "Ausstehende registrierte Hosts" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "Ausstehende registrierte Hosts" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "Ausstehende Hosts" @@ -7902,6 +7953,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "SQL-Fehler" @@ -9496,6 +9550,9 @@ msgstr "Es gibt keine Gruppen auf diesem Server." msgid "The CA private key is on this server" msgstr "Es gibt keine Gruppen auf diesem Server." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9529,6 +9586,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9613,6 +9673,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr "läuft nicht mehr" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9640,6 +9704,10 @@ msgstr "" msgid "The grid key." msgstr "Fehler beim Erstellen eines Tasks" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "Es gibt keine Gruppen auf diesem Server." + #, fuzzy msgid "The identity provider could not be reached" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9697,6 +9765,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "Der Schlüssel wird registrierten Hosts zugewiesen, wenn" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9815,6 +9886,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9831,7 +9905,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10379,6 +10452,9 @@ msgstr "Benutzername Attribut" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "Authentifizieren nicht möglich" @@ -10971,6 +11047,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11220,6 +11299,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -12086,6 +12168,10 @@ msgstr "nicht verfügbar" msgid "unchanged for" msgstr "Imaged für" +#, fuzzy +msgid "unknown action" +msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" + #, fuzzy msgid "unrecorded" msgstr "(empfohlen)" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 84db2ff9ee..7957662da1 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -1136,6 +1136,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Une image existe déjà avec ce nom!" @@ -1225,10 +1228,16 @@ msgstr "hôte approuvé" msgid "Approve Pending Hosts" msgstr "Les hôtes en attente" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Approuver hôtes sélectionnés" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approuver hôtes sélectionnés" @@ -1312,6 +1321,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2455,6 +2467,9 @@ msgstr "Options de débogage" msgid "Debug Task" msgstr "Déboguer" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Défaut" @@ -2555,6 +2570,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Menu create a échoué" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "Déployer" @@ -2936,6 +2954,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Aucun fichier a été téléchargé" @@ -3038,6 +3059,13 @@ msgstr "" msgid "FOG" msgstr "BROUILLARD" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "a été mis à jour avec succès" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG client Wiki" @@ -5012,6 +5040,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6347,6 +6378,9 @@ msgstr "Une image existe déjà avec ce nom!" msgid "No storagegroups assigned to this snapin" msgstr "Le groupe de stockage d'images attribuée est non valable" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6391,6 +6425,9 @@ msgstr "" msgid "No values passed" msgstr "Aucune valeur passées" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "Pas en mesure de mettre à jour" @@ -6867,9 +6904,23 @@ msgstr "en attente MACs" msgid "Pending Registered Hosts" msgstr "Dans l'attente des hôtes enregistrés" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "Inscription en attente créée par FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "Inscription en attente créée par FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "Dans l'attente des hôtes enregistrés" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "hôtes en attente" @@ -7898,6 +7949,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "Erreur SQL:" @@ -9489,6 +9543,9 @@ msgstr "Il n'y a pas de groupes sur ce serveur." msgid "The CA private key is on this server" msgstr "Il n'y a pas de groupes sur ce serveur." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9522,6 +9579,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9606,6 +9666,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr " n'existe plus" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9633,6 +9697,10 @@ msgstr "" msgid "The grid key." msgstr "Impossible de créer la tâche" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "Il n'y a pas de groupes sur ce serveur." + #, fuzzy msgid "The identity provider could not be reached" msgstr "Impossible de lire le fichier temporaire" @@ -9690,6 +9758,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9808,6 +9879,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Impossible de lire le fichier temporaire" @@ -9824,7 +9898,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10372,6 +10445,9 @@ msgstr "Nom d'utilisateur" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "Erreur serveur contactant" @@ -10963,6 +11039,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11211,6 +11290,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Impossible de lire le fichier temporaire" @@ -12075,6 +12157,10 @@ msgstr "Indisponible" msgid "unchanged for" msgstr "imager" +#, fuzzy +msgid "unknown action" +msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " + msgid "unrecorded" msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index a75b52dbc0..62a8cf48b4 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -1107,6 +1107,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Si è verificato errore di caricamento sconosciuto" +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Esiste già un utente con questo nome!" @@ -1194,10 +1197,16 @@ msgstr "Approva" msgid "Approve Pending Hosts" msgstr "Host in sospeso" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Approvare MAC selezionati" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approvare MAC selezionati" @@ -1280,6 +1289,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2393,6 +2405,9 @@ msgstr "Opzioni di debug" msgid "Debug Task" msgstr "mettere a punto" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Predefinito" @@ -2489,6 +2504,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Eliminazione di file remoti" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "Distribuisci" @@ -2867,6 +2885,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Nessun file è stato caricato" @@ -2964,6 +2985,13 @@ msgstr "" msgid "FOG" msgstr "NEBBIA" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "è stato cancellato" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG client Wiki" @@ -4871,6 +4899,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6167,6 +6198,9 @@ msgstr "Un host già esiste con questo nome!" msgid "No storagegroups assigned to this snapin" msgstr "Il gruppo di archiviazione immagine assegnato non è valido" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6209,6 +6243,9 @@ msgstr "" msgid "No values passed" msgstr "Nessun valore passato" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + msgid "No viable macs to use" msgstr "Nessun MAC valido da usare" @@ -6673,9 +6710,23 @@ msgstr "sospeso MAC" msgid "Pending Registered Hosts" msgstr "In attesa di host registrati" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "In attesa di registrazione creato da FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "In attesa di registrazione creato da FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "In attesa di host registrati" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "host in sospeso" @@ -7682,6 +7733,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + msgid "SQL Error" msgstr "Errore SQL:" @@ -9210,6 +9264,9 @@ msgstr "Non ci sono gruppi su questo server" msgid "The CA private key is on this server" msgstr "Non ci sono gruppi su questo server" +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9243,6 +9300,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9327,6 +9387,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr "non è più in esecuzione" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9354,6 +9418,10 @@ msgstr "" msgid "The grid key." msgstr "Impossibile creare un'attività" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "Non ci sono gruppi su questo server" + #, fuzzy msgid "The identity provider could not be reached" msgstr "Impossibile leggere il file temporaneo" @@ -9410,6 +9478,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "La chiave verrà assegnata agli host registrati quando una" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9528,6 +9599,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Impossibile leggere il file temporaneo" @@ -9544,7 +9618,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10078,6 +10151,9 @@ msgstr "Attributo nome utente" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "Impossibile contattare il server" @@ -10650,6 +10726,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -10891,6 +10970,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Impossibile leggere il file temporaneo" @@ -11715,6 +11797,10 @@ msgstr "non disponibile" msgid "unchanged for" msgstr "Immagini per" +#, fuzzy +msgid "unknown action" +msgstr "Si è verificato errore di caricamento sconosciuto" + #, fuzzy msgid "unrecorded" msgstr "Consigliato" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index bf42733e1a..292c2b7afb 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -1091,6 +1091,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "不明なアップロードエラーが発生しました" +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "この名前のプリンターは既に存在します!" @@ -1178,10 +1181,16 @@ msgstr "MAC アドレスを承認" msgid "Approve Pending Hosts" msgstr "保留中ホスト" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "選択したホストを承認" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "選択したホストを承認" @@ -1264,6 +1273,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2379,6 +2391,9 @@ msgstr "デバッグオプション" msgid "Debug Task" msgstr "デバッグ" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "既定" @@ -2474,6 +2489,9 @@ msgstr "" msgid "Deleting remote file" msgstr "リモートファイルを削除しています" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "展開" @@ -2852,6 +2870,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "ファイルはアップロードされませんでした" @@ -2949,6 +2970,13 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "強制終了されました" + +msgid "FOG Agent poll" +msgstr "" + msgid "FOG Client" msgstr "FOG クライアント" @@ -4832,6 +4860,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6133,6 +6164,9 @@ msgstr "このイメージを削除できる有効なマスターノードがあ msgid "No storagegroups assigned to this snapin" msgstr "割り当てられたイメージのストレージグループが無効です" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6176,6 +6210,9 @@ msgstr "" msgid "No values passed" msgstr "値が渡されていません" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + msgid "No viable macs to use" msgstr "使用可能な MAC アドレスがありません" @@ -6644,9 +6681,23 @@ msgstr "保留中 MAC アドレス" msgid "Pending Registered Hosts" msgstr "保留中の登録済みホスト" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "FOG_CLIENT により保留中の登録が作成されました" + msgid "Pending Registration created by FOG_CLIENT" msgstr "FOG_CLIENT により保留中の登録が作成されました" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "保留中の登録済みホスト" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "保留中ホスト" @@ -7644,6 +7695,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + msgid "SQL Error" msgstr "SQL エラー" @@ -9155,6 +9209,9 @@ msgstr "このサーバーにはグループがありません" msgid "The CA private key is on this server" msgstr "このサーバーにはグループがありません" +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9188,6 +9245,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9271,6 +9331,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr "選択したプリンターを追加" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9298,6 +9362,10 @@ msgstr "" msgid "The grid key." msgstr "タスクの作成に失敗しました" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "このサーバーにはグループがありません" + #, fuzzy msgid "The identity provider could not be reached" msgstr "強制終了できませんでした" @@ -9354,6 +9422,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "キーは、登録済みホストに次の場合割り当てられます" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9474,6 +9545,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "強制終了できませんでした" @@ -9491,7 +9565,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10022,6 +10095,9 @@ msgstr "ユーザー名属性" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "認証できません" @@ -10597,6 +10673,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -10838,6 +10917,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "強制終了できませんでした" @@ -11665,6 +11747,10 @@ msgstr "利用不可" msgid "unchanged for" msgstr "イメージ処理対象" +#, fuzzy +msgid "unknown action" +msgstr "不明なデータベースエラー" + #, fuzzy msgid "unrecorded" msgstr "保護されていません" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index c9c64dad94..855bca1e94 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -986,6 +986,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "" +msgid "An enrollment token, if the installer was given one." +msgstr "" + msgid "An entry already exists with this name!" msgstr "" @@ -1067,9 +1070,15 @@ msgstr "" msgid "Approve Pending Hosts" msgstr "" +msgid "Approve or deny an agent enrollment" +msgstr "" + msgid "Approve selected" msgstr "" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + msgid "Approved selected hosts!" msgstr "" @@ -1139,6 +1148,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2118,6 +2130,9 @@ msgstr "" msgid "Debug Task" msgstr "" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "" @@ -2201,6 +2216,9 @@ msgstr "" msgid "Deleting remote file" msgstr "" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "" @@ -2537,6 +2555,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + msgid "Every file was uploaded." msgstr "" @@ -2622,6 +2643,12 @@ msgstr "" msgid "FOG" msgstr "" +msgid "FOG Agent enrollment" +msgstr "" + +msgid "FOG Agent poll" +msgstr "" + msgid "FOG Client" msgstr "" @@ -4289,6 +4316,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -5432,6 +5462,9 @@ msgstr "" msgid "No storagegroups assigned to this snapin" msgstr "" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -5471,6 +5504,9 @@ msgstr "" msgid "No values passed" msgstr "" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + msgid "No viable macs to use" msgstr "" @@ -5890,9 +5926,21 @@ msgstr "" msgid "Pending Registered Hosts" msgstr "" +msgid "Pending Registration created by FOG_AGENT" +msgstr "" + msgid "Pending Registration created by FOG_CLIENT" msgstr "" +msgid "Pending agent enrollments" +msgstr "" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + msgid "Pending host" msgstr "" @@ -6772,6 +6820,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + msgid "SQL Error" msgstr "" @@ -8108,6 +8159,9 @@ msgstr "" msgid "The CA private key is on this server" msgstr "" +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -8138,6 +8192,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -8216,6 +8273,9 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +msgid "The enrollment is no longer pending." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -8240,6 +8300,9 @@ msgstr "" msgid "The grid key." msgstr "" +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "" + msgid "The identity provider could not be reached" msgstr "" @@ -8294,6 +8357,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -8404,6 +8470,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + msgid "The signing request could not be generated" msgstr "" @@ -8419,7 +8488,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -8916,6 +8984,9 @@ msgstr "" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + msgid "Unauthenticated." msgstr "" @@ -9428,6 +9499,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -9650,6 +9724,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + msgid "architecture not recorded" msgstr "" @@ -10413,6 +10490,9 @@ msgstr "" msgid "unchanged for" msgstr "" +msgid "unknown action" +msgstr "" + msgid "unrecorded" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 5f381684dd..ef8b00d40d 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -1135,6 +1135,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "Uma imagem já existe com este nome!" @@ -1224,10 +1227,16 @@ msgstr "hospedar aprovado" msgid "Approve Pending Hosts" msgstr "Anfitriões pendentes" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "Aprovar Hosts selecionados" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "Aprovar Hosts selecionados" @@ -1311,6 +1320,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2454,6 +2466,9 @@ msgstr "Opções de depuração" msgid "Debug Task" msgstr "Depurar" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "Padrão" @@ -2554,6 +2569,9 @@ msgstr "" msgid "Deleting remote file" msgstr "Menu Criar falhou" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "implantar" @@ -2935,6 +2953,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Nenhum arquivo foi transferido" @@ -3037,6 +3058,13 @@ msgstr "" msgid "FOG" msgstr "NÉVOA" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "foi atualizado com sucesso" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG Cliente Wiki" @@ -5011,6 +5039,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6347,6 +6378,9 @@ msgstr "Uma imagem já existe com este nome!" msgid "No storagegroups assigned to this snapin" msgstr "O grupo de armazenamento de imagens atribuído não é válido" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6391,6 +6425,9 @@ msgstr "" msgid "No values passed" msgstr "Não há valores passados" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "Não é capaz de atualizar" @@ -6867,9 +6904,23 @@ msgstr "pendentes MACs" msgid "Pending Registered Hosts" msgstr "Enquanto se aguarda hosts registrados" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "Na pendência de Registro criado por FOG_CLIENT" + msgid "Pending Registration created by FOG_CLIENT" msgstr "Na pendência de Registro criado por FOG_CLIENT" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "Enquanto se aguarda hosts registrados" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "anfitriões pendentes" @@ -7899,6 +7950,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "Erro SQL:" @@ -9491,6 +9545,9 @@ msgstr "Não existem grupos neste servidor." msgid "The CA private key is on this server" msgstr "Não existem grupos neste servidor." +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9524,6 +9581,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9608,6 +9668,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr " não existe mais" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9635,6 +9699,10 @@ msgstr "" msgid "The grid key." msgstr "Falha ao criar tarefa" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "Não existem grupos neste servidor." + #, fuzzy msgid "The identity provider could not be reached" msgstr "Não foi possível ler arquivo temporário" @@ -9692,6 +9760,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9810,6 +9881,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "Não foi possível ler arquivo temporário" @@ -9826,7 +9900,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10374,6 +10447,9 @@ msgstr "Nome de usuário" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "servidor entrar em contato com erro" @@ -10965,6 +11041,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11213,6 +11292,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "Não foi possível ler arquivo temporário" @@ -12077,6 +12159,10 @@ msgstr "Não disponível" msgid "unchanged for" msgstr "fotografada" +#, fuzzy +msgid "unknown action" +msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " + msgid "unrecorded" msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 830e06bbda..c939f3a128 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -1135,6 +1135,9 @@ msgstr "" msgid "An SQL error occurred" msgstr "发生未知上传错误。返回代码:" +msgid "An enrollment token, if the installer was given one." +msgstr "" + #, fuzzy msgid "An entry already exists with this name!" msgstr "图像已经存在具有此名称!" @@ -1224,10 +1227,16 @@ msgstr "主机批准" msgid "Approve Pending Hosts" msgstr "待主机" +msgid "Approve or deny an agent enrollment" +msgstr "" + #, fuzzy msgid "Approve selected" msgstr "批准选定主机" +msgid "Approved but the signer is unavailable; the agent retries." +msgstr "" + #, fuzzy msgid "Approved selected hosts!" msgstr "批准选定主机" @@ -1311,6 +1320,9 @@ msgstr "" msgid "Authenticated but not permitted." msgstr "" +msgid "Authenticated by the client certificate enrollment issued, verified by the web server and bound to the host by its key fingerprint before the route runs; no token or session applies. Records the check-in and answers with what this server can do. A certificate that no longer binds to a live host gets 401, which tells the agent to enroll again." +msgstr "" + msgid "Authentication missing or invalid." msgstr "" @@ -2454,6 +2466,9 @@ msgstr "调试选项" msgid "Debug Task" msgstr "调试" +msgid "Decided." +msgstr "" + msgid "Default" msgstr "默认" @@ -2554,6 +2569,9 @@ msgstr "" msgid "Deleting remote file" msgstr "菜单创建失败" +msgid "Denied by an admin. The agent backs off to hourly." +msgstr "" + msgid "Deploy" msgstr "部署" @@ -2935,6 +2953,9 @@ msgstr "" msgid "Every configured multicast port is already in use" msgstr "" +msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "没有文件被上传" @@ -3037,6 +3058,13 @@ msgstr "" msgid "FOG" msgstr "雾" +#, fuzzy +msgid "FOG Agent enrollment" +msgstr "已成功更新" + +msgid "FOG Agent poll" +msgstr "" + #, fuzzy msgid "FOG Client" msgstr "FOG客户维基" @@ -5011,6 +5039,9 @@ msgstr "" msgid "Issued by %s" msgstr "" +msgid "Issued. The certificate and the host it binds to." +msgstr "" + msgid "Issuer" msgstr "" @@ -6347,6 +6378,9 @@ msgstr "图像已经存在具有此名称!" msgid "No storagegroups assigned to this snapin" msgstr "分配图像存储组无效" +msgid "No such enrollment, or no such action." +msgstr "" + msgid "No such file in the boot directory." msgstr "" @@ -6391,6 +6425,9 @@ msgstr "" msgid "No values passed" msgstr "没有值传递" +msgid "No verified client certificate, or one bound to no live host." +msgstr "" + #, fuzzy msgid "No viable macs to use" msgstr "不能够更新" @@ -6867,9 +6904,23 @@ msgstr "待定的MAC" msgid "Pending Registered Hosts" msgstr "待注册主机" +#, fuzzy +msgid "Pending Registration created by FOG_AGENT" +msgstr "通过创建FOG_CLIENT登记待定" + msgid "Pending Registration created by FOG_CLIENT" msgstr "通过创建FOG_CLIENT登记待定" +#, fuzzy +msgid "Pending agent enrollments" +msgstr "待注册主机" + +msgid "Pending an admin decision. Poll again after retry_after seconds." +msgstr "" + +msgid "Pending enrollment rows." +msgstr "" + #, fuzzy msgid "Pending host" msgstr "待主机" @@ -7899,6 +7950,9 @@ msgstr "" msgid "SHA-256" msgstr "" +msgid "SMBIOS system UUID, system serial, board serial, chassis asset tag and the MAC list, as fog-agent identity prints them." +msgstr "" + #, fuzzy msgid "SQL Error" msgstr "SQL错误:" @@ -9491,6 +9545,9 @@ msgstr "有此服务器上没有组。" msgid "The CA private key is on this server" msgstr "有此服务器上没有组。" +msgid "The CSR is not a usable P-256 request, or a required field is missing." +msgstr "" + msgid "The Enroll Secure Boot task type does all of this for you: schedule it against a host or a group from Task Scheduling and the client boots FOS, which stages the request itself -- or enrolls outright with nothing to confirm, if the machine is in Setup Mode. The Enroll Secure Boot Key menu item stays for answering a pending request by hand, or for enrolling from local media on a machine FOS cannot boot." msgstr "" @@ -9524,6 +9581,9 @@ msgstr "" msgid "The account on the node itself that FOG signs in as, over SSH to check the node is reachable and over FTP to move images and snapins. Created by the installer, normally \"fogproject\". This is not a FOG web user." msgstr "" +msgid "The agent speaks a protocol this server does not." +msgstr "" + msgid "The architecture columns do not exist yet. Run the Database Schema Installer / Updater, then reload this page." msgstr "" @@ -9608,6 +9668,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The enrollment is no longer pending." +msgstr "不复存在" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9635,6 +9699,10 @@ msgstr "" msgid "The grid key." msgstr "无法创建任务" +#, fuzzy +msgid "The host this certificate is, and the capabilities this server offers." +msgstr "有此服务器上没有组。" + #, fuzzy msgid "The identity provider could not be reached" msgstr "无法读取临时文件" @@ -9692,6 +9760,9 @@ msgstr "" msgid "The key will be assigned to registered hosts when a" msgstr "" +msgid "The leaf followed by the agent CA, PEM." +msgstr "" + msgid "The logical sector size of the disk this image was captured from." msgstr "" @@ -9810,6 +9881,9 @@ msgstr "" msgid "The signed certificate, or full chain, leaf first (PEM)" msgstr "" +msgid "The signer is unavailable; nothing changed." +msgstr "" + #, fuzzy msgid "The signing request could not be generated" msgstr "无法读取临时文件" @@ -9826,7 +9900,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10374,6 +10447,9 @@ msgstr "用户名" msgid "Unauthenticated, and generated per request from this server's live routing and model metadata. Also served at /swagger.json, which is where most tooling looks first." msgstr "" +msgid "Unauthenticated, because this is how an agent obtains the client certificate it will authenticate with afterward. The agent posts a certificate signing request and its firmware identity; the server resolves the machine the way iPXE registration does and answers issued, pending or denied. Pending is the normal first answer for a machine nobody has approved yet -- the agent polls until an admin decides on /agent/enrollment/{id}/{action}, an enrollment token pre-approves it, or the server itself imaged the host recently (FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent can do nothing else: without a certificate no other agent route accepts it. Protocol 1." +msgstr "" + #, fuzzy msgid "Unauthenticated." msgstr "服务器连接出错" @@ -10965,6 +11041,9 @@ msgstr "" msgid "Who a filter can be shared with" msgstr "" +msgid "Why it waits: unknown-host, known-host-no-agent, rebind, identity-conflict, reissue." +msgstr "" + msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" @@ -11213,6 +11292,9 @@ msgstr "" msgid "answering 0 for a read that never ran" msgstr "" +msgid "approve signs the CSR, binds the certificate to the host and takes the host out of pending; deny records the refusal. Either way the agent learns the outcome on its next poll. Audited as agent.enroll." +msgstr "" + #, fuzzy msgid "architecture not recorded" msgstr "无法读取临时文件" @@ -12077,6 +12159,10 @@ msgstr "不可用" msgid "unchanged for" msgstr "成像" +#, fuzzy +msgid "unknown action" +msgstr "发生未知上传错误。返回代码:" + msgid "unrecorded" msgstr "" diff --git a/packages/web/src/Agent/Enrollment.php b/packages/web/src/Agent/Enrollment.php new file mode 100644 index 0000000000..733b892769 --- /dev/null +++ b/packages/web/src/Agent/Enrollment.php @@ -0,0 +1,673 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Base\FOGCore; +use FOG\Base\SmbiosIdentity; +use FOG\Items\AgentEnrollment; +use FOG\Items\AgentEnrollToken; +use FOG\Items\Host; +use FOG\Managers\HostManager; +use FOG\Router\Route; + +/** + * fog-agent enrollment: decides whether a key may act as a host. + * + * The wire contract is the agent's docs/design/protocol-v1.md. In one + * sentence: a stranger presents a firmware identity, a MAC list and a CSR, + * and nothing is issued until an admin clicks Approve, a valid enrollment + * token was presented, or this server itself imaged that host recently + * enough that the deploy counts as the approval. + * + * Two questions are kept apart on purpose. WHICH host is this is answered by + * the SMBIOS tuple and the MACs, the same evidence PXE boot uses, through the + * same resolver. IS it that host is answered by the certificate this class + * issues, and only after one of the three approvals. The identity is + * discoverable by anyone on the network, so it may resolve but never + * authenticate; that is why a known host with no agent still waits for a + * click unless a token or a deploy vouches for it -- otherwise anyone able + * to spoof its firmware values could collect its desired state, which may + * carry a directory-join credential. + * + * Signing follows nodecert.php exactly: the CSR is staged to disk and a + * root-owned helper signs it through sudo. This class never reads a CA key + * (ADR 0036). + * + * @category Enrollment + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class Enrollment extends FOGBase +{ + const PROTOCOL = 1; + + const REASON_UNKNOWN = 'unknown-host'; + const REASON_KNOWN = 'known-host-no-agent'; + const REASON_REBIND = 'rebind'; + const REASON_CONFLICT = 'identity-conflict'; + const REASON_REISSUE = 'reissue'; + const REASON_NO_MAC = 'no-mac'; + + const VIA_TOKEN = 'token'; + const VIA_DEPLOY = 'deploy'; + const VIA_ADMIN = 'admin'; + + /** + * Seconds the agent is told to wait before repeating a pending request. + */ + const RETRY_AFTER = 300; + + /** + * Agent JSON key => inventory field, in SmbiosIdentity::FIELDS terms. + * + * @var array + */ + const IDENTITY_MAP = [ + 'system_uuid' => 'sysuuid', + 'system_serial' => 'sysserial', + 'board_serial' => 'mbserial', + 'chassis_asset' => 'caseasset' + ]; + + /** + * Handles one enroll request. + * + * @param array $body the decoded JSON body + * @param string $remoteIP the caller's address, for the row and the log + * + * @return array [int HTTP code, array JSON payload] + */ + public static function handle(array $body, $remoteIP) + { + if ((int)($body['protocol'] ?? 0) !== self::PROTOCOL) { + return [426, ['status' => 'unsupported', 'reason' => 'protocol']]; + } + $csr = (string)($body['csr_pem'] ?? ''); + $fingerprint = self::fingerprint($csr); + if (null === $fingerprint) { + return [400, ['status' => 'error', 'reason' => 'csr']]; + } + $identity = is_array($body['identity'] ?? null) ? $body['identity'] : []; + $macs = self::_macs($identity['macs'] ?? []); + $token = trim((string)($body['token'] ?? '')); + + $ids = Route::getIds('agentenrollment', ['fingerprint' => $fingerprint], 'id'); + $Row = count($ids) ? new AgentEnrollment((int)array_shift($ids)) : null; + if ($Row && !$Row->isValid()) { + $Row = null; + } + $now = self::niceDate()->format('Y-m-d H:i:s'); + + // A repeat while the answer already exists. The agent polls the same + // request every few minutes; the row is the memory of what was + // decided about that key, so the decision is answered from it and + // never re-derived from the identity, which the caller controls. + if ($Row) { + $Row->set('updated', $now) + ->set('remoteIP', (string)$remoteIP) + ->set('agentVersion', (string)($body['agent_version'] ?? '')); + switch ($Row->get('state')) { + case AgentEnrollment::STATE_DENIED: + $Row->save(); + return [403, ['status' => 'denied', 'reason' => 'admin']]; + case AgentEnrollment::STATE_ISSUED: + $cert = (string)$Row->get('cert'); + if ($cert !== '') { + // Approved since the last poll. Hand it over once. + $Row->set('cert', '')->save(); + return [200, self::_issuedPayload($Row, $cert)]; + } + // Same key, certificate already collected: the agent lost + // its certificate but kept its key. Not automatic -- an + // admin looks at it, the same as a rebind. + $Row->set('state', AgentEnrollment::STATE_PENDING) + ->set('reason', self::REASON_REISSUE) + ->save(); + self::_audit($Row, 'waiting: ' . self::REASON_REISSUE); + return [202, self::_pendingPayload($Row)]; + default: + // Still pending. The automatic paths are re-checked: a token + // may be valid now, or the deploy that vouches for this host + // may have finished since the agent first asked. + $Row->save(); + $via = self::_autoApproval($Row, $token); + if ($via) { + return self::_issueNow($Row, $via, $remoteIP); + } + return [202, self::_pendingPayload($Row)]; + } + } + + // First contact for this key. Resolve the host the way boot does. + list($hostID, $reason) = self::_resolve($identity, $macs); + + $Row = self::getClass('AgentEnrollment') + ->set('fingerprint', $fingerprint) + ->set('csr', $csr) + ->set('identity', json_encode($identity)) + ->set('hostname', (string)($body['hostname'] ?? '')) + ->set('os', (string)($body['os'] ?? '')) + ->set('arch', (string)($body['arch'] ?? '')) + ->set('agentVersion', (string)($body['agent_version'] ?? '')) + ->set('remoteIP', (string)$remoteIP) + ->set('hostID', $hostID) + ->set('reason', $reason) + ->set('state', AgentEnrollment::STATE_PENDING) + ->set('cert', '') + ->set('created', $now) + ->set('updated', $now); + + if (0 === $hostID && self::REASON_UNKNOWN === $reason) { + // Nobody has seen this machine. It becomes a pending host, the + // same thing the current client's auto-registration produces, + // so the admin sees it in the one place they already look. + // Needs a MAC: a host without a primary MAC is not a host FOG + // can address. + if (empty($macs)) { + $Row->set('reason', self::REASON_NO_MAC)->save(); + self::_audit($Row, 'waiting: no MAC address reported'); + return [202, self::_pendingPayload($Row)]; + } + $hostID = self::_createPendingHost($Row, $identity, $macs); + $Row->set('hostID', $hostID); + } + $Row->save(); + + $via = self::_autoApproval($Row, $token); + if ($via) { + return self::_issueNow($Row, $via, $remoteIP); + } + self::_audit($Row, 'waiting: ' . $Row->get('reason')); + return [202, self::_pendingPayload($Row)]; + } + + /** + * An admin approves a pending request: the CSR is signed and the host + * bound. The agent collects the certificate on its next poll. + * + * @param int $id the enrollment row + * @param string $by the approving user's name, for the row and the audit + * + * @throws \RuntimeException with an HTTP code when it cannot be done + * + * @return AgentEnrollment + */ + public static function approve($id, $by) + { + $Row = new AgentEnrollment((int)$id); + if (!$Row->isValid()) { + throw new \RuntimeException('no such enrollment', 404); + } + if (AgentEnrollment::STATE_PENDING !== $Row->get('state')) { + throw new \RuntimeException('enrollment is not pending', 409); + } + if ((int)$Row->get('hostID') < 1) { + throw new \RuntimeException('enrollment is not bound to a host', 409); + } + $cert = self::_issue($Row, self::VIA_ADMIN, (string)$by); + $Row->set('cert', $cert)->save(); + return $Row; + } + + /** + * An admin denies a pending request. The key stays denied: repeats of + * the same request are answered from the row without re-deciding. + * + * @param int $id the enrollment row + * @param string $by the denying user's name + * + * @throws \RuntimeException with an HTTP code when it cannot be done + * + * @return AgentEnrollment + */ + public static function deny($id, $by) + { + $Row = new AgentEnrollment((int)$id); + if (!$Row->isValid()) { + throw new \RuntimeException('no such enrollment', 404); + } + if (AgentEnrollment::STATE_PENDING !== $Row->get('state')) { + throw new \RuntimeException('enrollment is not pending', 409); + } + $Row->set('state', AgentEnrollment::STATE_DENIED) + ->set('decided', self::niceDate()->format('Y-m-d H:i:s')) + ->set('decidedBy', (string)$by) + ->set('decidedVia', self::VIA_ADMIN) + ->set('cert', '') + ->save(); + self::_audit($Row, 'denied by ' . $by, (string)$by); + return $Row; + } + + /** + * The sha256 of the CSR's SubjectPublicKeyInfo, hex. Null when the CSR + * does not parse or carries no usable key. + * + * The public key, not the CSR bytes: the same key in two differently + * encoded requests must land on the same row, and the certificate a + * client later presents is matched to the host by this same value. + * + * @param string $csrPEM the request + * + * @return string|null + */ + public static function fingerprint($csrPEM) + { + if (strpos($csrPEM, '-----BEGIN CERTIFICATE REQUEST-----') === false) { + return null; + } + // Same bytes Principal::verify() hashes out of the issued + // certificate, so the binding survives from CSR to client cert. + return Principal::spkiFingerprint(@openssl_csr_get_public_key($csrPEM)); + } + + /** + * Resolves the host the request is about. + * + * Firmware first, under the same setting boot uses (FOG_HOST_IDENTIFY_SMBIOS + * off means MAC only), then the MAC list. Both answering with different + * hosts is reported, not guessed at: the firmware's answer is kept, as + * enforce mode would, and the admin sees the conflict as the reason. + * + * @param array $identity the identity block from the request + * @param array $macs validated MACs + * + * @return array [int hostID, string reason] + */ + private static function _resolve(array $identity, array $macs) + { + $smbiosID = null; + if ('off' !== FOGCore::getSetting('FOG_HOST_IDENTIFY_SMBIOS')) { + $ids = []; + foreach (self::IDENTITY_MAP as $key => $field) { + $ids[$field] = SmbiosIdentity::canonicalize( + (string)($identity[$key] ?? '') + ); + } + $smbiosID = HostManager::resolveHostBySmbios($ids); + } + $macID = 0; + if (!empty($macs)) { + try { + self::getClass('HostManager')->getHostByMacAddresses($macs); + if (self::$Host->isValid() && !self::$Host->get('pending')) { + $macID = (int)self::$Host->get('id'); + } + } catch (\Exception $e) { + $macID = 0; + } + } + $hostID = (int)($smbiosID ?: $macID); + if ($hostID < 1) { + return [0, self::REASON_UNKNOWN]; + } + if ($smbiosID && $macID && (int)$smbiosID !== $macID) { + return [(int)$smbiosID, self::REASON_CONFLICT]; + } + $Host = new Host($hostID); + if ('' !== (string)$Host->get('agentFingerprint')) { + return [$hostID, self::REASON_REBIND]; + } + return [$hostID, self::REASON_KNOWN]; + } + + /** + * The two approvals that need no click. Returns the via, or '' when + * the request has to wait for an admin. + * + * @param AgentEnrollment $Row the request + * @param string $token the token presented, if any + * + * @return string + */ + private static function _autoApproval(AgentEnrollment $Row, $token) + { + if ('' !== $token && self::_consumeToken($token)) { + return self::VIA_TOKEN; + } + // A deploy vouches only for the host it imaged, and only for a + // request that is a first binding or a rebind of that host. It never + // resolves a conflict and never creates a host. + $hostID = (int)$Row->get('hostID'); + if ($hostID > 0 + && in_array($Row->get('reason'), [self::REASON_KNOWN, self::REASON_REBIND], true) + && self::_recentDeploy(new Host($hostID)) + ) { + return self::VIA_DEPLOY; + } + return ''; + } + + /** + * Did this server complete a deploy to the host recently enough? + * + * hostLastDeploy is written when an imaging task completes, so it is + * exactly "this server put an operating system on that machine". The + * window is FOG_AGENT_ENROLL_DEPLOY_WINDOW hours; 0 turns the path off. + * + * @param Host $Host the host + * + * @return bool + */ + private static function _recentDeploy(Host $Host) + { + $hours = (int)FOGCore::getSetting('FOG_AGENT_ENROLL_DEPLOY_WINDOW'); + $deployed = (string)$Host->get('deployed'); + if ($hours < 1 || !self::validDate($deployed)) { + return false; + } + $when = strtotime($deployed); + return $when !== false && (time() - $when) <= $hours * 3600; + } + + /** + * Validates a token and consumes one use. Only the hash is stored. + * + * @param string $token the token as presented + * + * @return bool + */ + private static function _consumeToken($token) + { + $hash = hash('sha256', $token); + $ids = Route::getIds('agentenrolltoken', ['hash' => $hash], 'id'); + if (!count($ids)) { + return false; + } + $Token = new AgentEnrollToken((int)array_shift($ids)); + if (!$Token->isValid()) { + return false; + } + $expires = (string)$Token->get('expires'); + if (self::validDate($expires) && strtotime($expires) < time()) { + return false; + } + $uses = (int)$Token->get('uses'); + if (0 === $uses) { + return false; + } + if ($uses > 0) { + $Token->set('uses', $uses - 1)->save(); + } + return true; + } + + /** + * Creates the pending host an unknown machine becomes, with an + * inventory row carrying the firmware identity so the next boot or + * request resolves to it. + * + * @param AgentEnrollment $Row the request + * @param array $identity the identity block + * @param array $macs validated MACs, at least one + * + * @return int the new host id + */ + private static function _createPendingHost(AgentEnrollment $Row, array $identity, array $macs) + { + $name = (string)$Row->get('hostname'); + $Probe = self::getClass('Host'); + if ('' === $name || !$Probe->isHostnameSafe($name)) { + // Fifteen characters, the NetBIOS bound isHostnameSafe enforces. + $name = 'agent-' . substr((string)$Row->get('fingerprint'), 0, 8); + } + $base = $name; + $n = 1; + while (self::getClass('HostManager')->exists($name)) { + $suffix = '-' . $n++; + $name = substr($base, 0, 15 - strlen($suffix)) . $suffix; + } + $Host = self::getClass('Host') + ->set('name', $name) + ->set('description', _('Pending Registration created by FOG_AGENT')) + ->set('imageID', null) + ->set('pending', '1') + ->addPriMAC(array_shift($macs)); + $Host->save(); + $hostID = (int)$Host->get('id'); + // After save, not before: addMAC() writes the hostMAC rows + // immediately with whatever id the object holds, and an unsaved + // host holds none -- the batch guard in FOGManagerController then + // rejects the empty hostID and the whole enroll dies with a 500. + // addPriMAC() is different: it only stages the primary, and + // Host::save() writes that row itself once the id exists. + if (!empty($macs)) { + $Host->addMAC($macs); + } + + $ids = []; + foreach (self::IDENTITY_MAP as $key => $field) { + $ids[$field] = SmbiosIdentity::canonicalize((string)($identity[$key] ?? '')); + } + $usable = SmbiosIdentity::usable($ids); + if (!empty($usable)) { + $Inventory = self::getClass('Inventory')->set('hostID', $hostID); + foreach ($usable as $field => $value) { + $Inventory->set($field, $value); + } + $Inventory->save(); + } + return $hostID; + } + + /** + * Issues on an automatic path and answers the agent in the same + * request. + * + * @param AgentEnrollment $Row the request + * @param string $via token or deploy + * @param string $remoteIP the caller + * + * @return array [int, array] + */ + private static function _issueNow(AgentEnrollment $Row, $via, $remoteIP) + { + try { + $cert = self::_issue($Row, $via, ''); + } catch (\RuntimeException $e) { + error_log( + sprintf( + 'FOG agent enroll: signing for host %d from %s failed: %s', + (int)$Row->get('hostID'), + $remoteIP, + $e->getMessage() + ) + ); + return [503, ['status' => 'error', 'reason' => 'signing']]; + } + // Collected in this response; nothing left in the row to hand out. + $Row->set('cert', '')->save(); + return [200, self::_issuedPayload($Row, $cert)]; + } + + /** + * Signs the stored CSR for the bound host, binds the fingerprint to the + * host, marks the row issued and audits it. Returns the PEM the agent + * gets: the leaf followed by the issuing chain. + * + * A pending host approved this way stops being pending: approving the + * agent is approving the machine. + * + * @param AgentEnrollment $Row the request + * @param string $via token, deploy or admin + * @param string $by the admin, or '' on an automatic path + * + * @throws \RuntimeException when the helper refuses + * + * @return string + */ + private static function _issue(AgentEnrollment $Row, $via, $by) + { + $hostID = (int)$Row->get('hostID'); + $Host = new Host($hostID); + if (!$Host->isValid()) { + throw new \RuntimeException('host no longer exists', 409); + } + list($leaf, $chain) = self::_sign((string)$Row->get('csr'), $hostID); + $parsed = openssl_x509_parse($leaf); + $notAfter = is_array($parsed) && isset($parsed['validTo_time_t']) + ? gmdate('Y-m-d H:i:s', (int)$parsed['validTo_time_t']) + : null; + $now = self::niceDate()->format('Y-m-d H:i:s'); + + $Host->set('agentFingerprint', (string)$Row->get('fingerprint')) + ->set('agentNotAfter', $notAfter) + ->set('agentVersion', (string)$Row->get('agentVersion')) + ->set('agentCheckin', $now); + if ($Host->get('pending')) { + $Host->set('pending', '0'); + } + $Host->save(); + + $Row->set('state', AgentEnrollment::STATE_ISSUED) + ->set('decided', $now) + ->set('decidedBy', (string)$by) + ->set('decidedVia', $via) + ->save(); + self::_audit( + $Row, + sprintf('issued via %s, key %s', $via, substr((string)$Row->get('fingerprint'), 0, 16)), + (string)$by + ); + return $leaf . $chain; + } + + /** + * The staging-and-sudo handshake nodecert.php uses, for the agent type. + * The helper builds the subject from the host id it reads out of the + * staged file; nothing in the CSR's subject is used. + * + * @param string $csr the request, PEM + * @param int $hostID the host to name in the certificate + * + * @throws \RuntimeException when the helper refuses + * + * @return array [leaf PEM, chain PEM] + */ + private static function _sign($csr, $hostID) + { + $staging = FOG_BASE_DIR . DS . 'nodecert-staging'; + if (!is_dir($staging) || !is_writable($staging)) { + throw new \RuntimeException('agent certificate issuance is not configured', 503); + } + $reqid = bin2hex(openssl_random_pseudo_bytes(16)); + $csrfile = $staging . DS . $reqid . '.csr'; + $hostfile = $staging . DS . $reqid . '.agent'; + $outfile = $staging . DS . $reqid . '.pem'; + $chainfile = $staging . DS . $reqid . '.chain'; + file_put_contents($csrfile, $csr); + file_put_contents($hostfile, (int)$hostID . "\n"); + $cmd = 'sudo -n ' + . escapeshellarg(rtrim(FOG_BASE_DIR, DS) . '/bin/fog-sign-node-cert') + . ' agent ' . escapeshellarg($reqid) . ' 2>&1'; + $output = shell_exec($cmd); + $leaf = file_exists($outfile) ? file_get_contents($outfile) : ''; + $chain = file_exists($chainfile) ? file_get_contents($chainfile) : ''; + foreach ([$csrfile, $hostfile, $outfile, $chainfile] as $tmp) { + if (file_exists($tmp)) { + unlink($tmp); + } + } + if (!$leaf) { + throw new \RuntimeException(trim((string)$output) ?: 'signing failed', 503); + } + return [$leaf, $chain]; + } + + /** + * Validated, lower-cased, de-duplicated MACs from the request. + * + * @param mixed $macs whatever the agent sent + * + * @return array + */ + private static function _macs($macs) + { + $out = []; + foreach ((array)$macs as $mac) { + $mac = strtolower(trim((string)$mac)); + if (filter_var($mac, FILTER_VALIDATE_MAC)) { + $out[$mac] = $mac; + } + } + return array_values($out); + } + + /** + * The 202 body. + * + * @param AgentEnrollment $Row the request + * + * @return array + */ + private static function _pendingPayload(AgentEnrollment $Row) + { + return [ + 'status' => 'pending', + 'reason' => (string)$Row->get('reason'), + 'retry_after' => self::RETRY_AFTER + ]; + } + + /** + * The 200 body. + * + * @param AgentEnrollment $Row the request + * @param string $cert leaf plus chain, PEM + * + * @return array + */ + private static function _issuedPayload(AgentEnrollment $Row, $cert) + { + $Host = new Host((int)$Row->get('hostID')); + return [ + 'status' => 'issued', + 'host_id' => (int)$Row->get('hostID'), + 'certificate_pem' => $cert, + 'not_after' => (string)$Host->get('agentNotAfter') + ]; + } + + /** + * Every decision leaves a row. An agent that was let in without a + * trail is the thing an admin cannot answer questions about later. + * + * @param AgentEnrollment $Row the request + * @param string $text what happened + * @param string $by the admin, or '' for the agent itself + * + * @return void + */ + private static function _audit(AgentEnrollment $Row, $text, $by = '') + { + $hostID = (int)$Row->get('hostID'); + $label = $hostID > 0 ? (string)(new Host($hostID))->get('name') : (string)$Row->get('hostname'); + $row = [ + 'type' => 'agent.enroll', + 'subjectType' => 'host', + 'subjectID' => $hostID, + 'subjectLabel' => $label, + 'renderable' => 1, + 'text' => $text + ]; + if ('' === $by) { + $row['authSource'] = Audit::SOURCE_ANONYMOUS; + } + Audit::record($row); + } +} diff --git a/packages/web/src/Agent/Principal.php b/packages/web/src/Agent/Principal.php new file mode 100644 index 0000000000..2c3b495008 --- /dev/null +++ b/packages/web/src/Agent/Principal.php @@ -0,0 +1,134 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +namespace FOG\Agent; + +/** + * Turns the web server's client-certificate variables into a key + * fingerprint the router can bind to a host. + * + * Deliberately NOT a FOGBase: it touches no globals and no database, so + * tests/agent-principal.test.php can drive it with certificates minted on + * the spot and prove what it refuses. The host lookup is the router's. + * + * Two checks, both required, and the second is why this class exists: + * + * 1. The web server said SUCCESS. nginx and Apache both verify the chain + * and the validity dates before PHP ever runs; SSL_CLIENT_VERIFY is + * how they say so. + * 2. PHP verifies the chain AGAIN, against the agent CA bundle and for + * the client-auth purpose. The web server's trust file is whatever + * the vhost was written with -- on an Apache install it is also the + * server's own chain, and an external-CA install can point it + * anywhere -- so "the server accepted it" does not prove "the FOG + * Agent CA issued it". This check does, and it costs one X509 + * verification per request. + * + * The binding is the SPKI fingerprint, the same sha256 of the public key + * that enrollment stored on the host, so a certificate whose key is not + * the enrolled key is not this host's agent whatever its subject says. + * + * @category Principal + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class Principal +{ + /** + * sha256 of a public key's SPKI, as enrollment stores it on the host. + * + * One definition, shared by the CSR side (Enrollment::fingerprint) and + * the certificate side (verify) so the two can never drift apart. + * + * @param mixed $pub the public key (resource before PHP 8, object after), or false + * + * @return string|null the hex fingerprint, null for an unusable key + */ + public static function spkiFingerprint($pub) + { + if (false === $pub) { + return null; + } + $details = openssl_pkey_get_details($pub); + if (!is_array($details) || empty($details['key'])) { + return null; + } + return hash('sha256', (string)$details['key']); + } + + /** + * The PEM the web server handed over, in the form openssl wants. + * + * nginx's $ssl_client_escaped_cert is URL-encoded (its raw variant + * would break the fastcgi record on newlines); Apache's SSL_CLIENT_CERT + * is plain PEM. Tell them apart by the newline: plain PEM always has + * one after its BEGIN line, the escaped form carries %0A instead. + * NOT by "-----BEGIN" -- URL escaping leaves dashes and letters alone, + * so that marker survives escaping and tells you nothing. + * + * @param string $raw the variable as received + * + * @return string + */ + public static function pem($raw) + { + $raw = (string)$raw; + if (false === strpos($raw, "\n")) { + $raw = rawurldecode($raw); + } + return $raw; + } + + /** + * Verifies the calling agent's certificate. + * + * @param array $server the request's $_SERVER + * @param string $bundle path to the agent CA bundle (agent CA + root) + * + * @return array|null ['fingerprint' => sha256 hex, 'not_after' => + * unix time] or null when anything is not right. + * Null carries no reason on purpose: the caller + * answers 401 either way, and the reasons are only + * interesting to someone with the server's logs. + */ + public static function verify(array $server, $bundle) + { + if ('SUCCESS' !== (string)($server['SSL_CLIENT_VERIFY'] ?? '')) { + return null; + } + $pem = self::pem((string)($server['SSL_CLIENT_CERT'] ?? '')); + if ('' === $pem || !is_readable($bundle)) { + return null; + } + $cert = @openssl_x509_read($pem); + if (false === $cert) { + return null; + } + // Chain, dates and the client-auth purpose, against OUR CA. This + // is check 2 in the class comment and it must stay independent + // of what the vhost trusts. + if (true !== openssl_x509_checkpurpose($cert, X509_PURPOSE_SSL_CLIENT, [$bundle])) { + return null; + } + $parsed = openssl_x509_parse($cert); + $fingerprint = self::spkiFingerprint(@openssl_pkey_get_public($cert)); + if (null === $fingerprint || !is_array($parsed)) { + return null; + } + return [ + 'fingerprint' => $fingerprint, + 'not_after' => (int)($parsed['validTo_time_t'] ?? 0), + ]; + } +} diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 8ae972f5d0..20395e9b9f 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -334,6 +334,14 @@ class Authorization extends FOGBase // than left to the unknown-route fallback so the intent is recorded. 'openapi' => null, 'openapiSwaggerAlias' => null, + // fog-agent enrollment: public by construction (the caller is asking + // for its credential), and it decides nothing an admin did not + // approve -- see FOG\Agent\Enrollment. The admin side reads and + // edits hosts, so it carries the host permissions. + 'agentenroll' => null, + 'agentpoll' => null, // fog-agent: gated by the client certificate in Route, not by a token + 'agentenrollments' => 'host.view', + 'agentenrollmentdecide' => 'host.edit', 'export' => 'system.export', 'kernelUpdate' => 'settings.view', 'initrdUpdate' => 'settings.view', diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index 7b5c727cc8..af706089fa 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -6287,9 +6287,14 @@ private static function _bootFileRow($name) if (null === self::$_bootFileRows) { self::$_bootFileRows = []; try { - $rows = self::getClass('BootFileManager')->find(); - foreach ((array)$rows as $row) { - if ($row && $row->isValid()) { + // getIds(), not the manager: 1.6's FOGManagerController has + // no find(). The 1.5 call this replaces threw "undefined + // method" into the catch below on every request, so the + // accelerator had never once been populated -- exactly the + // silent failure that catch was NOT meant to cover. + foreach ((array)self::getIds('bootfile') as $id) { + $row = new \FOG\Items\BootFile((int)$id); + if ($row->isValid()) { self::$_bootFileRows[(string)$row->get('name')] = $row; } } diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index d8e72ef85b..677e3b3317 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 415); + define('FOG_SCHEMA', 416); define('FOG_BCACHE_VER', 359); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Boot/Registration.php b/packages/web/src/Boot/Registration.php index 7105fb06dd..4110a83fd7 100644 --- a/packages/web/src/Boot/Registration.php +++ b/packages/web/src/Boot/Registration.php @@ -368,7 +368,6 @@ private function _fullReg() ->addGroup($groupsToJoin) ->addSnapin($snapinsToJoin) ->addPriMAC($this->PriMAC) - ->addMAC($this->MACs) ->setAD( $useAD, $ADDomain, @@ -390,6 +389,13 @@ private function _fullReg() ); } self::$Host->load(); + // Only after the save: addMAC() writes the hostMAC rows at once + // with the id the object holds, and before save() a new host + // holds none, so the secondaries went in with an empty hostID + // (the insertBatch guard now rejects that outright). The + // primary is unaffected -- addPriMAC() stages it and save() + // writes it once the id exists. + self::$Host->addMAC($this->MACs); Audit::identify( 'host', (int)self::$Host->get('id'), @@ -661,8 +667,7 @@ private function _quickReg() ->set('name', $this->macsimple) ->set('description', $this->description) ->set('modules', $this->modulesToJoin) - ->addPriMAC($this->PriMAC) - ->addMAC($this->MACs); + ->addPriMAC($this->PriMAC); if ($prodkeyget > 0) { $productKey = trim((string)filter_var($stripped['productKey'] ?? '', FILTER_UNSAFE_RAW)); if ($productKey !== '' && !preg_match('/^[A-Za-z0-9\\-]{1,29}$/', $productKey)) { @@ -681,6 +686,13 @@ private function _quickReg() ); } self::$Host->load(); + // Only after the save: addMAC() writes the hostMAC rows at once + // with the id the object holds, and before save() a new host + // holds none, so the secondaries went in with an empty hostID + // (the insertBatch guard now rejects that outright). The + // primary is unaffected -- addPriMAC() stages it and save() + // writes it once the id exists. + self::$Host->addMAC($this->MACs); Audit::identify( 'host', (int)self::$Host->get('id'), diff --git a/packages/web/src/Items/AgentEnrollToken.php b/packages/web/src/Items/AgentEnrollToken.php new file mode 100644 index 0000000000..56a1a6cc17 --- /dev/null +++ b/packages/web/src/Items/AgentEnrollToken.php @@ -0,0 +1,60 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * An admin's pre-approval for fog-agent enrollment. + * + * Only the sha256 of the token is stored. Uses count down to zero; -1 means + * unlimited until the expiry. See schema step 416. + * + * @category AgentEnrollToken + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentEnrollToken extends FOGController +{ + /** + * The database table. + * + * @var string + */ + protected $databaseTable = 'agentEnrollToken'; + /** + * The database fields. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'atID', + 'name' => 'atName', + 'hash' => 'atHash', + 'uses' => 'atUses', + 'expires' => 'atExpires', + 'createdBy' => 'atCreatedBy', + 'created' => 'atCreated' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'hash' + ]; +} diff --git a/packages/web/src/Items/AgentEnrollment.php b/packages/web/src/Items/AgentEnrollment.php new file mode 100644 index 0000000000..2341f5f1ec --- /dev/null +++ b/packages/web/src/Items/AgentEnrollment.php @@ -0,0 +1,76 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * One fog-agent's request to be issued a certificate. + * + * Keyed by the key's fingerprint, one row per agent key, refreshed on every + * repeat of the request while it waits. See schema step 416 for the fields. + * + * @category AgentEnrollment + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentEnrollment extends FOGController +{ + const STATE_PENDING = 'pending'; + const STATE_ISSUED = 'issued'; + const STATE_DENIED = 'denied'; + + /** + * The database table. + * + * @var string + */ + protected $databaseTable = 'agentEnrollment'; + /** + * The database fields. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'aeID', + 'hostID' => 'aeHostID', + 'fingerprint' => 'aeFingerprint', + 'csr' => 'aeCSR', + 'identity' => 'aeIdentity', + 'hostname' => 'aeHostname', + 'os' => 'aeOS', + 'arch' => 'aeArch', + 'agentVersion' => 'aeAgentVersion', + 'remoteIP' => 'aeRemoteIP', + 'reason' => 'aeReason', + 'state' => 'aeState', + 'cert' => 'aeCert', + 'created' => 'aeCreated', + 'updated' => 'aeUpdated', + 'decided' => 'aeDecided', + 'decidedBy' => 'aeDecidedBy', + 'decidedVia' => 'aeDecidedVia' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'fingerprint', + 'csr' + ]; +} diff --git a/packages/web/src/Items/Host.php b/packages/web/src/Items/Host.php index 1e9e2a1c91..2878a42bef 100644 --- a/packages/web/src/Items/Host.php +++ b/packages/web/src/Items/Host.php @@ -124,7 +124,15 @@ class Host extends FOGController 'efiexit' => 'hostExitEfi', 'enforce' => 'hostEnforce', 'token' => 'hostInfoKey', - 'tokenlock' => 'hostInfoLock' + 'tokenlock' => 'hostInfoLock', + // fog-agent binding (schema 416). agentFingerprint is the sha256 of + // the agent key's SubjectPublicKeyInfo and is what a client + // certificate is matched against; the rest is what the agent last + // reported about itself. + 'agentFingerprint' => 'hostAgentFingerprint', + 'agentNotAfter' => 'hostAgentNotAfter', + 'agentVersion' => 'hostAgentVersion', + 'agentCheckin' => 'hostAgentCheckin' ]; /** * The required fields diff --git a/packages/web/src/Managers/AgentEnrollTokenManager.php b/packages/web/src/Managers/AgentEnrollTokenManager.php new file mode 100644 index 0000000000..06fc8a6eb4 --- /dev/null +++ b/packages/web/src/Managers/AgentEnrollTokenManager.php @@ -0,0 +1,29 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * Manager for fog-agent enrollment tokens. + * + * @category AgentEnrollTokenManager + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentEnrollTokenManager extends FOGManagerController +{ +} diff --git a/packages/web/src/Managers/AgentEnrollmentManager.php b/packages/web/src/Managers/AgentEnrollmentManager.php new file mode 100644 index 0000000000..7d0a86c3e1 --- /dev/null +++ b/packages/web/src/Managers/AgentEnrollmentManager.php @@ -0,0 +1,29 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * Manager for fog-agent enrollment requests. + * + * @category AgentEnrollmentManager + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentEnrollmentManager extends FOGManagerController +{ +} diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index 1c6f9551b6..2ee0b31a1a 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2466,6 +2466,190 @@ private static function _fixedPaths() ) ) ], + '/agent/v1/enroll' => [ + 'post' => self::_op( + '', + 'agentenroll', + _('FOG Agent enrollment'), + _('Unauthenticated, because this is how an agent obtains ' + . 'the client certificate it will authenticate with ' + . 'afterward. The agent posts a certificate signing ' + . 'request and its firmware identity; the server ' + . 'resolves the machine the way iPXE registration ' + . 'does and answers issued, pending or denied. Pending ' + . 'is the normal first answer for a machine nobody has ' + . 'approved yet -- the agent polls until an admin ' + . 'decides on /agent/enrollment/{id}/{action}, an ' + . 'enrollment token pre-approves it, or the server ' + . 'itself imaged the host recently ' + . '(FOG_AGENT_ENROLL_DEPLOY_WINDOW). A pending agent ' + . 'can do nothing else: without a certificate no other ' + . 'agent route accepts it. Protocol 1.'), + [ + '200' => [ + 'description' => _('Issued. The certificate and ' + . 'the host it binds to.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string', 'enum' => ['issued']], + 'host_id' => ['type' => 'integer'], + 'certificate_pem' => [ + 'type' => 'string', + 'description' => _('The leaf followed by the agent CA, PEM.') + ], + 'not_after' => ['type' => 'string'] + ] + ]]] + ], + '202' => [ + 'description' => _('Pending an admin decision. ' + . 'Poll again after retry_after seconds.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string', 'enum' => ['pending']], + 'reason' => [ + 'type' => 'string', + 'description' => _('Why it waits: unknown-host, ' + . 'known-host-no-agent, rebind, ' + . 'identity-conflict, reissue.') + ], + 'retry_after' => ['type' => 'integer'] + ] + ]]] + ], + '400' => ['description' => _('The CSR is not a usable P-256 request, or a required field is missing.')], + '403' => ['description' => _('Denied by an admin. The agent backs off to hourly.')], + '426' => ['description' => _('The agent speaks a protocol this server does not.')], + '503' => ['description' => _('Approved but the signer is unavailable; the agent retries.')] + ], + [], + [ + 'required' => true, + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'required' => ['protocol', 'csr_pem', 'identity'], + 'properties' => [ + 'protocol' => ['type' => 'integer', 'enum' => [1]], + 'agent_version' => ['type' => 'string'], + 'os' => ['type' => 'string'], + 'arch' => ['type' => 'string'], + 'hostname' => ['type' => 'string'], + 'identity' => [ + 'type' => 'object', + 'description' => _('SMBIOS system UUID, system serial, ' + . 'board serial, chassis asset tag and the MAC list, ' + . 'as fog-agent identity prints them.') + ], + 'csr_pem' => ['type' => 'string'], + 'token' => [ + 'type' => 'string', + 'description' => _('An enrollment token, if the installer was given one.') + ] + ] + ]]] + ] + ) + ], + '/agent/v1/poll' => [ + 'post' => self::_op( + '', + 'agentpoll', + _('FOG Agent poll'), + _('Authenticated by the client certificate enrollment ' + . 'issued, verified by the web server and bound to ' + . 'the host by its key fingerprint before the route ' + . 'runs; no token or session applies. Records the ' + . 'check-in and answers with what this server can ' + . 'do. A certificate that no longer binds to a live ' + . 'host gets 401, which tells the agent to enroll ' + . 'again.'), + [ + '200' => [ + 'description' => _('The host this certificate is, ' + . 'and the capabilities this server offers.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string', 'enum' => ['ok']], + 'protocol' => ['type' => 'integer'], + 'host' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'] + ] + ], + 'capabilities' => [ + 'type' => 'array', + 'items' => ['type' => 'string'] + ], + 'poll_interval' => ['type' => 'integer'], + 'server_time' => ['type' => 'string'] + ] + ]]] + ], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')] + ], + [], + [ + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'agent_version' => ['type' => 'string'] + ] + ]]] + ] + ) + ], + '/agent/enrollments' => [ + 'get' => self::_op( + '', + 'agentenrollments', + _('Pending agent enrollments'), + _('Every enrollment still waiting for a decision, ' + . 'without the CSR. What the Pending Agents page reads.'), + $json( + [ + 'type' => 'object', + 'properties' => [ + 'data' => ['type' => 'array', 'items' => ['type' => 'object']], + 'msg' => ['type' => 'string'] + ] + ], + _('Pending enrollment rows.') + ) + ) + ], + '/agent/enrollment/{id}/{action}' => [ + 'post' => self::_op( + '', + 'agentenrollmentdecide', + _('Approve or deny an agent enrollment'), + _('approve signs the CSR, binds the certificate to the ' + . 'host and takes the host out of pending; deny ' + . 'records the refusal. Either way the agent learns ' + . 'the outcome on its next poll. Audited as ' + . 'agent.enroll.'), + [ + '200' => ['description' => _('Decided.')], + '404' => ['description' => _('No such enrollment, or no such action.')], + '503' => ['description' => _('The signer is unavailable; nothing changed.')] + ] + self::_conflictResponse( + _('The enrollment is no longer pending.') + ), + [ + self::_idParameter(), + [ + 'name' => 'action', + 'in' => 'path', + 'required' => true, + 'schema' => ['type' => 'string', 'enum' => ['approve', 'deny']] + ] + ] + ) + ], '/system/openapi' => [ 'get' => self::_op( '', diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index c66b04d1b9..36b5621595 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -190,6 +190,15 @@ class Route extends FOGBase * @var bool */ public static $apiRequest = false; + /** + * The host behind the verified fog-agent client certificate, set + * before dispatch for every /agent/v1/ route except enroll. Handlers + * under that prefix can rely on it: a request with no bound host + * never reaches them. + * + * @var \FOG\Items\Host|null + */ + public static $agentHost = null; /** * Requested relation-expansion tokens (lowercased) from ?expand=a,b,c. * @@ -876,7 +885,12 @@ public function __construct() // swagger.json is where a great many people and tools look first, // Swagger UI having been the name for this long before it was // renamed OpenAPI. Same handler, same document. - $webrootbase . 'swagger.json' + $webrootbase . 'swagger.json', + // fog-agent enrollment. The caller has no certificate yet -- that + // is what it is asking for -- so it cannot authenticate. The + // handler issues nothing on its own authority: see + // FOG\Agent\Enrollment for the three approvals. + $webrootbase . 'agent/v1/enroll' ]; /** * A plugin may declare one of its /ext/ routes reachable without API @@ -911,6 +925,28 @@ public function __construct() * traffic (the CSRF-able surface) without touching headless clients. */ $sessionAuthed = self::$FOGUser->isValid(); + /** + * fog-agent past enrollment authenticates with the client + * certificate the web server verified -- never a token, never a + * session (design 0001 5.2). Decided HERE, before any route + * matches, so a handler under the prefix cannot be reached + * without a bound host whatever it forgets to check. Enroll is + * the one exception and it is in $unauthexact above. + */ + if (!$isunauth + && 0 === strpos($requripath, $webrootbase . 'agent/v1/') + ) { + self::$agentHost = self::_agentPrincipal(); + if (!self::$agentHost) { + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_UNAUTHORIZED, + json_encode(['error' => 'client certificate required']) + ); + } + // Authenticated by certificate: the token and session tests + // below are for humans and API tokens and would only 401 it. + $isunauth = true; + } if (!$sessionAuthed && !$isunauth ) { @@ -1511,6 +1547,12 @@ protected static function defineRoutes(\FastRoute\RouteCollector $r) ); self::_registerRoute($r, 'HEAD|GET', '/system/[status|info]', [__CLASS__, 'status'], 'status'); self::_registerRoute($r, 'GET', '/system/openapi', [__CLASS__, 'openapi'], 'openapi'); + // fog-agent (protocol v1). The enroll route is public, listed in + // $unauthexact above; the other two are the admin's side of it. + self::_registerRoute($r, 'POST', '/agent/v1/enroll', [__CLASS__, 'agentEnroll'], 'agentenroll'); + self::_registerRoute($r, 'POST', '/agent/v1/poll', [__CLASS__, 'agentPoll'], 'agentpoll'); + self::_registerRoute($r, 'GET', '/agent/enrollments', [__CLASS__, 'agentEnrollments'], 'agentenrollments'); + self::_registerRoute($r, 'POST', '/agent/enrollment/[i:id]/[*:action]', [__CLASS__, 'agentEnrollmentDecide'], 'agentenrollmentdecide'); // Alias. swagger.json is the filename people and tooling reach // for first -- Swagger UI predates the OpenAPI rename and the // habit stuck. Same handler, same document, so neither name is @@ -2742,6 +2784,198 @@ public static function status() 'msg' => _('success') ]; } + /** + * fog-agent enrollment, protocol v1. + * + * Public: the agent is asking for the credential it would otherwise + * present. Everything that decides lives in FOG\Agent\Enrollment; this + * only moves bytes. The status codes are the contract the agent was + * written against (its docs/design/protocol-v1.md): 200 issued, 202 + * pending, 403 denied, 426 wrong protocol. + * + * @return void + */ + public static function agentEnroll() + { + $remoteIP = filter_var((string)self::$remoteaddr, FILTER_VALIDATE_IP) + ? (string)self::$remoteaddr + : ''; + list($code, $payload) = \FOG\Agent\Enrollment::handle(self::_jsonBody(), $remoteIP); + HTTPResponseCodes::breakHead($code, json_encode($payload)); + } + /** + * The host behind this request's client certificate, or null. + * + * Principal::verify() does the cryptography; this is the binding: the + * certificate's key must be the key enrollment stored on exactly one + * live, non-pending host. A deleted host or a re-enrolled key both + * come back null, which the gate turns into a 401 -- and a 401 is what + * tells the agent to drop its certificate and enroll again. + * + * @return \FOG\Items\Host|null + */ + private static function _agentPrincipal() + { + $verified = \FOG\Agent\Principal::verify( + $_SERVER, + BASEPATH . 'management/other/agent-ca-bundle.pem' + ); + if (null === $verified) { + return null; + } + // A direct statement, NOT getIds('host'): that lookup puts the + // calling user's site scope into the WHERE, and this request has + // no user -- the agent IS the host -- so the scope resolves to + // 1=0 and every agent on the server gets 401. Proved live + // 2026-09-03: verify() passed, getIds() returned [], poll failed. + // One indexed column, so this is as cheap as the scoped path. + // + // LIMIT 2, because exactly one host may carry this key. Two would + // mean the binding is ambiguous, and an ambiguous principal is no + // principal. + $res = self::$DB->query( + 'SELECT `hostID` FROM `hosts`' + . ' WHERE `hostAgentFingerprint` = :fp AND `hostPending` = 0' + . ' LIMIT 2', + [], + ['fp' => $verified['fingerprint']] + ); + if (false !== $res->error) { + return null; + } + $rows = $res->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + if (!is_array($rows) || 1 !== count($rows)) { + return null; + } + $Host = new \FOG\Items\Host((int)$rows[0]['hostID']); + if (!$Host->isValid()) { + return null; + } + return $Host; + } + /** + * fog-agent's poll: "I am here, this version; anything for me?" + * + * The hard floor of protocol 1 (design 0001 5.1). Records the check-in + * and answers with what this server can do, so a feature the server + * lacks is simply not listed and the agent leaves it idle. Nothing + * secret rides this answer. Written through the manager rather than + * Host::save() because a save re-writes the MAC association on every + * call, and this is called every few minutes by every host. + * + * @return void + */ + public static function agentPoll() + { + $Host = self::$agentHost; + $body = self::_jsonBody(); + $version = substr( + preg_replace('/[^A-Za-z0-9.+_-]/', '', (string)($body['agent_version'] ?? '')), + 0, + 50 + ); + $fields = ['agentCheckin' => self::niceDate()->format('Y-m-d H:i:s')]; + if ('' !== $version) { + $fields['agentVersion'] = $version; + } + self::getClass('HostManager')->update( + ['id' => (int)$Host->get('id')], + '', + $fields + ); + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_OK, + json_encode( + [ + 'status' => 'ok', + 'protocol' => \FOG\Agent\Enrollment::PROTOCOL, + 'host' => [ + 'id' => (int)$Host->get('id'), + 'name' => (string)$Host->get('name'), + ], + // Grows as capabilities land server-side; an empty + // list is a valid answer and the agent idles on it. + 'capabilities' => [], + 'poll_interval' => 300, + 'server_time' => self::niceDate()->format('c'), + ] + ) + ); + } + /** + * The pending fog-agent enrollments, for the admin's list. + * + * The CSR is left out: it is large and the admin decides on the + * identity, the hostname and where the request came from, not on the + * key bytes. + * + * @return void + */ + public static function agentEnrollments() + { + $rows = []; + foreach ((array)self::getList('agentenrollment', ['state' => 'pending'], 'AND', 'id') as $row) { + $row = (array)$row; + $identity = json_decode((string)($row['identity'] ?? ''), true); + $rows[] = [ + 'id' => (int)($row['id'] ?? 0), + 'hostID' => (int)($row['hostID'] ?? 0), + 'hostname' => (string)($row['hostname'] ?? ''), + 'os' => (string)($row['os'] ?? ''), + 'arch' => (string)($row['arch'] ?? ''), + 'agentVersion' => (string)($row['agentVersion'] ?? ''), + 'remoteIP' => (string)($row['remoteIP'] ?? ''), + 'reason' => (string)($row['reason'] ?? ''), + 'fingerprint' => (string)($row['fingerprint'] ?? ''), + 'identity' => is_array($identity) ? $identity : [], + 'created' => (string)($row['created'] ?? ''), + 'updated' => (string)($row['updated'] ?? '') + ]; + } + self::$data = ['data' => $rows, 'msg' => _('success')]; + } + /** + * Approve or deny one pending fog-agent enrollment. + * + * Approving signs the stored request and binds the key to the host; the + * agent collects the certificate on its next poll. Denying pins the key + * as refused so its repeats are answered without re-deciding. + * + * @param int $id the enrollment row + * @param string $action approve or deny + * + * @return void + */ + public static function agentEnrollmentDecide($id, $action) + { + $by = (string)self::$FOGUser->get('name'); + try { + switch ((string)$action) { + case 'approve': + $Row = \FOG\Agent\Enrollment::approve((int)$id, $by); + break; + case 'deny': + $Row = \FOG\Agent\Enrollment::deny((int)$id, $by); + break; + default: + self::sendResponse(HTTPResponseCodes::HTTP_NOT_FOUND, _('unknown action')); + return; + } + } catch (\RuntimeException $e) { + $code = (int)$e->getCode(); + self::sendResponse( + $code >= 400 && $code <= 599 ? $code : HTTPResponseCodes::HTTP_INTERNAL_SERVER_ERROR, + json_encode(['error' => $e->getMessage()]) + ); + return; + } + self::$data = [ + 'id' => (int)$Row->get('id'), + 'hostID' => (int)$Row->get('hostID'), + 'state' => (string)$Row->get('state'), + 'msg' => _('success') + ]; + } /** * Serves an OpenAPI description of this server's API. * diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 8da633c369..b6720665bd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -129,4319 +129,4043 @@ parameters: - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 385 + count: 386 path: packages/web/commons/schema.php - - message: '#^PHPDoc type string of property FOG\\Events\\HostList\:\:\$active is not covariant with PHPDoc type bool of overridden property FOG\\Base\\Event\:\:\$active\.$#' - identifier: property.phpDocType + message: '#^Parameter \#2 \$id of method FOG\\Base\\FOGManagerController\:\:exists\(\) expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Events/HostList.php + path: packages/web/maintenance/create_update_node.php - - message: '#^Property FOG\\Events\\HostList\:\:\$active \(string\) does not accept default value of type false\.$#' - identifier: property.defaultValue + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse count: 1 - path: packages/web/src/Events/HostList.php + path: packages/web/management/index.php - - message: '#^PHPDoc tag @var has invalid value \(\$name\)\: Unexpected token "\$name", expected type at offset 53 on line 4$#' - identifier: phpDoc.parseError + message: '#^Strict comparison using \!\=\= between '''' and '''' will always evaluate to false\.$#' + identifier: notIdentical.alwaysFalse count: 1 - path: packages/web/src/Hooks/BootItem.php + path: packages/web/management/index.php - - message: '#^PHPDoc tag @throws with type FOG\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Variable \$HookManager might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Hooks/BootTask.php + path: packages/web/management/index.php - - message: '#^Parameter \#1 \$txt of static method FOG\\Base\\Hook\:\:log\(\) expects string, true given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/HookDebugger.php + message: '#^Variable \$currentUser might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: packages/web/management/index.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/HookDebugger.php + message: '#^Variable \$foglang might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: packages/web/management/index.php - - message: '#^Parameter \#3 \$logfile of static method FOG\\Base\\Hook\:\:log\(\) expects int, bool given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/HookDebugger.php + message: '#^Accessing self\:\:\$FOGUser outside of class scope\.$#' + identifier: outOfClass.self + count: 8 + path: packages/web/management/other/index.php - - message: '#^Parameter \#4 \$logbrow of static method FOG\\Base\\Hook\:\:log\(\) expects int, bool given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/HookDebugger.php + message: '#^Accessing self\:\:\$HookManager outside of class scope\.$#' + identifier: outOfClass.self + count: 2 + path: packages/web/management/other/index.php - - message: '#^Parameter \#1 \$txt of static method FOG\\Base\\Hook\:\:log\(\) expects string, true given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/Template.php + message: '#^Accessing self\:\:\$pluginIsAvailable outside of class scope\.$#' + identifier: outOfClass.self + count: 2 + path: packages/web/management/other/index.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type + message: '#^Calling self\:\:displayTheme\(\) outside of class scope\.$#' + identifier: outOfClass.self count: 1 - path: packages/web/src/Hooks/Template.php + path: packages/web/management/other/index.php - - message: '#^Parameter \#3 \$logfile of static method FOG\\Base\\Hook\:\:log\(\) expects int, false given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Hooks/Template.php + message: '#^Calling self\:\:formatByteSize\(\) outside of class scope\.$#' + identifier: outOfClass.self + count: 2 + path: packages/web/management/other/index.php - - message: '#^Parameter \#4 \$logbrow of static method FOG\\Base\\Hook\:\:log\(\) expects int, true given\.$#' - identifier: argument.type + message: '#^Calling self\:\:formatTime\(\) outside of class scope\.$#' + identifier: outOfClass.self count: 1 - path: packages/web/src/Hooks/Template.php + path: packages/web/management/other/index.php - - message: '#^Constructor of class FOG\\Pages\\ActivityManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Calling self\:\:getClass\(\) outside of class scope\.$#' + identifier: outOfClass.self count: 1 - path: packages/web/src/Pages/ActivityManagement.php + path: packages/web/management/other/index.php - - message: '#^Constructor of class FOG\\Pages\\ApiDocumentation has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Calling self\:\:getMessage\(\) outside of class scope\.$#' + identifier: outOfClass.self count: 1 - path: packages/web/src/Pages/ApiDocumentation.php + path: packages/web/management/other/index.php - - message: '#^Constructor of class FOG\\Pages\\AuditManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/AuditManagement.php + message: '#^Calling self\:\:getSetting\(\) outside of class scope\.$#' + identifier: outOfClass.self + count: 7 + path: packages/web/management/other/index.php - - message: '#^Constructor of class FOG\\Pages\\ClientManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/src/Pages/ClientManagement.php + path: packages/web/management/other/index.php - - message: '#^Call to function unset\(\) contains undefined variable \$SystemUptime\.$#' - identifier: unset.variable - count: 1 - path: packages/web/src/Pages/DashboardPage.php + message: '#^Variable \$this might not be defined\.$#' + identifier: variable.undefined + count: 11 + path: packages/web/management/other/index.php - - message: '#^Call to function unset\(\) contains undefined variable \$fields\.$#' - identifier: unset.variable + message: '#^Call to function is_array\(\) with null will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Pages/DashboardPage.php - - - - message: '#^Call to function unset\(\) contains undefined variable \$tftp\.$#' - identifier: unset.variable - count: 2 - path: packages/web/src/Pages/DashboardPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Constructor of class FOG\\Pages\\DashboardPage has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Call to function is_string\(\) with null will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Pages/DashboardPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Static property FOG\\Pages\\DashboardPage\:\:\$_tftp is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/DashboardPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Variable \$pendingMACs might not be defined\.$#' - identifier: variable.undefined + message: '#^Empty array passed to foreach\.$#' + identifier: foreach.emptyArray count: 1 - path: packages/web/src/Pages/DashboardPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Call to function unset\(\) contains undefined variable \$findWhere\.$#' - identifier: unset.variable + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Call to function unset\(\) contains undefined variable \$setWhere\.$#' - identifier: unset.variable - count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 5 + path: packages/web/src/Auth/Authorization.php - - message: '#^Call to function unset\(\) contains undefined variable \$val\.$#' - identifier: unset.variable + message: '#^Method FOG\\Auth\\Authorization\:\:_pluginScopeIDs\(\) never returns array so it can be removed from the return type\.$#' + identifier: return.unusedType count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Cannot access property \$name on null\.$#' - identifier: property.nonObject + message: '#^Method FOG\\Auth\\Authorization\:\:_pluginScopeWhere\(\) never returns string so it can be removed from the return type\.$#' + identifier: return.unusedType count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Constructor of class FOG\\Pages\\FOGConfigurationPage has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + message: '#^PHPDoc tag @throws with type FOG\\Auth\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 4 + path: packages/web/src/Auth/Authorization.php - - message: '#^Offset ''FOG_PXE_HIDDENMENU…''\|''FOG_PXE_MENU_TIMEOUT'' on array\{FOG_PXE_HIDDENMENU_TIMEOUT\: true, FOG_PXE_MENU_TIMEOUT\: true\} in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset + message: '#^Parameter \#1 \$input of function array_values contains unresolvable type\.$#' + identifier: argument.unresolvableType count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Offset ''refresh'' does not exist on array\{checkbox\: array, numeric\: array, ip\: array\}\.$#' - identifier: offsetAccess.notFound + message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php - - - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Ternary operator condition is always true\.$#' - identifier: ternary.alwaysTrue + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Auth/Authorization.php - - message: '#^Variable \$ip might not be defined\.$#' - identifier: variable.undefined + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Base/EventManager.php - - message: '#^Variable \$objGetter might not be defined\.$#' - identifier: variable.undefined + message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/FOGConfigurationPage.php + path: packages/web/src/Base/EventManager.php - - message: '#^Variable \$set might not be defined\.$#' - identifier: variable.undefined - count: 11 - path: packages/web/src/Pages/FOGConfigurationPage.php + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: packages/web/src/Base/EventManager.php - - message: '#^Constructor of class FOG\\Pages\\GroupManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Method FOG\\Base\\EventManager\:\:register\(\) should return bool but returns \$this\(FOG\\Base\\EventManager\)\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/GroupManagement.php + path: packages/web/src/Base/EventManager.php - - message: '#^Method FOG\\Pages\\GroupManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Pages/GroupManagement.php + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 3 + path: packages/web/src/Base/EventManager.php - - message: '#^Method FOG\\Pages\\GroupManagement\:\:getModulesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^PHPDoc tag @var above a method has no effect\.$#' + identifier: varTag.misplaced count: 1 - path: packages/web/src/Pages/GroupManagement.php + path: packages/web/src/Base/EventManager.php - - message: '#^Method FOG\\Pages\\GroupManagement\:\:getPrintersList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/GroupManagement.php + path: packages/web/src/Base/EventManager.php - - message: '#^Method FOG\\Pages\\GroupManagement\:\:getSnapinsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse count: 1 - path: packages/web/src/Pages/GroupManagement.php - - - - message: '#^Parameter \#1 \$array \(array\, mixed\>\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' - identifier: arrayFilter.same - count: 1 - path: packages/web/src/Pages/GroupManagement.php - - - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Pages/GroupManagement.php + path: packages/web/src/Base/EventManager.php - - message: '#^Parameter \#3 \$body of static method FOG\\Base\\FOGPage\:\:makeModal\(\) expects string, null given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Pages/GroupManagement.php + message: '#^Access to an undefined property FOG\\Base\\FOGBase\:\:\$databaseFields\.$#' + identifier: property.notFound + count: 3 + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: packages/web/src/Pages/GroupManagement.php + message: '#^Call to an undefined method FOG\\Base\\FOGBase\:\:key\(\)\.$#' + identifier: method.notFound + count: 3 + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) \(void\) is used\.$#' - identifier: method.void - count: 2 - path: packages/web/src/Pages/GroupManagement.php + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 3 + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$printers might not be defined\.$#' - identifier: variable.undefined + message: '#^Call to function is_array\(\) with list will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/GroupManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$val might not be defined\.$#' - identifier: variable.undefined + message: '#^Call to function is_array\(\) with non\-falsy\-string will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Pages/GroupManagement.php - - - - message: '#^Access to an undefined property FOG\\Pages\\HostManagement\:\:\$exitEfi\.$#' - identifier: property.notFound - count: 5 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Access to an undefined property FOG\\Pages\\HostManagement\:\:\$exitNorm\.$#' - identifier: property.notFound - count: 5 - path: packages/web/src/Pages/HostManagement.php + message: '#^Call to function is_bool\(\) with bool will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: packages/web/src/Base/FOGBase.php - - message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' + message: '#^Call to function is_int\(\) with int will always evaluate to true\.$#' identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\HostManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Call to function is_numeric\(\) with \*NEVER\* will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:getModulesList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Pages/HostManagement.php + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 8 + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:getPrintersList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:getSnapinsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Comparison operation "\>\=" between ''1''\|''2''\|''3''\|''4''\|''5''\|''6''\|''7'' and 0 is always true\.$#' + identifier: greaterOrEqual.alwaysTrue count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:pending\(\) should return false but empty return statement found\.$#' - identifier: return.empty + message: '#^Default value of the parameter \#2 \$key \(false\) of method FOG\\Base\\FOGBase\:\:aesdecrypt\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:pending\(\) should return false but return statement is missing\.$#' - identifier: return.missing + message: '#^Default value of the parameter \#2 \$key \(false\) of method FOG\\Base\\FOGBase\:\:aesencrypt\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:pendingMacs\(\) should return false but empty return statement found\.$#' - identifier: return.empty + message: '#^Default value of the parameter \#3 \$enctype \(string\) of method FOG\\Base\\FOGBase\:\:aesdecrypt\(\) is incompatible with type int\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\HostManagement\:\:pendingMacs\(\) should return false but return statement is missing\.$#' - identifier: return.missing + message: '#^Default value of the parameter \#3 \$enctype \(string\) of method FOG\\Base\\FOGBase\:\:aesencrypt\(\) is incompatible with type int\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/HostManagement.php - - - - message: '#^Parameter \#3 \$body of static method FOG\\Base\\FOGPage\:\:makeModal\(\) expects string, null given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Pages/HostManagement.php - - - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void - count: 3 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 2 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Ternary operator condition is always false\.$#' - identifier: ternary.alwaysFalse + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$code might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$msg might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:__construct\(\) with return type void returns \$this\(FOG\\Base\\FOGBase\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$val might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) has invalid return type FOG\\Base\\key\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Pages/HostManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' - identifier: binaryOp.invalid + message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns \(int\|string\)\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\ImpersonateManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns int\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImpersonateManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\ImageManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns int\|string\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Expression on left side of \?\? is not nullable\.$#' - identifier: nullCoalesce.expr + message: '#^Method FOG\\Base\\FOGBase\:\:cryptoRandSecure\(\) should return string but returns \(float\|int\)\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\ImageManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Base\\FOGBase\:\:formatByteSize\(\) should return float but returns string\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\ImageManagement\:\:getSessionsList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Base\\FOGBase\:\:getHostItem\(\) should return array\|object but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\ImageManagement\:\:getStoragegroupsList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Pages/ImageManagement.php - - - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but returns array\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$msgSuccess might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$storagegroups might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:sendData\(\) should return string but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$titleFail might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:sendData\(\) should return string but returns array\\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Variable \$titleSuccess might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGBase\:\:setSetting\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Pages/ImageManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\IpxeManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Method FOG\\Base\\FOGBase\:\:setSetting\(\) should return FOG\\Base\\this but returns bool\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/IpxeManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\ModuleManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Method FOG\\Base\\FOGBase\:\:var_dump_log\(\) should return string\|null but return statement is missing\.$#' + identifier: return.missing count: 1 - path: packages/web/src/Pages/ModuleManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\ModuleManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Pages/ModuleManagement.php + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 5 + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Offset string does not exist on ''ABCDEFGHIJKLMNOPQRS…''\.$#' + identifier: offsetAccess.notFound count: 1 - path: packages/web/src/Pages/ModuleManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\PluginManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/PluginManagement.php + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 14 + path: packages/web/src/Base/FOGBase.php - - message: '#^PHPDoc tag @return has invalid value \(false;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 121 on line 6$#' - identifier: phpDoc.parseError + message: '#^Parameter \#1 \$array \(array\, non\-falsy\-string\>\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' + identifier: arrayFilter.same count: 1 - path: packages/web/src/Pages/PluginManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^PHPDoc tag @throws with type FOG\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Parameter \#1 \$array \(array\{''autologout'', ''displaymanager'', ''hostnamechanger'', ''hostregister'', ''powermanagement'', ''printermanager'', ''snapinclient'', ''taskreboot'', \.\.\.\}\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' + identifier: arrayFilter.same count: 1 - path: packages/web/src/Pages/PluginManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Parameter \#1 \$main of static method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + message: '#^Parameter \#1 \$haystack of callable ''stripos''\|''strpos'' expects string, array given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Pages/PluginManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Parameter \#1 \(array\) of echo cannot be converted to string\.$#' - identifier: echo.nonString - count: 1 - path: packages/web/src/Pages/PluginManagement.php + message: '#^Parameter \#1 \$method of function openssl_cipher_iv_length expects string, int given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Base/FOGBase.php - - message: '#^Parameter \#2 \$hookMain of static method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + message: '#^Parameter \#1 \$string of function strlen expects string, float\|int given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Pages/PluginManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Ternary operator condition is always false\.$#' - identifier: ternary.alwaysFalse - count: 3 - path: packages/web/src/Pages/PluginManagement.php + message: '#^Parameter \#1 \$timezone of class DateTimeZone constructor expects string, object given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Base/FOGBase.php - - message: '#^Constructor of class FOG\\Pages\\PrinterManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Parameter \#2 \$id of static method FOG\\Router\\Route\:\:delete\(\) expects int, string\|false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/PrinterManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\PrinterManagement\:\:getHostsDefaultList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#2 \$method of function openssl_decrypt expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/PrinterManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\PrinterManagement\:\:getHostsList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#2 \$method of function openssl_encrypt expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/PrinterManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^PHPDoc tag @throws with type FOG\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Parameter \#2 \$start of function substr expects int, float given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/PrinterManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Property FOG\\Pages\\PrinterManagement\:\:\$_config is unused\.$#' - identifier: property.unused + message: '#^Parameter \#3 \$length of function substr expects int, float given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/PrinterManagement.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Call to function is_array\(\) with \*NEVER\* will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: packages/web/src/Pages/ProcessLogin.php + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 2 + path: packages/web/src/Base/FOGBase.php - - message: '#^Empty array passed to foreach\.$#' - identifier: foreach.emptyArray - count: 1 - path: packages/web/src/Pages/ProcessLogin.php + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse + count: 2 + path: packages/web/src/Base/FOGBase.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 1 - path: packages/web/src/Pages/ProcessLogin.php + message: '#^Static property FOG\\Base\\FOGBase\:\:\$TimeZone \(object\) in empty\(\) is not falsy\.$#' + identifier: empty.property + count: 3 + path: packages/web/src/Base/FOGBase.php - - message: '#^Method FOG\\Pages\\ProcessLogin\:\:processMainLogin\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void - count: 3 - path: packages/web/src/Pages/ProcessLogin.php + message: '#^Static property FOG\\Base\\FOGBase\:\:\$httpproto \(string\) does not accept default value of type false\.$#' + identifier: property.defaultValue + count: 1 + path: packages/web/src/Base/FOGBase.php - - message: '#^Offset ''icon'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Offset ''label'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset + message: '#^Variable \$mac might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Offset ''url'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' - identifier: isset.offset + message: '#^Variable \$sesVars in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Property FOG\\Pages\\ProcessLogin\:\:\$_langMenu is unused\.$#' - identifier: property.unused + message: '#^Variable \$token might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGBase.php - - message: '#^Result of static method FOG\\Pages\\ProcessLogin\:\:mainLoginForm\(\) \(void\) is used\.$#' - identifier: staticMethod.void - count: 3 - path: packages/web/src/Pages/ProcessLogin.php + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: packages/web/src/Base/FOGController.php - - message: '#^Strict comparison using \=\=\= between 0 and 0 will always evaluate to true\.$#' - identifier: identical.alwaysTrue + message: '#^Method FOG\\Base\\FOGController\:\:__construct\(\) with return type void returns \$this\(FOG\\Base\\FOGController\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGController.php - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable + message: '#^Method FOG\\Base\\FOGController\:\:__destruct\(\) with return type void returns false but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Pages/ProcessLogin.php + path: packages/web/src/Base/FOGController.php - - message: '#^Argument of an invalid type string supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable + message: '#^Method FOG\\Base\\FOGController\:\:destroy\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Pages/ReportManagement.php + path: packages/web/src/Base/FOGController.php - - message: '#^Call to function _\(\) on a separate line has no effect\.$#' - identifier: function.resultUnused - count: 15 - path: packages/web/src/Pages/ReportManagement.php + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 9 + path: packages/web/src/Base/FOGController.php - - message: '#^Constructor of class FOG\\Pages\\ReportManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/ReportManagement.php + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type + count: 4 + path: packages/web/src/Base/FOGController.php - - message: '#^Static method FOG\\Pages\\ReportManagement\:\:_reportNamesForTranslation\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: packages/web/src/Pages/ReportManagement.php + message: '#^Parameter \#3 \$c of method FOG\\Base\\FOGController\:\:buildQuery\(\) expects array, null given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Base/FOGController.php - - message: '#^Constructor of class FOG\\Pages\\RoleManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Property FOG\\Base\\FOGController\:\:\$databaseTable \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 - path: packages/web/src/Pages/RoleManagement.php + path: packages/web/src/Base/FOGController.php - - message: '#^Method FOG\\Pages\\RoleManagement\:\:getSitesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$columns might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/RoleManagement.php + path: packages/web/src/Base/FOGController.php - - message: '#^Method FOG\\Pages\\RoleManagement\:\:getUserGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$idField might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/RoleManagement.php + path: packages/web/src/Base/FOGController.php - - message: '#^Method FOG\\Pages\\RoleManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' + message: '#^Binary operation "\+" between string and string results in an error\.$#' + identifier: binaryOp.invalid + count: 2 + path: packages/web/src/Base/FOGCore.php + + - + message: '#^Method FOG\\Base\\FOGCore\:\:setEnv\(\) with return type void returns mixed but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Pages/RoleManagement.php + path: packages/web/src/Base/FOGCore.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Parameter \#1 \$size of static method FOG\\Base\\FOGBase\:\:formatByteSize\(\) expects float\|int, string\|false\|null given\.$#' + identifier: argument.type count: 3 - path: packages/web/src/Pages/RoleManagement.php - - - - message: '#^Access to an undefined property FOG\\Pages\\SchemaUpdaterPage\:\:\$schema\.$#' - identifier: property.notFound - count: 4 - path: packages/web/src/Pages/SchemaUpdaterPage.php + path: packages/web/src/Base/FOGCore.php - - message: '#^Constant FOG_SCHEMA_INSTALL_TOKEN not found\.$#' - identifier: constant.notFound + message: '#^Parameter \#2 \$newvalue of function ini_set expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/SchemaUpdaterPage.php + path: packages/web/src/Base/FOGCore.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^Variable \$loadAvg might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/SchemaUpdaterPage.php + path: packages/web/src/Base/FOGCore.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: packages/web/src/Pages/SchemaUpdaterPage.php + message: '#^Access to an undefined property FOG\\Base\\FOGManagerController\:\:\$sqlTotalStr\.$#' + identifier: property.notFound + count: 2 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type - count: 4 - path: packages/web/src/Pages/SchemaUpdaterPage.php + message: '#^Access to an undefined property FOG\\Base\\FOGManagerController\:\:\$tablename\.$#' + identifier: property.notFound + count: 2 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Path in include\(\) "/commons/schema\.php" is not a file or it does not exist\.$#' - identifier: include.fileNotFound - count: 1 - path: packages/web/src/Pages/SchemaUpdaterPage.php + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Constructor of class FOG\\Pages\\ServerInfo has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Call to function unset\(\) contains undefined variable \$findKeys\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Parameter \#1 \$size of static method FOG\\Base\\FOGBase\:\:formatByteSize\(\) expects float\|int, string given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Pages/ServerInfo.php + message: '#^Cannot call method prepare\(\) on resource\.$#' + identifier: method.nonObject + count: 1 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICDro might not be defined\.$#' - identifier: variable.undefined + message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICDropInfo might not be defined\.$#' - identifier: variable.undefined + message: '#^Default value of the parameter \#2 \$id \(int\) of method FOG\\Base\\FOGManagerController\:\:exists\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICErr might not be defined\.$#' - identifier: variable.undefined + message: '#^Default value of the parameter \#3 \$orderby \(string\) of method FOG\\Base\\FOGManagerController\:\:order\(\) is incompatible with type array\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICErrInfo might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGManagerController\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICMac might not be defined\.$#' - identifier: variable.undefined + message: '#^PHPDoc tag @param has invalid value \(\* \$val Value to bind\)\: Unexpected token "\*", expected type at offset 194 on line 6$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICRec might not be defined\.$#' - identifier: variable.undefined + message: '#^PHPDoc tag @return has invalid value \(\[\]\)\: Unexpected token "\[", expected type at offset 1316 on line 24$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICRecSized might not be defined\.$#' - identifier: variable.undefined + message: '#^PHPDoc tag @return has invalid value \(\[\]\)\: Unexpected token "\[", expected type at offset 63 on line 4$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICTrans might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: packages/web/src/Pages/ServerInfo.php + message: '#^Parameter \#1 \$db of static method FOG\\Base\\FOGManagerController\:\:sqlexec\(\) expects resource, object given\.$#' + identifier: argument.type + count: 3 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Variable \$NICTransSized might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter \#2 \$bindings of static method FOG\\Base\\FOGManagerController\:\:sqlexec\(\) expects array, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/ServerInfo.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Constructor of class FOG\\Pages\\ServiceConfigurationPage has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Parameter \#2 \$orderby of static method FOG\\Base\\FOGManagerController\:\:orderColumn\(\) expects string, array given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/ServiceConfigurationPage.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, int given\.$#' + message: '#^Parameter \#3 \$orderby of static method FOG\\Base\\FOGManagerController\:\:order\(\) expects array, string given\.$#' identifier: argument.type - count: 4 - path: packages/web/src/Pages/ServiceConfigurationPage.php - - - - message: '#^Variable \$Module might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: packages/web/src/Pages/ServiceConfigurationPage.php + count: 1 + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Constructor of class FOG\\Pages\\SiteManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Strict comparison using \!\=\= between null and mixed will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getGrantRolesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getGrantUserGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$dups might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$findKeys might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$insertID might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getUserGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$waszero might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/SiteManagement.php + path: packages/web/src/Base/FOGManagerController.php - - message: '#^Method FOG\\Pages\\SiteManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Pages/SiteManagement.php + message: '#^Access to an undefined property FOG\\Base\\FOGPage\:\:\$dataFind\.$#' + identifier: property.notFound + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void - count: 6 - path: packages/web/src/Pages/SiteManagement.php + message: '#^Access to an undefined property FOG\\Base\\FOGPage\:\:\$dataReplace\.$#' + identifier: property.notFound + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\SnapinManagement\:\:_maker\(\) with return type void returns string\|false but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Pages/SnapinManagement.php + message: '#^Call to an undefined method FOG\\Base\\FOGPage\:\:_addFields\(\)\.$#' + identifier: method.notFound + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\SnapinManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Call to an undefined static method FOG\\Base\\FOGPage\:\:getIds\(\)\.$#' + identifier: staticMethod.notFound count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\SnapinManagement\:\:getStoragegroupsList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Call to function unset\(\) contains undefined variable \$actionbox\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type + message: '#^Cannot unset offset \*NEVER\* on array\{\}\.$#' + identifier: unset.offset count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Comparison operation "\>" between 0 and 0 is always false\.$#' + identifier: greater.alwaysFalse count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Pages\\SnapinManagement\:\:_maker\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Comparison operation "\>" between int\<1, 5\> and 0 is always true\.$#' + identifier: greater.alwaysTrue count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Static property FOG\\Base\\FOGBase\:\:\$selected \(bool\|int\) does not accept string\.$#' - identifier: assign.propertyType - count: 3 - path: packages/web/src/Pages/SnapinManagement.php + message: '#^Default value of the parameter \#1 \$main \(string\) of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) is incompatible with type array\.$#' + identifier: parameter.defaultValue + count: 1 + path: packages/web/src/Base/FOGPage.php - - message: '#^Static property FOG\\Pages\\SnapinManagement\:\:\$_template2 \(string\) does not accept null\.$#' - identifier: assign.propertyType + message: '#^Default value of the parameter \#2 \$hookMain \(string\) of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) is incompatible with type array\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$storagegroups might not be defined\.$#' - identifier: variable.undefined + message: '#^Elseif condition is always false\.$#' + identifier: elseif.alwaysFalse count: 1 - path: packages/web/src/Pages/SnapinManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Constructor of class FOG\\Pages\\StorageGroupManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + message: '#^Empty array passed to foreach\.$#' + identifier: foreach.emptyArray + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getImagesList\(\) with return type void returns mixed but should not return anything\.$#' + message: '#^Method FOG\\Base\\FOGPage\:\:__construct\(\) with return type void returns mixed but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getSnapinsList\(\) with return type void returns mixed but should not return anything\.$#' + message: '#^Method FOG\\Base\\FOGPage\:\:assocItemsList\(\) with return type void returns mixed but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getStorageNodesList\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Base\\FOGPage\:\:authorize\(\) invoked with 1 parameter, 0 required\.$#' + identifier: arguments.count count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$StorageGroup might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) with return type void returns string\|false but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$storagenodes might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGPage\:\:unisearch\(\) should return string but return statement is missing\.$#' + identifier: return.missing count: 1 - path: packages/web/src/Pages/StorageGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Constructor of class FOG\\Pages\\StorageNodeManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^If condition is always false\.$#' - identifier: if.alwaysFalse + message: '#^Parameter \#1 \$dom of static method FOG\\Util\\FOGCron\:\:checkDOMField\(\) expects int, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\StorageNodeManagement\:\:storagenodeGeneralPost\(\) with return type void returns string but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#1 \$dow of static method FOG\\Util\\FOGCron\:\:checkDOWField\(\) expects int, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + message: '#^Parameter \#1 \$hours of static method FOG\\Util\\FOGCron\:\:checkHoursField\(\) expects int, string given\.$#' identifier: argument.type - count: 3 - path: packages/web/src/Pages/StorageNodeManagement.php + count: 1 + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Pages\\StorageNodeManagement\:\:storagenodeGeneralPost\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Parameter \#1 \$minutes of static method FOG\\Util\\FOGCron\:\:checkMinutesField\(\) expects int, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$StorageNode might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter \#1 \$month of static method FOG\\Util\\FOGCron\:\:checkMonthField\(\) expects int, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$warning might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: packages/web/src/Pages/StorageNodeManagement.php + message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(string\)\: bool\)\|null, ''strlen'' given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Constructor of class FOG\\Pages\\TaskManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter - count: 1 - path: packages/web/src/Pages/TaskManagement.php + message: '#^Parameter \#2 \$id of static method FOG\\Base\\FOGPage\:\:makeTabUpdateURL\(\) expects int, string given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Base/FOGPage.php - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Pages/TaskManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Variable \$columns might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter &\$hookMain by\-ref type of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + identifier: parameterByRef.type count: 1 - path: packages/web/src/Pages/TaskManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Constructor of class FOG\\Pages\\UserGroupManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Parameter &\$main by\-ref type of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + identifier: parameterByRef.type count: 1 - path: packages/web/src/Pages/UserGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getRolesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse count: 1 - path: packages/web/src/Pages/UserGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getSitesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/src/Pages/UserGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Strict comparison using \!\=\= between non\-empty\-string and null will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue count: 1 - path: packages/web/src/Pages/UserGroupManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void - count: 3 - path: packages/web/src/Pages/UserGroupManagement.php + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse + count: 4 + path: packages/web/src/Base/FOGPage.php - - message: '#^Constructor of class FOG\\Pages\\UserManagement has an unused parameter \$name\.$#' - identifier: constructor.unusedParameter + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue count: 1 - path: packages/web/src/Pages/UserManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserManagement\:\:__construct\(\) with return type void returns \$this\(FOG\\Pages\\UserManagement\) but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$storagegroups might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Pages/UserManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$sub in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Pages/UserManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Method FOG\\Pages\\UserManagement\:\:getRolesList\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$tabstr in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Pages/UserManagement.php + path: packages/web/src/Base/FOGPage.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' - identifier: method.void - count: 2 - path: packages/web/src/Pages/UserManagement.php + message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Variable \$User might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\FOGPageManager\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Pages/UserManagement.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Method FOG\\Base\\FOGPageManager\:\:_register\(\) with return type void returns \$this\(FOG\\Base\\FOGPageManager\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Reports/File_Deleter.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Reports/History_Report.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Parameter &\$value by\-ref type of method FOG\\Base\\FOGPageManager\:\:replaceVariable\(\) expects string, string\|null given\.$#' + identifier: parameterByRef.type count: 1 - path: packages/web/src/Reports/Hosts_And_Users.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse count: 1 - path: packages/web/lib/reports/inventory_report.report.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Variable \$class in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Reports/Product_Keys.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void + message: '#^Variable \$className in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Reports/Run_History.php + path: packages/web/src/Base/FOGPageManager.php - - message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' - identifier: method.void - count: 1 - path: packages/web/src/Reports/Snapin_List.php + message: '#^Access to an undefined property FOG\\Base\\Hook\:\:\$node\.$#' + identifier: property.notFound + count: 4 + path: packages/web/src/Base/Hook.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' identifier: function.alreadyNarrowedType count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/HookManager.php - - message: '#^Call to function unset\(\) contains undefined variable \$handler\.$#' - identifier: unset.variable + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/HookManager.php - - message: '#^Call to function unset\(\) contains undefined variable \$match\.$#' - identifier: unset.variable + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/HookManager.php - - message: '#^Instanceof between \*NEVER\* and Traversable will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/LoadGlobals.php - - message: '#^Method AltoRouter\:\:__call\(\) with return type void returns \$this\(AltoRouter\) but should not return anything\.$#' - identifier: return.void + message: '#^Access to an undefined property FOG\\Base\\Page\:\:\$imagelink\.$#' + identifier: property.notFound count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/Page.php - - message: '#^Method AltoRouter\:\:_compileRoute\(\) should return string but returns array\\|string\>\.$#' - identifier: return.type + message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/Page.php - - message: '#^Method AltoRouter\:\:getBasePath\(\) should return array but returns string\.$#' - identifier: return.type + message: '#^Path in include\(\) "management/other/index\.php" is not a file or it does not exist\.$#' + identifier: include.fileNotFound count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/Page.php - - message: '#^Offset ''regex'' does not exist on string\.$#' - identifier: offsetAccess.notFound + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/Page.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/lib/router/altorouter.class.php + path: packages/web/src/Base/StorageEpoch.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse + message: '#^If condition is always false\.$#' + identifier: if.alwaysFalse count: 1 - path: packages/web/management/index.php + path: packages/web/src/Base/System.php - - message: '#^Strict comparison using \!\=\= between '''' and '''' will always evaluate to false\.$#' - identifier: notIdentical.alwaysFalse + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/management/index.php + path: packages/web/src/Base/System.php - - message: '#^Variable \$HookManager might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Base\\System\:\:_versionCompare\(\) returns void but does not have any side effects\.$#' + identifier: void.pure count: 1 - path: packages/web/management/index.php - - - - message: '#^Variable \$currentUser might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: packages/web/management/index.php + path: packages/web/src/Base/System.php - - message: '#^Variable \$foglang might not be defined\.$#' - identifier: variable.undefined - count: 2 - path: packages/web/management/index.php + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue + count: 1 + path: packages/web/src/Base/System.php - - message: '#^Accessing self\:\:\$FOGUser outside of class scope\.$#' - identifier: outOfClass.self - count: 8 - path: packages/web/management/other/index.php + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 1 + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Accessing self\:\:\$HookManager outside of class scope\.$#' - identifier: outOfClass.self + message: '#^Negated boolean expression is always true\.$#' + identifier: booleanNot.alwaysTrue count: 2 - path: packages/web/management/other/index.php + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Accessing self\:\:\$pluginIsAvailable outside of class scope\.$#' - identifier: outOfClass.self - count: 2 - path: packages/web/management/other/index.php + message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' + identifier: argument.type + count: 3 + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Calling self\:\:formatByteSize\(\) outside of class scope\.$#' - identifier: outOfClass.self + message: '#^Result of \|\| is always true\.$#' + identifier: booleanOr.alwaysTrue count: 2 - path: packages/web/management/other/index.php + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Calling self\:\:formatTime\(\) outside of class scope\.$#' - identifier: outOfClass.self - count: 1 - path: packages/web/management/other/index.php - - - - message: '#^Calling self\:\:displayTheme\(\) outside of class scope\.$#' - identifier: outOfClass.self + message: '#^Variable \$chkdsk in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/management/other/index.php + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Calling self\:\:getClass\(\) outside of class scope\.$#' - identifier: outOfClass.self - count: 1 - path: packages/web/management/other/index.php + message: '#^Variable \$ip might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: packages/web/src/Boot/BootMenuBase.php - - message: '#^Calling self\:\:getMessage\(\) outside of class scope\.$#' - identifier: outOfClass.self + message: '#^Call to function is_numeric\(\) with int\<0, max\> will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/management/other/index.php - - - - message: '#^Calling self\:\:getSetting\(\) outside of class scope\.$#' - identifier: outOfClass.self - count: 7 - path: packages/web/management/other/index.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue - count: 1 - path: packages/web/management/other/index.php + message: '#^Cannot call method get\(\) on string\.$#' + identifier: method.nonObject + count: 2 + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Variable \$this might not be defined\.$#' - identifier: variable.undefined - count: 11 - path: packages/web/management/other/index.php + message: '#^Cannot call method isValid\(\) on string\.$#' + identifier: method.nonObject + count: 2 + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Call to function is_array\(\) with null will always evaluate to false\.$#' - identifier: function.impossibleType - count: 1 - path: packages/web/src/Auth/Authorization.php + message: '#^Instanceof between FOG\\Items\\Host and FOG\\Items\\Host will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 2 + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Call to function is_string\(\) with null will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Instanceof between FOG\\Items\\Image and FOG\\Items\\Image will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Empty array passed to foreach\.$#' - identifier: foreach.emptyArray - count: 1 - path: packages/web/src/Auth/Authorization.php + message: '#^Parameter \#1 \$TaskType of method FOG\\Items\\Host\:\:createImagePackage\(\) expects int, object given\.$#' + identifier: argument.type + count: 3 + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue + message: '#^Parameter \#1 \$key of static method FOG\\Base\\FOGBase\:\:arrayInsertAfter\(\) expects string, int\<0, max\> given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue - count: 5 - path: packages/web/src/Auth/Authorization.php + message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' + identifier: argument.type + count: 4 + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Method FOG\\Auth\\Authorization\:\:_pluginScopeIDs\(\) never returns array so it can be removed from the return type\.$#' - identifier: return.unusedType + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Method FOG\\Auth\\Authorization\:\:_pluginScopeWhere\(\) never returns string so it can be removed from the return type\.$#' - identifier: return.unusedType + message: '#^Property FOG\\Boot\\IpxeBootMenu\:\:\$_path is never written, only read\.$#' + identifier: property.onlyRead count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Property FOG\\Boot\\IpxeBootMenu\:\:\$_shutdown is never written, only read\.$#' + identifier: property.onlyRead count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/IpxeBootMenu.php - - message: '#^PHPDoc tag @throws with type FOG\\Auth\\Exception is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Boot\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable - count: 4 - path: packages/web/src/Auth/Authorization.php - - - - message: '#^Parameter \#1 \$input of function array_values contains unresolvable type\.$#' - identifier: argument.unresolvableType count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/Registration.php - - message: '#^Parameter \#2 \$msg of static method FOG\\Router\\Route\:\:sendResponse\(\) expects int, string given\.$#' + message: '#^Parameter \#1 \$TaskType of method FOG\\Items\\Host\:\:createImagePackage\(\) expects int, object given\.$#' identifier: argument.type - count: 2 - path: packages/web/src/Auth/Authorization.php - - - - message: '#^Strict comparison using \=\=\= between null and string will always evaluate to false\.$#' - identifier: identical.alwaysFalse count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/Registration.php - - message: '#^Ternary operator condition is always false\.$#' - identifier: ternary.alwaysFalse + message: '#^Parameter \#1 \$input of function str_pad expects string, \(float\|int\) given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/Registration.php - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable + message: '#^Parameter \#1 \$macs of method FOG\\Managers\\HostManager\:\:getHostByMacAddresses\(\) expects array, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Auth/Authorization.php + path: packages/web/src/Boot/Registration.php - - message: '#^Argument of an invalid type string supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable + message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, \(float\|int\) given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/Registration.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: packages/web/src/Base/EventManager.php + message: '#^Parameter \#3 \$pad_string of function str_pad expects string, int given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Boot/Registration.php - - message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Variable \$ADDomain might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/Registration.php - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 2 - path: packages/web/src/Base/EventManager.php + message: '#^Variable \$ADOU might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Boot/Registration.php - - message: '#^Cannot use array destructuring on string\.$#' - identifier: offsetAccess.nonArray + message: '#^Variable \$ADPass might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/Registration.php - - message: '#^Method FOG\\Base\\EventManager\:\:register\(\) should return bool but returns \$this\(FOG\\Base\\EventManager\)\.$#' - identifier: return.type + message: '#^Variable \$ADUser might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/Registration.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 3 - path: packages/web/src/Base/EventManager.php + message: '#^Variable \$enforce might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Boot/Registration.php - - message: '#^PHPDoc tag @var above a method has no effect\.$#' - identifier: varTag.misplaced + message: '#^Variable \$useAD might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/Registration.php - - message: '#^Parameter \#1 \$array1 of static method FOG\\Base\\FOGBase\:\:fastmerge\(\) expects array, string given\.$#' - identifier: argument.type + message: '#^Comparison operation "\<" between int\<0, max\> and 0 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/WakeOnLan.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type + message: '#^Method FOG\\Boot\\WakeOnLan\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/WakeOnLan.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse count: 1 - path: packages/web/src/Base/EventManager.php + path: packages/web/src/Boot/WakeOnLan.php - - message: '#^Access to an undefined property FOG\\Base\\FOGBase\:\:\$databaseFields\.$#' - identifier: property.notFound - count: 3 - path: packages/web/src/Base/FOGBase.php + message: '#^Strict comparison using \=\=\= between array and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse + count: 1 + path: packages/web/src/Boot/WakeOnLan.php - - message: '#^Call to an undefined method FOG\\Base\\FOGBase\:\:key\(\)\.$#' + message: '#^Call to an undefined method FOG\\Client\\FOGClient\:\:json\(\)\.$#' identifier: method.notFound - count: 3 - path: packages/web/src/Base/FOGBase.php + count: 4 + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 3 - path: packages/web/src/Base/FOGBase.php + message: '#^Instanceof between FOG\\Items\\Host and FOG\\Items\\Host will always evaluate to true\.$#' + identifier: instanceof.alwaysTrue + count: 1 + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_array\(\) with list will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns int but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_array\(\) with non\-falsy\-string will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns string but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_bool\(\) with bool will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns string\|false but should not return anything\.$#' + identifier: return.void count: 2 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_int\(\) with int will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_numeric\(\) with \*NEVER\* will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/FOGClient.php - - message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/RegisterClient.php - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 8 - path: packages/web/src/Base/FOGBase.php + message: '#^Parameter \#1 \$mac of method FOG\\Items\\Host\:\:addPendMAC\(\) expects array\\|string, array\ given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Client/RegisterClient.php - - message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/ServiceModule.php - - message: '#^Comparison operation "\>\=" between ''1''\|''2''\|''3''\|''4''\|''5''\|''6''\|''7'' and 0 is always true\.$#' - identifier: greaterOrEqual.alwaysTrue - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^Instanceof between null and FOG\\Items\\StorageGroup will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse + count: 2 + path: packages/web/src/Client/SnapinClient.php - - message: '#^Default value of the parameter \#2 \$key \(false\) of method FOG\\Base\\FOGBase\:\:aesdecrypt\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^Instanceof between null and FOG\\Items\\StorageNode will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse + count: 2 + path: packages/web/src/Client/SnapinClient.php - - message: '#^Default value of the parameter \#2 \$key \(false\) of method FOG\\Base\\FOGBase\:\:aesencrypt\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Method FOG\\Client\\SnapinClient\:\:json\(\) should return array\\|void but returns array\\>\>\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/SnapinClient.php - - message: '#^Default value of the parameter \#3 \$enctype \(string\) of method FOG\\Base\\FOGBase\:\:aesdecrypt\(\) is incompatible with type int\.$#' - identifier: parameter.defaultValue - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 3 + path: packages/web/src/Client/SnapinClient.php - - message: '#^Default value of the parameter \#3 \$enctype \(string\) of method FOG\\Base\\FOGBase\:\:aesencrypt\(\) is incompatible with type int\.$#' - identifier: parameter.defaultValue + message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/SnapinClient.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 2 - path: packages/web/src/Base/FOGBase.php + message: '#^Parameter \#1 \$str of function urlencode expects string, object given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Client/SnapinClient.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/SnapinClient.php - - message: '#^Method FOG\\Base\\FOGBase\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Parameter \#4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/SnapinClient.php - - message: '#^Method FOG\\Base\\FOGBase\:\:__construct\(\) with return type void returns \$this\(FOG\\Base\\FOGBase\) but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 4 + path: packages/web/src/Client/SnapinClient.php - - message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) has invalid return type FOG\\Base\\key\.$#' - identifier: class.notFound + message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Client/UserTrack.php - - message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns \(int\|string\)\.$#' - identifier: return.type + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns int\.$#' - identifier: return.type - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 2 + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:arrayFind\(\) should return FOG\\Base\\key but returns int\|string\.$#' - identifier: return.type + message: '#^Result of \|\| is always false\.$#' + identifier: booleanOr.alwaysFalse count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:cryptoRandSecure\(\) should return string but returns \(float\|int\)\.$#' - identifier: return.type + message: '#^Static method FOG\\Db\\DatabaseManager\:\:_convertEngine\(\) is unused\.$#' + identifier: method.unused count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:fileitems\(\) should return string but returns array\\.$#' - identifier: return.type + message: '#^Static method FOG\\Db\\DatabaseManager\:\:_getVersion\(\) is unused\.$#' + identifier: method.unused count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:fileitems\(\) should return string but returns array\, array\\>\.$#' - identifier: return.type + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:fileitems\(\) should return string but returns list\\.$#' - identifier: return.type + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/DatabaseManager.php - - message: '#^Method FOG\\Base\\FOGBase\:\:formatByteSize\(\) should return float but returns string\.$#' - identifier: return.type + message: '#^Constant DATABASE_PASSWORD not found\.$#' + identifier: constant.notFound count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/Mysqldump.php - - message: '#^Method FOG\\Base\\FOGBase\:\:getHostItem\(\) should return array\|object but empty return statement found\.$#' - identifier: return.empty + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but empty return statement found\.$#' - identifier: return.empty + message: '#^Call to function is_bool\(\) with object will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but returns array\.$#' - identifier: return.type + message: '#^Cannot call method exec\(\) on resource\.$#' + identifier: method.nonObject count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:getMasterInterface\(\) should return string but returns false\.$#' - identifier: return.type + message: '#^Cannot call method lastInsertId\(\) on resource\.$#' + identifier: method.nonObject count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:sendData\(\) should return string but empty return statement found\.$#' - identifier: return.empty + message: '#^Cannot call method prepare\(\) on resource\.$#' + identifier: method.nonObject count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:sendData\(\) should return string but returns array\\.$#' - identifier: return.type - count: 1 - path: packages/web/src/Base/FOGBase.php + message: '#^Cannot call method query\(\) on resource\.$#' + identifier: method.nonObject + count: 3 + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:setSetting\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Cannot call method quote\(\) on resource\.$#' + identifier: method.nonObject count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:validDate\(\) should return object but returns bool\.$#' - identifier: return.type + message: '#^Dead catch \- PDOException is never thrown in the try block\.$#' + identifier: catch.neverThrown count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:validDate\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue + count: 3 + path: packages/web/src/Db/PDODB.php + + - + message: '#^Method FOG\\Db\\PDODB\:\:__construct\(\) with return type void returns \$this\(FOG\\Db\\PDODB\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:validDate\(\) should return object but returns true\.$#' - identifier: return.type + message: '#^Method FOG\\Db\\PDODB\:\:_connect\(\) has FOG\\Db\\PDOException in PHPDoc @throws tag but it''s not thrown\.$#' + identifier: throws.unusedType count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Method FOG\\Base\\FOGBase\:\:var_dump_log\(\) should return string\|null but return statement is missing\.$#' - identifier: return.missing + message: '#^Method FOG\\Db\\PDODB\:\:link\(\) should return object but returns resource\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - message: '#^Negated boolean expression is always false\.$#' identifier: booleanNot.alwaysFalse - count: 6 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Offset string does not exist on ''ABCDEFGHIJKLMNOPQRS…''\.$#' - identifier: offsetAccess.notFound - count: 1 - path: packages/web/src/Base/FOGBase.php + count: 7 + path: packages/web/src/Db/PDODB.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Db\\PDOException is not subtype of Throwable$#' identifier: throws.notThrowable - count: 14 - path: packages/web/src/Base/FOGBase.php + count: 6 + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$array \(array\, non\-falsy\-string\>\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' - identifier: arrayFilter.same + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$array \(array\{''autologout'', ''displaymanager'', ''hostnamechanger'', ''hostregister'', ''powermanagement'', ''printermanager'', ''snapinclient'', ''taskreboot'', \.\.\.\}\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' - identifier: arrayFilter.same + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$array_arg of function natcasesort expects an array of values castable to string, list\\> given\.$#' - identifier: argument.type + message: '#^Static method FOG\\Db\\PDODB\:\:_boundReadTimeout\(\) is unused\.$#' + identifier: method.unused count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$haystack of callable ''stripos''\|''strpos'' expects string, array given\.$#' - identifier: argument.type + message: '#^Static method FOG\\Db\\PDODB\:\:_pinSessionZone\(\) is unused\.$#' + identifier: method.unused count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$method of function openssl_cipher_iv_length expects string, int given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_dbName \(string\) does not accept false\.$#' + identifier: assign.propertyType count: 2 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$string of function strlen expects string, float\|int given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_dbName \(string\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#1 \$timezone of class DateTimeZone constructor expects string, object given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) does not accept false\.$#' + identifier: assign.propertyType count: 2 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Parameter \#2 \$id of static method FOG\\Router\\Route\:\:delete\(\) expects int, string\|false given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Parameter \#2 \$method of function openssl_decrypt expects string, int given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#2 \$method of function openssl_encrypt expects string, int given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) does not accept null\.$#' + identifier: assign.propertyType count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#2 \$start of function substr expects int, float given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Parameter \#3 \$length of function substr expects int, float given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Db\\PDODB\:\:\$_options is never read, only written\.$#' + identifier: property.onlyWritten count: 1 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse - count: 2 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse - count: 2 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Static property FOG\\Base\\FOGBase\:\:\$TimeZone \(object\) does not accept string\.$#' + message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) does not accept null\.$#' identifier: assign.propertyType - count: 1 - path: packages/web/src/Base/FOGBase.php + count: 3 + path: packages/web/src/Db/PDODB.php - - message: '#^Static property FOG\\Base\\FOGBase\:\:\$TimeZone \(object\) in empty\(\) is not falsy\.$#' + message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) in empty\(\) is not falsy\.$#' identifier: empty.property - count: 3 - path: packages/web/src/Base/FOGBase.php + count: 1 + path: packages/web/src/Db/PDODB.php - - message: '#^Static property FOG\\Base\\FOGBase\:\:\$httpproto \(string\) does not accept default value of type false\.$#' - identifier: property.defaultValue + message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 1 - path: packages/web/src/Base/FOGBase.php - - - - message: '#^Variable \$mac might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: packages/web/src/Base/FOGBase.php + count: 3 + path: packages/web/src/Db/PDODB.php - - message: '#^Variable \$sesVars in isset\(\) always exists and is not nullable\.$#' + message: '#^Variable \$data in isset\(\) always exists and is not nullable\.$#' identifier: isset.variable count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Variable \$token might not be defined\.$#' + message: '#^Variable \$errInfo might not be defined\.$#' identifier: variable.undefined count: 1 - path: packages/web/src/Base/FOGBase.php + path: packages/web/src/Db/PDODB.php - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 2 - path: packages/web/src/Base/FOGController.php + message: '#^Path in include\(\) "/commons/schema\-constraints\.php" is not a file or it does not exist\.$#' + identifier: include.fileNotFound + count: 1 + path: packages/web/src/Db/SchemaReconciler.php - - message: '#^Method FOG\\Base\\FOGController\:\:__construct\(\) with return type void returns \$this\(FOG\\Base\\FOGController\) but should not return anything\.$#' - identifier: return.void + message: '#^Path in include\(\) "/commons/schema\-expected\.php" is not a file or it does not exist\.$#' + identifier: include.fileNotFound count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Db/SchemaReconciler.php - - message: '#^Method FOG\\Base\\FOGController\:\:__destruct\(\) with return type void returns false but should not return anything\.$#' - identifier: return.void + message: '#^PHPDoc type string of property FOG\\Events\\HostList\:\:\$active is not covariant with PHPDoc type bool of overridden property FOG\\Base\\Event\:\:\$active\.$#' + identifier: property.phpDocType count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Events/HostList.php - - message: '#^Method FOG\\Base\\FOGController\:\:destroy\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Property FOG\\Events\\HostList\:\:\$active \(string\) does not accept default value of type false\.$#' + identifier: property.defaultValue count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Events/HostList.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 9 - path: packages/web/src/Base/FOGController.php + message: '#^PHPDoc tag @var has invalid value \(\$name\)\: Unexpected token "\$name", expected type at offset 53 on line 4$#' + identifier: phpDoc.parseError + count: 1 + path: packages/web/src/Hooks/BootItem.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + message: '#^Parameter \#1 \$txt of static method FOG\\Base\\Hook\:\:log\(\) expects string, true given\.$#' identifier: argument.type - count: 4 - path: packages/web/src/Base/FOGController.php + count: 1 + path: packages/web/src/Hooks/HookDebugger.php - - message: '#^Parameter \#3 \$c of method FOG\\Base\\FOGController\:\:buildQuery\(\) expects array, null given\.$#' + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' identifier: argument.type - count: 2 - path: packages/web/src/Base/FOGController.php - - - - message: '#^Property FOG\\Base\\FOGController\:\:\$databaseTable \(string\) in isset\(\) is not nullable\.$#' - identifier: isset.property count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Hooks/HookDebugger.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue + message: '#^Parameter \#3 \$logfile of static method FOG\\Base\\Hook\:\:log\(\) expects int, bool given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Hooks/HookDebugger.php - - message: '#^Variable \$columns might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter \#4 \$logbrow of static method FOG\\Base\\Hook\:\:log\(\) expects int, bool given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGController.php + path: packages/web/src/Hooks/HookDebugger.php - - message: '#^Variable \$idField might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter \#1 \$txt of static method FOG\\Base\\Hook\:\:log\(\) expects string, true given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGController.php - - - - message: '#^Binary operation "\+" between string and string results in an error\.$#' - identifier: binaryOp.invalid - count: 2 - path: packages/web/src/Base/FOGCore.php + path: packages/web/src/Hooks/Template.php - - message: '#^Method FOG\\Base\\FOGCore\:\:setEnv\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGCore.php + path: packages/web/src/Hooks/Template.php - - message: '#^Parameter \#1 \$size of static method FOG\\Base\\FOGBase\:\:formatByteSize\(\) expects float\|int, string\|false\|null given\.$#' + message: '#^Parameter \#3 \$logfile of static method FOG\\Base\\Hook\:\:log\(\) expects int, false given\.$#' identifier: argument.type - count: 3 - path: packages/web/src/Base/FOGCore.php + count: 1 + path: packages/web/src/Hooks/Template.php - - message: '#^Parameter \#2 \$newvalue of function ini_set expects string, int given\.$#' + message: '#^Parameter \#4 \$logbrow of static method FOG\\Base\\Hook\:\:log\(\) expects int, true given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Base/FOGCore.php + path: packages/web/src/Hooks/Template.php - - message: '#^Variable \$loadAvg might not be defined\.$#' - identifier: variable.undefined + message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' + identifier: binaryOp.invalid count: 1 - path: packages/web/src/Base/FOGCore.php - - - - message: '#^Access to an undefined property FOG\\Base\\FOGManagerController\:\:\$sqlTotalStr\.$#' - identifier: property.notFound - count: 2 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Access to an undefined property FOG\\Base\\FOGManagerController\:\:\$tablename\.$#' - identifier: property.notFound - count: 2 - path: packages/web/src/Base/FOGManagerController.php + message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: packages/web/src/Items/Group.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Call to function unset\(\) contains undefined variable \$affected_rows\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Call to function unset\(\) contains undefined variable \$findKeys\.$#' + message: '#^Call to function unset\(\) contains undefined variable \$first_id\.$#' identifier: unset.variable count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Cannot call method prepare\(\) on resource\.$#' - identifier: method.nonObject + message: '#^Call to function unset\(\) contains undefined variable \$ids\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Comparison operation "\<" between int\<1, max\> and 1 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^Call to function unset\(\) contains undefined variable \$multicastsessionassocs\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Default value of the parameter \#2 \$id \(int\) of method FOG\\Base\\FOGManagerController\:\:exists\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Comparison operation "\>" between int\<4, 5\> and 0 is always true\.$#' + identifier: greater.alwaysTrue count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Default value of the parameter \#3 \$orderby \(string\) of method FOG\\Base\\FOGManagerController\:\:order\(\) is incompatible with type array\.$#' - identifier: parameter.defaultValue + message: '#^Method FOG\\Items\\Group\:\:_createSnapinTasking\(\) should return array but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Method FOG\\Base\\FOGManagerController\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Method FOG\\Items\\Group\:\:destroy\(\) should return bool but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^PHPDoc tag @param has invalid value \(\* \$val Value to bind\)\: Unexpected token "\*", expected type at offset 194 on line 6$#' - identifier: phpDoc.parseError + message: '#^PHPDoc tag @param references unknown parameter\: \$useAD$#' + identifier: parameter.notFound count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^PHPDoc tag @return has invalid value \(\[\]\)\: Unexpected token "\[", expected type at offset 1316 on line 24$#' - identifier: phpDoc.parseError + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^PHPDoc tag @return has invalid value \(\[\]\)\: Unexpected token "\[", expected type at offset 63 on line 4$#' - identifier: phpDoc.parseError + message: '#^Return type \(bool\) of method FOG\\Items\\Group\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' + identifier: method.childReturnType count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Parameter \#1 \$db of static method FOG\\Base\\FOGManagerController\:\:sqlexec\(\) expects resource, object given\.$#' - identifier: argument.type - count: 3 - path: packages/web/src/Base/FOGManagerController.php - - - - message: '#^Parameter \#2 \$bindings of static method FOG\\Base\\FOGManagerController\:\:sqlexec\(\) expects array, string given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Base/FOGManagerController.php + message: '#^Strict comparison using \!\=\= between string and null will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 4 + path: packages/web/src/Items/Group.php - - message: '#^Parameter \#2 \$orderby of static method FOG\\Base\\FOGManagerController\:\:orderColumn\(\) expects string, array given\.$#' - identifier: argument.type + message: '#^Strict comparison using \=\=\= between int and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Group.php - - message: '#^Parameter \#3 \$orderby of static method FOG\\Base\\FOGManagerController\:\:order\(\) expects array, string given\.$#' - identifier: argument.type + message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' + identifier: binaryOp.invalid count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Host.php - - message: '#^Strict comparison using \!\=\= between null and mixed will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: packages/web/src/Base/FOGManagerController.php + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 2 + path: packages/web/src/Items/Host.php - - message: '#^Ternary operator condition is always true\.$#' - identifier: ternary.alwaysTrue - count: 1 - path: packages/web/src/Base/FOGManagerController.php + message: '#^Cannot access property \$id on int\.$#' + identifier: property.nonObject + count: 6 + path: packages/web/src/Items/Host.php - - message: '#^Variable \$dups might not be defined\.$#' - identifier: variable.undefined + message: '#^Cannot access property \$isCapture on int\.$#' + identifier: property.nonObject count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Host.php - - message: '#^Variable \$findKeys might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: packages/web/src/Base/FOGManagerController.php + message: '#^Cannot access property \$isImagingTask on int\.$#' + identifier: property.nonObject + count: 3 + path: packages/web/src/Items/Host.php - - message: '#^Variable \$insertID might not be defined\.$#' - identifier: variable.undefined + message: '#^Cannot access property \$isMulticast on int\.$#' + identifier: property.nonObject count: 1 - path: packages/web/src/Base/FOGManagerController.php + path: packages/web/src/Items/Host.php - - message: '#^Variable \$waszero might not be defined\.$#' - identifier: variable.undefined + message: '#^Cannot access property \$isSnapinTask on int\.$#' + identifier: property.nonObject count: 1 - path: packages/web/src/Base/FOGManagerController.php - - - - message: '#^Access to an undefined property FOG\\Base\\FOGPage\:\:\$dataFind\.$#' - identifier: property.notFound - count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Access to an undefined property FOG\\Base\\FOGPage\:\:\$dataReplace\.$#' - identifier: property.notFound + message: '#^Cannot access property \$isSnapinTasking on int\.$#' + identifier: property.nonObject count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Call to an undefined method FOG\\Base\\FOGPage\:\:_addFields\(\)\.$#' - identifier: method.notFound + message: '#^Comparison operation "\>" between int\<1, max\> and 0 is always true\.$#' + identifier: greater.alwaysTrue count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Call to function unset\(\) contains undefined variable \$actionbox\.$#' - identifier: unset.variable + message: '#^Default value of the parameter \#1 \$mac \(false\) of method FOG\\Items\\Host\:\:clientMacCheck\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Cannot unset offset \*NEVER\* on array\{\}\.$#' - identifier: unset.offset + message: '#^Default value of the parameter \#1 \$mac \(false\) of method FOG\\Items\\Host\:\:imageMacCheck\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Comparison operation "\>" between 0 and 0 is always false\.$#' - identifier: greater.alwaysFalse + message: '#^Default value of the parameter \#4 \$Task \(false\) of method FOG\\Items\\Host\:\:_createSnapinTasking\(\) is incompatible with type object\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Comparison operation "\>" between int\<1, 5\> and 0 is always true\.$#' - identifier: greater.alwaysTrue + message: '#^Default value of the parameter \#8 \$passreset \(false\) of method FOG\\Items\\Host\:\:_createTasking\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Default value of the parameter \#1 \$main \(string\) of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) is incompatible with type array\.$#' - identifier: parameter.defaultValue + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Default value of the parameter \#2 \$hookMain \(string\) of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) is incompatible with type array\.$#' - identifier: parameter.defaultValue + message: '#^Instanceof between null and FOG\\Items\\StorageGroup will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Elseif condition is always false\.$#' - identifier: elseif.alwaysFalse - count: 1 - path: packages/web/src/Base/FOGPage.php + message: '#^Instanceof between null and FOG\\Items\\StorageNode will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse + count: 2 + path: packages/web/src/Items/Host.php - - message: '#^Empty array passed to foreach\.$#' - identifier: foreach.emptyArray + message: '#^Instanceof between string and FOG\\Items\\MACAddress will always evaluate to false\.$#' + identifier: instanceof.alwaysFalse count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Method FOG\\Base\\FOGPage\:\:__construct\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Left side of && is always false\.$#' + identifier: booleanAnd.leftAlwaysFalse count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Method FOG\\Base\\FOGPage\:\:assocItemsList\(\) with return type void returns mixed but should not return anything\.$#' + message: '#^Method FOG\\Items\\Host\:\:_createSnapinTasking\(\) with return type void returns \$this\(FOG\\Items\\Host\) but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Method FOG\\Base\\FOGPage\:\:authorize\(\) invoked with 1 parameter, 0 required\.$#' - identifier: arguments.count + message: '#^Method FOG\\Items\\Host\:\:addPendMAC\(\) has invalid return type FOG\\Items\\obect\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) with return type void returns string\|false but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Items\\Host\:\:addPendMAC\(\) should return FOG\\Items\\obect but returns \$this\(FOG\\Items\\Host\)\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Method FOG\\Base\\FOGPage\:\:unisearch\(\) should return string but return statement is missing\.$#' - identifier: return.missing + message: '#^Method FOG\\Items\\Host\:\:createImagePackage\(\) should return string but returns true\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Method FOG\\Items\\Host\:\:ignore\(\) should return object but return statement is missing\.$#' + identifier: return.missing + count: 2 + path: packages/web/src/Items/Host.php + + - + message: '#^Negated boolean expression is always true\.$#' + identifier: booleanNot.alwaysTrue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable + count: 3 + path: packages/web/src/Items/Host.php + + - + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Parameter \#1 \$dom of static method FOG\\Util\\FOGCron\:\:checkDOMField\(\) expects int, string given\.$#' - identifier: argument.type + message: '#^Result of \|\| is always true\.$#' + identifier: booleanOr.alwaysTrue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Parameter \#1 \$dow of static method FOG\\Util\\FOGCron\:\:checkDOWField\(\) expects int, string given\.$#' - identifier: argument.type + message: '#^Static property FOG\\Items\\Host\:\:\$_hostalo \(int\) does not accept default value of type array\.$#' + identifier: property.defaultValue count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Parameter \#1 \$hours of static method FOG\\Util\\FOGCron\:\:checkHoursField\(\) expects int, string given\.$#' - identifier: argument.type + message: '#^Variable \$StorageGroup might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Host.php - - message: '#^Parameter \#1 \$minutes of static method FOG\\Util\\FOGCron\:\:checkMinutesField\(\) expects int, string given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Base/FOGPage.php + message: '#^Variable \$StorageNode might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: packages/web/src/Items/Host.php - - message: '#^Parameter \#1 \$month of static method FOG\\Util\\FOGCron\:\:checkMonthField\(\) expects int, string given\.$#' - identifier: argument.type + message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Image.php - - message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(string\)\: bool\)\|null, ''strlen'' given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Base/FOGPage.php + message: '#^Method FOG\\Items\\Image\:\:setPrimaryGroup\(\) should return array but return statement is missing\.$#' + identifier: return.missing + count: 1 + path: packages/web/src/Items/Image.php - - message: '#^Parameter \#2 \$id of static method FOG\\Base\\FOGPage\:\:makeTabUpdateURL\(\) expects int, string given\.$#' - identifier: argument.type + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 2 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Image.php - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' identifier: argument.type - count: 1 - path: packages/web/src/Base/FOGPage.php + count: 3 + path: packages/web/src/Items/Image.php - - message: '#^Parameter &\$hookMain by\-ref type of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' - identifier: parameterByRef.type + message: '#^Variable \$DBIDs might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Image.php - - message: '#^Parameter &\$main by\-ref type of method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' - identifier: parameterByRef.type + message: '#^Method FOG\\Items\\MACAddress\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/MACAddress.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse - count: 1 - path: packages/web/src/Base/FOGPage.php + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 2 + path: packages/web/src/Items/MACAddress.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue - count: 1 - path: packages/web/src/Base/FOGPage.php - - - - message: '#^Strict comparison using \!\=\= between non\-empty\-string and null will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue + message: '#^Parameter \#4 \$optval of function socket_set_option expects array\|int\|string, true given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGPage.php - - - - message: '#^Ternary operator condition is always false\.$#' - identifier: ternary.alwaysFalse - count: 4 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/MACAddress.php - - message: '#^Ternary operator condition is always true\.$#' - identifier: ternary.alwaysTrue + message: '#^Property FOG\\Items\\MACAddress\:\:\$_Host is never read, only written\.$#' + identifier: property.onlyWritten count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/MACAddress.php - - message: '#^Variable \$storagegroups might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Items\\Module\:\:save\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/Module.php - - message: '#^Variable \$sub in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Method FOG\\Items\\MulticastSession\:\:cancel\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/MulticastSession.php - - message: '#^Variable \$tabstr in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Method FOG\\Items\\MulticastSession\:\:complete\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/FOGPage.php + path: packages/web/src/Items/MulticastSession.php - - message: '#^Argument of an invalid type string supplied for foreach, only iterables are supported\.$#' - identifier: foreach.nonIterable - count: 1 - path: packages/web/src/Base/FOGPageManager.php + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 2 + path: packages/web/src/Items/MulticastSession.php - - message: '#^Call to function is_object\(\) with object will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Offset ''requires'' on array\{\} on left side of \?\? does not exist\.$#' + identifier: nullCoalesce.offset count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Method FOG\\Base\\FOGPageManager\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Offset mixed on array\{\} on left side of \?\? does not exist\.$#' + identifier: nullCoalesce.offset count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Method FOG\\Base\\FOGPageManager\:\:_register\(\) with return type void returns \$this\(FOG\\Base\\FOGPageManager\) but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#1 \$array \(array\{\}\) to function array_filter is empty, call has no effect\.$#' + identifier: arrayFilter.empty count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Parameter \#1 \$array \(array\{\}\) to function array_values is empty, call has no effect\.$#' + identifier: arrayValues.empty count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Parameter &\$value by\-ref type of method FOG\\Base\\FOGPageManager\:\:replaceVariable\(\) expects string, string\|null given\.$#' - identifier: parameterByRef.type + message: '#^Parameter \#3 \$alias of class PharData constructor expects string, null given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse + message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#' + identifier: notIdentical.alwaysFalse count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Plugin.php - - message: '#^Variable \$class in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Method FOG\\Items\\PowerManagement\:\:save\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/PowerManagement.php - - message: '#^Variable \$className in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Method FOG\\Items\\Printer\:\:destroy\(\) should return bool but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/FOGPageManager.php + path: packages/web/src/Items/Printer.php - - message: '#^Access to an undefined property FOG\\Base\\Hook\:\:\$node\.$#' - identifier: property.notFound - count: 4 - path: packages/web/src/Base/Hook.php + message: '#^Return type \(bool\) of method FOG\\Items\\Printer\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' + identifier: method.childReturnType + count: 1 + path: packages/web/src/Items/Printer.php - - message: '#^Call to function is_string\(\) with string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Method FOG\\Items\\Role\:\:destroy\(\) should return bool but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/HookManager.php + path: packages/web/src/Items/Role.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Method FOG\\Items\\Role\:\:save\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/HookManager.php + path: packages/web/src/Items/Role.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type + message: '#^Return type \(bool\) of method FOG\\Items\\Role\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' + identifier: method.childReturnType count: 1 - path: packages/web/src/Base/HookManager.php + path: packages/web/src/Items/Role.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse count: 1 - path: packages/web/src/Base/LoadGlobals.php + path: packages/web/src/Items/Role.php - - message: '#^Access to an undefined property FOG\\Base\\Page\:\:\$imagelink\.$#' - identifier: property.notFound + message: '#^Method FOG\\Items\\ScheduledTask\:\:cancel\(\) should return bool but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/Page.php + path: packages/web/src/Items/ScheduledTask.php - - message: '#^PHPDoc tag @throws with type FOG\\Base\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^PHPDoc tag @return has invalid value \(object\.\)\: Unexpected token "\.", expected TOKEN_HORIZONTAL_WS at offset 81 on line 4$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Base/Page.php + path: packages/web/src/Items/ScheduledTask.php - - message: '#^Path in include\(\) "management/other/index\.php" is not a file or it does not exist\.$#' - identifier: include.fileNotFound + message: '#^Call to function is_bool\(\) with bool will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Base/Page.php + path: packages/web/src/Items/Schema.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue + message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Base/Page.php + path: packages/web/src/Items/Schema.php - - message: '#^If condition is always false\.$#' - identifier: if.alwaysFalse + message: '#^Default value of the parameter \#2 \$table \(array\) of method FOG\\Items\\Schema\:\:dropDuplicateData\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Base/System.php + path: packages/web/src/Items/Schema.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Base/System.php + path: packages/web/src/Items/Schema.php - - message: '#^Method FOG\\Base\\System\:\:_versionCompare\(\) returns void but does not have any side effects\.$#' - identifier: void.pure + message: '#^Method FOG\\Items\\Schema\:\:dropDuplicateData\(\) with return type void returns array\ but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Base/System.php + path: packages/web/src/Items/Schema.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue + message: '#^Method FOG\\Items\\Schema\:\:exportdb\(\) should return string but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Base/System.php + path: packages/web/src/Items/Schema.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Boot/BootMenuBase.php + path: packages/web/src/Items/Schema.php - - message: '#^Negated boolean expression is always true\.$#' - identifier: booleanNot.alwaysTrue + message: '#^Parameter \#1 \$seconds of function set_time_limit expects int, string given\.$#' + identifier: argument.type count: 2 - path: packages/web/src/Boot/BootMenuBase.php + path: packages/web/src/Items/Schema.php - - message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' + message: '#^Parameter \#1 \$var of function count expects array\|Countable, string given\.$#' identifier: argument.type - count: 3 - path: packages/web/src/Boot/BootMenuBase.php + count: 1 + path: packages/web/src/Items/Schema.php - - message: '#^Result of \|\| is always true\.$#' - identifier: booleanOr.alwaysTrue - count: 2 - path: packages/web/src/Boot/BootMenuBase.php + message: '#^Strict comparison using \!\=\= between null and mixed will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue + count: 1 + path: packages/web/src/Items/Schema.php - - message: '#^Variable \$chkdsk in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Strict comparison using \=\=\= between 1 and 1 will always evaluate to true\.$#' + identifier: identical.alwaysTrue count: 1 - path: packages/web/src/Boot/BootMenuBase.php + path: packages/web/src/Items/Schema.php - - message: '#^Variable \$ip might not be defined\.$#' - identifier: variable.undefined - count: 3 - path: packages/web/src/Boot/BootMenuBase.php + message: '#^Call to function unset\(\) contains undefined variable \$viewop\.$#' + identifier: unset.variable + count: 1 + path: packages/web/src/Items/Setting.php - - message: '#^Call to function is_numeric\(\) with int\<0, max\> will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php + path: packages/web/src/Items/Setting.php - - message: '#^Cannot call method get\(\) on string\.$#' - identifier: method.nonObject - count: 2 - path: packages/web/src/Boot/IpxeBootMenu.php + message: '#^Method FOG\\Items\\Site\:\:destroy\(\) should return bool but returns object\.$#' + identifier: return.type + count: 1 + path: packages/web/src/Items/Site.php - - message: '#^Cannot call method isValid\(\) on string\.$#' - identifier: method.nonObject - count: 2 - path: packages/web/src/Boot/IpxeBootMenu.php + message: '#^Method FOG\\Items\\Site\:\:save\(\) should return object but returns false\.$#' + identifier: return.type + count: 1 + path: packages/web/src/Items/Site.php - - message: '#^Instanceof between FOG\\Items\\Host and FOG\\Items\\Host will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue - count: 2 - path: packages/web/src/Boot/IpxeBootMenu.php + message: '#^Return type \(bool\) of method FOG\\Items\\Site\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' + identifier: method.childReturnType + count: 1 + path: packages/web/src/Items/Site.php - - message: '#^Instanceof between FOG\\Items\\Image and FOG\\Items\\Image will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue + message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' + identifier: smaller.alwaysFalse count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php + path: packages/web/src/Items/Snapin.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Method FOG\\Items\\Snapin\:\:loadPath\(\) with return type void returns \$this\(FOG\\Items\\Snapin\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php + path: packages/web/src/Items/Snapin.php - - message: '#^Parameter \#1 \$TaskType of method FOG\\Items\\Host\:\:createImagePackage\(\) expects int, object given\.$#' - identifier: argument.type - count: 3 - path: packages/web/src/Boot/IpxeBootMenu.php - - - - message: '#^Parameter \#1 \$key of static method FOG\\Base\\FOGBase\:\:arrayInsertAfter\(\) expects string, int\<0, max\> given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php - - - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php - - - - message: '#^Property FOG\\Boot\\IpxeBootMenu\:\:\$_path is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php - - - - message: '#^Property FOG\\Boot\\IpxeBootMenu\:\:\$_shutdown is never written, only read\.$#' - identifier: property.onlyRead - count: 1 - path: packages/web/src/Boot/IpxeBootMenu.php - - - - message: '#^If condition is always false\.$#' - identifier: if.alwaysFalse + message: '#^Method FOG\\Items\\Snapin\:\:setPrimaryGroup\(\) should return array but return statement is missing\.$#' + identifier: return.missing count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/Snapin.php - - message: '#^PHPDoc tag @throws with type FOG\\Boot\\Exception is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable - count: 1 - path: packages/web/src/Boot/Registration.php - - - - message: '#^Parameter \#1 \$TaskType of method FOG\\Items\\Host\:\:createImagePackage\(\) expects int, object given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Boot/Registration.php + count: 2 + path: packages/web/src/Items/Snapin.php - - message: '#^Parameter \#1 \$input of function str_pad expects string, \(float\|int\) given\.$#' + message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/Snapin.php - - message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, \(float\|int\) given\.$#' + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' identifier: argument.type - count: 1 - path: packages/web/src/Boot/Registration.php + count: 4 + path: packages/web/src/Items/Snapin.php - - message: '#^Parameter \#3 \$pad_string of function str_pad expects string, int given\.$#' + message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' identifier: argument.type - count: 2 - path: packages/web/src/Boot/Registration.php - - - - message: '#^Variable \$ADDomain might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: packages/web/src/Boot/Registration.php - - - - message: '#^Variable \$ADOU might not be defined\.$#' - identifier: variable.undefined - count: 1 - path: packages/web/src/Boot/Registration.php - - - - message: '#^Variable \$ADPass might not be defined\.$#' - identifier: variable.undefined count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/Snapin.php - - message: '#^Variable \$ADUser might not be defined\.$#' - identifier: variable.undefined + message: '#^Call to function unset\(\) contains undefined variable \$node\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/StorageGroup.php - - message: '#^Variable \$enforce might not be defined\.$#' - identifier: variable.undefined + message: '#^Loose comparison using \=\= between null and null will always evaluate to true\.$#' + identifier: equal.alwaysTrue count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/StorageGroup.php - - message: '#^Variable \$useAD might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Items\\StorageGroup\:\:save\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Boot/Registration.php + path: packages/web/src/Items/StorageGroup.php - - message: '#^Comparison operation "\<" between int\<0, max\> and 0 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Boot/WakeOnLan.php + path: packages/web/src/Items/StorageGroup.php - - message: '#^Method FOG\\Boot\\WakeOnLan\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Comparison operation "\<" between object and 1 results in an error\.$#' + identifier: smaller.invalid count: 1 - path: packages/web/src/Boot/WakeOnLan.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse + message: '#^Method FOG\\Items\\StorageNode\:\:_getData\(\) with return type void returns array but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Boot/WakeOnLan.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Strict comparison using \=\=\= between array and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse + message: '#^Method FOG\\Items\\StorageNode\:\:_getData\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Boot/WakeOnLan.php - - - - message: '#^Call to an undefined method FOG\\Client\\FOGClient\:\:json\(\)\.$#' - identifier: method.notFound - count: 4 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Instanceof between FOG\\Items\\Host and FOG\\Items\\Host will always evaluate to true\.$#' - identifier: instanceof.alwaysTrue + message: '#^Method FOG\\Items\\StorageNode\:\:get\(\) should return object but returns string\.$#' + identifier: return.type count: 1 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns int but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Items\\StorageNode\:\:getNodeFailure\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns string but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Items\\StorageNode\:\:getNodeFailure\(\) should return object but returns true\.$#' + identifier: return.type count: 1 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Method FOG\\Client\\FOGClient\:\:__construct\(\) with return type void returns string\|false but should not return anything\.$#' - identifier: return.void + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 2 - path: packages/web/src/Client/FOGClient.php - - - - message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 1 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue + message: '#^PHPDoc tag @return has invalid value \(void;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 76 on line 4$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Client/FOGClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Parameter \#1 \$array \(null\) to function array_filter is empty, call has no effect\.$#' + identifier: arrayFilter.empty count: 1 - path: packages/web/src/Client/RegisterClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Parameter \#1 \$mac of method FOG\\Items\\Host\:\:addPendMAC\(\) expects array\\|string, array\ given\.$#' + message: '#^Parameter \#1 \$array_arg of function natcasesort expects array, null given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Client/RegisterClient.php - - - - message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 1 - path: packages/web/src/Client/ServiceModule.php - - - - message: '#^Instanceof between null and FOG\\Items\\StorageGroup will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse - count: 2 - path: packages/web/src/Client/SnapinClient.php - - - - message: '#^Instanceof between null and FOG\\Items\\StorageNode will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse - count: 2 - path: packages/web/src/Client/SnapinClient.php - - - - message: '#^Method FOG\\Client\\SnapinClient\:\:json\(\) should return array\\|void but returns array\\>\>\.$#' - identifier: return.type - count: 1 - path: packages/web/src/Client/SnapinClient.php - - - - message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 3 - path: packages/web/src/Client/SnapinClient.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' + message: '#^Parameter \#1 \$input of function array_filter expects array, null given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Client/SnapinClient.php + path: packages/web/src/Items/StorageNode.php - message: '#^Parameter \#1 \$str of function urlencode expects string, object given\.$#' identifier: argument.type - count: 1 - path: packages/web/src/Client/SnapinClient.php + count: 2 + path: packages/web/src/Items/StorageNode.php - - message: '#^Parameter \#2 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + message: '#^Parameter \#2 \$haystack of function array_search expects array, object given\.$#' identifier: argument.type - count: 1 - path: packages/web/src/Client/SnapinClient.php + count: 2 + path: packages/web/src/Items/StorageNode.php - - message: '#^Parameter \#4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' identifier: argument.type - count: 1 - path: packages/web/src/Client/SnapinClient.php - - - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse - count: 4 - path: packages/web/src/Client/SnapinClient.php + count: 2 + path: packages/web/src/Items/StorageNode.php - - message: '#^PHPDoc tag @throws with type FOG\\Client\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Parameter \#4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Client/UserTrack.php + path: packages/web/src/Items/StorageNode.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 1 - path: packages/web/src/Db/DatabaseManager.php + message: '#^Result of method FOG\\Items\\StorageNode\:\:_getData\(\) \(void\) is used\.$#' + identifier: method.void + count: 3 + path: packages/web/src/Items/StorageNode.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Strict comparison using \=\=\= between null and false will always evaluate to false\.$#' + identifier: identical.alwaysFalse count: 2 - path: packages/web/src/Db/DatabaseManager.php + path: packages/web/src/Items/StorageNode.php - - message: '#^Result of \|\| is always false\.$#' - identifier: booleanOr.alwaysFalse - count: 1 - path: packages/web/src/Db/DatabaseManager.php + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 2 + path: packages/web/src/Items/Task.php - - message: '#^Static method FOG\\Db\\DatabaseManager\:\:_convertEngine\(\) is unused\.$#' - identifier: method.unused - count: 1 - path: packages/web/src/Db/DatabaseManager.php + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue + count: 2 + path: packages/web/src/Items/TaskLog.php - - message: '#^Static method FOG\\Db\\DatabaseManager\:\:_getVersion\(\) is unused\.$#' - identifier: method.unused + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/src/Db/DatabaseManager.php + path: packages/web/src/Items/User.php - - message: '#^Ternary operator condition is always true\.$#' - identifier: ternary.alwaysTrue + message: '#^Method FOG\\Items\\User\:\:isLoggedIn\(\) should return bool but returns \$this\(FOG\\Items\\User\)\|FOG\\Items\\User\.$#' + identifier: return.type count: 1 - path: packages/web/src/Db/DatabaseManager.php + path: packages/web/src/Items/User.php - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable - count: 1 - path: packages/web/src/Db/DatabaseManager.php + message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 2 + path: packages/web/src/Items/User.php - - message: '#^Constant DATABASE_PASSWORD not found\.$#' - identifier: constant.notFound + message: '#^Right side of && is always true\.$#' + identifier: booleanAnd.rightAlwaysTrue count: 1 - path: packages/web/src/Db/Mysqldump.php + path: packages/web/src/Items/User.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Strict comparison using \!\=\= between true and false will always evaluate to true\.$#' + identifier: notIdentical.alwaysTrue count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/User.php - - message: '#^Call to function is_bool\(\) with object will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Variable \$displayName in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/User.php - - message: '#^Cannot call method lastInsertId\(\) on resource\.$#' - identifier: method.nonObject + message: '#^Variable \$lastactivity in isset\(\) is never defined\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/User.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse + message: '#^Method FOG\\Items\\UserGroup\:\:destroy\(\) should return bool but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Base/StorageEpoch.php + path: packages/web/src/Items/UserGroup.php - - message: '#^Cannot call method exec\(\) on resource\.$#' - identifier: method.nonObject + message: '#^Method FOG\\Items\\UserGroup\:\:save\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/UserGroup.php - - message: '#^Static method FOG\\Db\\PDODB\:\:_pinSessionZone\(\) is unused\.$#' - identifier: method.unused + message: '#^Return type \(bool\) of method FOG\\Items\\UserGroup\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' + identifier: method.childReturnType count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/UserGroup.php - - message: '#^Cannot call method prepare\(\) on resource\.$#' - identifier: method.nonObject + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Cannot call method query\(\) on resource\.$#' - identifier: method.nonObject - count: 3 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Items/UserGroup.php - - message: '#^Cannot call method quote\(\) on resource\.$#' - identifier: method.nonObject + message: '#^Call to method get\(\) on an unknown class FOG\\Managers\\APIToken\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/APITokenManager.php - - message: '#^Dead catch \- PDOException is never thrown in the try block\.$#' - identifier: catch.neverThrown + message: '#^Method FOG\\Managers\\APITokenManager\:\:visibleToken\(\) has invalid return type FOG\\Managers\\APIToken\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue - count: 3 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/APITokenManager.php - - message: '#^Method FOG\\Db\\PDODB\:\:__construct\(\) with return type void returns \$this\(FOG\\Db\\PDODB\) but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Managers\\APITokenManager\:\:visibleToken\(\) should return FOG\\Managers\\APIToken\|null but returns FOG\\Items\\APIToken\.$#' + identifier: return.type count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/APITokenManager.php - - message: '#^Method FOG\\Db\\PDODB\:\:_connect\(\) has FOG\\Db\\PDOException in PHPDoc @throws tag but it''s not thrown\.$#' - identifier: throws.unusedType + message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Method FOG\\Db\\PDODB\:\:link\(\) should return object but returns resource\.$#' - identifier: return.type + message: '#^Comparison operation "\>" between int\<1, max\> and 0 is always true\.$#' + identifier: greater.alwaysTrue count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 7 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^PHPDoc tag @throws with type FOG\\Db\\PDOException is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Managers\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable - count: 6 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue - count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Static method FOG\\Db\\PDODB\:\:_boundReadTimeout\(\) is unused\.$#' - identifier: method.unused + message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(string\)\: bool\)\|null, ''strlen'' given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_dbName \(string\) does not accept false\.$#' - identifier: assign.propertyType - count: 2 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_dbName \(string\) in isset\(\) is not nullable\.$#' - identifier: isset.property + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) does not accept false\.$#' - identifier: assign.propertyType + message: '#^Variable \$MACHost on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable count: 2 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) does not accept null\.$#' - identifier: assign.propertyType - count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_link \(resource\) in isset\(\) is not nullable\.$#' - identifier: isset.property - count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_options is never read, only written\.$#' - identifier: property.onlyWritten + message: '#^Variable \$macs on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.variable count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/HostManager.php - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) does not accept null\.$#' + message: '#^Static property FOG\\Base\\FOGBase\:\:\$selected \(bool\|int\) does not accept string\.$#' identifier: assign.propertyType - count: 3 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) in empty\(\) is not falsy\.$#' - identifier: empty.property count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Static property FOG\\Db\\PDODB\:\:\$_queryResult \(object\) in isset\(\) is not nullable\.$#' - identifier: isset.property - count: 1 - path: packages/web/src/Db/PDODB.php - - - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable - count: 3 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Managers/PXEMenuOptionsManager.php - - message: '#^Variable \$data in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$host\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Variable \$errInfo might not be defined\.$#' - identifier: variable.undefined + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$mode\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Db/PDODB.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Path in include\(\) "/commons/schema\-constraints\.php" is not a file or it does not exist\.$#' - identifier: include.fileNotFound + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$passive\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Db/SchemaReconciler.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Path in include\(\) "/commons/schema\-expected\.php" is not a file or it does not exist\.$#' - identifier: include.fileNotFound + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$password\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Db/SchemaReconciler.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' - identifier: binaryOp.invalid + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$port\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$timeout\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function unset\(\) contains undefined variable \$affected_rows\.$#' - identifier: unset.variable + message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$username\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function unset\(\) contains undefined variable \$first_id\.$#' - identifier: unset.variable + message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:nlist\(\)\.$#' + identifier: method.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function unset\(\) contains undefined variable \$ids\.$#' - identifier: unset.variable + message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:pasv\(\)\.$#' + identifier: method.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function unset\(\) contains undefined variable \$insert_value\.$#' - identifier: unset.variable + message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:put\(\)\.$#' + identifier: method.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function unset\(\) contains undefined variable \$multicastsessionassocs\.$#' - identifier: unset.variable - count: 1 - path: packages/web/src/Items/Group.php + message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:rawlist\(\)\.$#' + identifier: method.notFound + count: 3 + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$id on int\.$#' - identifier: property.nonObject - count: 6 - path: packages/web/src/Items/Group.php + message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:rmdir\(\)\.$#' + identifier: method.notFound + count: 2 + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$initIDs on int\.$#' - identifier: property.nonObject + message: '#^Call to function is_object\(\) with resource will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$isDeploy on int\.$#' - identifier: property.nonObject + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$isImagingTask on int\.$#' - identifier: property.nonObject + message: '#^Method FOG\\Net\\FOGFTP\:\:__set\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$isMulticast on int\.$#' - identifier: property.nonObject + message: '#^Method FOG\\Net\\FOGFTP\:\:connect\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$isSnapinTask on int\.$#' - identifier: property.nonObject + message: '#^Method FOG\\Net\\FOGFTP\:\:rename\(\) has invalid return type FOG\\Net\\ftp_rename\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$isSnapinTasking on int\.$#' - identifier: property.nonObject + message: '#^Method FOG\\Net\\FOGFTP\:\:rename\(\) should return FOG\\Net\\ftp_rename but returns \$this\(FOG\\Net\\FOGFTP\)\.$#' + identifier: return.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Comparison operation "\>" between int\<4, 5\> and 0 is always true\.$#' - identifier: greater.alwaysTrue + message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) has invalid return type FOG\\Net\\ftp_size\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Method FOG\\Items\\Group\:\:_createSnapinTasking\(\) should return array but empty return statement found\.$#' - identifier: return.empty + message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) should return FOG\\Net\\ftp_size but returns float\.$#' + identifier: return.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Method FOG\\Items\\Group\:\:destroy\(\) should return bool but returns object\.$#' + message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) should return FOG\\Net\\ftp_size but returns int\.$#' identifier: return.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Method FOG\\Items\\Group\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^PHPDoc tag @return has invalid value \(\$this;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 172 on line 7$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^PHPDoc tag @param references unknown parameter\: \$useAD$#' - identifier: parameter.notFound + message: '#^PHPDoc tag @return has invalid value \(array;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 121 on line 6$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' + message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable - count: 1 - path: packages/web/src/Items/Group.php + count: 2 + path: packages/web/src/Net/FOGFTP.php - - message: '#^Return type \(bool\) of method FOG\\Items\\Group\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' - identifier: method.childReturnType + message: '#^Parameter \#1 \$password of function password_hash expects string, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Strict comparison using \!\=\= between string and null will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 4 - path: packages/web/src/Items/Group.php + message: '#^Parameter \#1 \$password of function password_hash expects string, true given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Net/FOGFTP.php - - message: '#^Strict comparison using \=\=\= between int and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse + message: '#^Parameter \#2 \$mode of function ftp_chmod expects int, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Group.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' - identifier: binaryOp.invalid + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGFTP.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 2 - path: packages/web/src/Items/Host.php + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue + count: 1 + path: packages/web/src/Net/FOGFTP.php - - message: '#^Cannot access property \$id on int\.$#' - identifier: property.nonObject - count: 6 - path: packages/web/src/Items/Host.php + message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$host\.$#' + identifier: property.notFound + count: 1 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Cannot access property \$isCapture on int\.$#' - identifier: property.nonObject + message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$password\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Cannot access property \$isImagingTask on int\.$#' - identifier: property.nonObject - count: 3 - path: packages/web/src/Items/Host.php + message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$port\.$#' + identifier: property.notFound + count: 1 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Cannot access property \$isMulticast on int\.$#' - identifier: property.nonObject + message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$username\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Cannot access property \$isSnapinTask on int\.$#' - identifier: property.nonObject + message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:auth_password\(\)\.$#' + identifier: method.notFound count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Cannot access property \$isSnapinTasking on int\.$#' - identifier: property.nonObject + message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:sftp_rmdir\(\)\.$#' + identifier: method.notFound count: 2 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Comparison operation "\>" between int\<1, max\> and 0 is always true\.$#' - identifier: greater.alwaysTrue + message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:sftp_unlink\(\)\.$#' + identifier: method.notFound count: 2 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Default value of the parameter \#1 \$mac \(false\) of method FOG\\Items\\Host\:\:clientMacCheck\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Call to function is_object\(\) with resource will always evaluate to false\.$#' + identifier: function.impossibleType count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Default value of the parameter \#1 \$mac \(false\) of method FOG\\Items\\Host\:\:imageMacCheck\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Default value of the parameter \#4 \$Task \(false\) of method FOG\\Items\\Host\:\:_createSnapinTasking\(\) is incompatible with type object\.$#' - identifier: parameter.defaultValue + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Default value of the parameter \#8 \$passreset \(false\) of method FOG\\Items\\Host\:\:_createTasking\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Method FOG\\Net\\FOGSSH\:\:__set\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue + message: '#^Method FOG\\Net\\FOGSSH\:\:connect\(\) should return object but returns false\.$#' + identifier: return.type count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Instanceof between null and FOG\\Items\\StorageGroup will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse - count: 1 - path: packages/web/src/Items/Host.php + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse + count: 2 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Instanceof between null and FOG\\Items\\StorageNode will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse - count: 2 - path: packages/web/src/Items/Host.php + message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable + count: 3 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Instanceof between string and FOG\\Items\\MACAddress will always evaluate to false\.$#' - identifier: instanceof.alwaysFalse - count: 2 - path: packages/web/src/Items/Host.php + message: '#^Parameter \#1 \$password of function password_hash expects string, int given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Left side of && is always false\.$#' - identifier: booleanAnd.leftAlwaysFalse + message: '#^Parameter \#1 \$password of function password_hash expects string, true given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Method FOG\\Items\\Host\:\:_createSnapinTasking\(\) with return type void returns \$this\(FOG\\Items\\Host\) but should not return anything\.$#' - identifier: return.void + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Method FOG\\Items\\Host\:\:addPendMAC\(\) has invalid return type FOG\\Items\\obect\.$#' - identifier: class.notFound + message: '#^Property FOG\\Net\\FOGSSH\:\:\$_link \(resource\) does not accept null\.$#' + identifier: assign.propertyType count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Method FOG\\Items\\Host\:\:addPendMAC\(\) should return FOG\\Items\\obect but returns \$this\(FOG\\Items\\Host\)\.$#' - identifier: return.type + message: '#^Property FOG\\Net\\FOGSSH\:\:\$_sftp \(resource\) does not accept null\.$#' + identifier: assign.propertyType count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Method FOG\\Items\\Host\:\:createImagePackage\(\) should return string but returns true\.$#' - identifier: return.type + message: '#^Property FOG\\Net\\FOGSSH\:\:\$_sftp \(resource\) in isset\(\) is not nullable\.$#' + identifier: isset.property count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGSSH.php - - message: '#^Method FOG\\Items\\Host\:\:ignore\(\) should return object but return statement is missing\.$#' - identifier: return.missing - count: 2 - path: packages/web/src/Items/Host.php + message: '#^Result of && is always false\.$#' + identifier: booleanAnd.alwaysFalse + count: 1 + path: packages/web/src/Net/FOGSSH.php - - message: '#^Negated boolean expression is always true\.$#' - identifier: booleanNot.alwaysTrue + message: '#^Access to an undefined property FOG\\Net\\FOGURLRequests\:\:\$headers\.$#' + identifier: property.notFound count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 3 - path: packages/web/src/Items/Host.php + message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse - count: 2 - path: packages/web/src/Items/Host.php + message: '#^Default value of the parameter \#6 \$callback \(false\) of method FOG\\Net\\FOGURLRequests\:\:process\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue + count: 1 + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Result of \|\| is always true\.$#' - identifier: booleanOr.alwaysTrue + message: '#^Default value of the parameter \#7 \$file \(false\) of method FOG\\Net\\FOGURLRequests\:\:process\(\) is incompatible with type string\.$#' + identifier: parameter.defaultValue count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Static property FOG\\Items\\Host\:\:\$_hostalo \(int\) does not accept default value of type array\.$#' - identifier: property.defaultValue + message: '#^Method FOG\\Net\\FOGURLRequests\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Variable \$StorageGroup might not be defined\.$#' - identifier: variable.undefined + message: '#^Method FOG\\Net\\FOGURLRequests\:\:__set\(\) with return type void returns \$this\(FOG\\Net\\FOGURLRequests\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Host.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Variable \$StorageNode might not be defined\.$#' - identifier: variable.undefined - count: 4 - path: packages/web/src/Items/Host.php + message: '#^Method FOG\\Net\\FOGURLRequests\:\:execute\(\) should return object but returns array\\.$#' + identifier: return.type + count: 1 + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^Method FOG\\Net\\FOGURLRequests\:\:process\(\) should return array but returns object\.$#' + identifier: return.type count: 1 - path: packages/web/src/Items/Image.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Method FOG\\Items\\Image\:\:setPrimaryGroup\(\) should return array but return statement is missing\.$#' - identifier: return.missing + message: '#^Parameter \#1 \$obj of function spl_object_id expects object, \(resource\|false\) given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Image.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/Image.php + message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(lowercase\-string\)\: bool\)\|null, ''strlen'' given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + message: '#^Parameter \#2 \$mode of function stream_set_blocking expects bool, int given\.$#' identifier: argument.type - count: 3 - path: packages/web/src/Items/Image.php + count: 1 + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Variable \$DBIDs might not be defined\.$#' - identifier: variable.undefined + message: '#^Parameter &\$url by\-ref type of method FOG\\Net\\FOGURLRequests\:\:_validUrl\(\) expects string, string\|false given\.$#' + identifier: parameterByRef.type count: 1 - path: packages/web/src/Items/Image.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^Method FOG\\Items\\MACAddress\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Variable \$url in isset\(\) always exists and is not nullable\.$#' + identifier: isset.variable count: 1 - path: packages/web/src/Items/MACAddress.php + path: packages/web/src/Net/FOGURLRequests.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/MACAddress.php + message: '#^Call to function is_numeric\(\) with int\\|int\<1, max\> will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 7 + path: packages/web/src/Net/Ping.php - - message: '#^Parameter \#4 \$optval of function socket_set_option expects array\|int\|string, true given\.$#' - identifier: argument.type + message: '#^Method FOG\\Net\\Ping\:\:execSend\(\) has invalid return type FOG\\Net\\error\.$#' + identifier: class.notFound count: 1 - path: packages/web/src/Items/MACAddress.php + path: packages/web/src/Net/Ping.php - - message: '#^Property FOG\\Items\\MACAddress\:\:\$_Host is never read, only written\.$#' - identifier: property.onlyWritten - count: 1 - path: packages/web/src/Items/MACAddress.php + message: '#^Method FOG\\Net\\Ping\:\:execSend\(\) should return FOG\\Net\\error but returns int\.$#' + identifier: return.type + count: 2 + path: packages/web/src/Net/Ping.php - - message: '#^Method FOG\\Items\\Module\:\:save\(\) should return object but returns false\.$#' + message: '#^Method FOG\\Net\\Ping\:\:execute\(\) should return int but returns FOG\\Net\\error\.$#' identifier: return.type count: 1 - path: packages/web/src/Items/Module.php + path: packages/web/src/Net/Ping.php - - message: '#^Method FOG\\Items\\MulticastSession\:\:cancel\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' + identifier: throws.notThrowable count: 1 - path: packages/web/src/Items/MulticastSession.php + path: packages/web/src/Net/Ping.php - - message: '#^Method FOG\\Items\\MulticastSession\:\:complete\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Constructor of class FOG\\Pages\\ActivityManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/MulticastSession.php + path: packages/web/src/Pages/ActivityManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/MulticastSession.php + message: '#^Constructor of class FOG\\Pages\\ApiDocumentation has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter + count: 1 + path: packages/web/src/Pages/ApiDocumentation.php - - message: '#^Offset ''requires'' on array\{\} on left side of \?\? does not exist\.$#' - identifier: nullCoalesce.offset + message: '#^Constructor of class FOG\\Pages\\AuditManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Plugin.php + path: packages/web/src/Pages/AuditManagement.php - - message: '#^Offset mixed on array\{\} on left side of \?\? does not exist\.$#' - identifier: nullCoalesce.offset + message: '#^Constructor of class FOG\\Pages\\ClientManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Plugin.php + path: packages/web/src/Pages/ClientManagement.php - - message: '#^Parameter \#1 \$array \(array\{\}\) to function array_filter is empty, call has no effect\.$#' - identifier: arrayFilter.empty + message: '#^Call to function unset\(\) contains undefined variable \$SystemUptime\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Items/Plugin.php + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Parameter \#1 \$array \(array\{\}\) to function array_values is empty, call has no effect\.$#' - identifier: arrayValues.empty + message: '#^Call to function unset\(\) contains undefined variable \$fields\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Items/Plugin.php + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Parameter \#3 \$alias of class PharData constructor expects string, null given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Items/Plugin.php + message: '#^Call to function unset\(\) contains undefined variable \$tftp\.$#' + identifier: unset.variable + count: 2 + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Strict comparison using \!\=\= between null and null will always evaluate to false\.$#' - identifier: notIdentical.alwaysFalse + message: '#^Constructor of class FOG\\Pages\\DashboardPage has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Plugin.php + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Method FOG\\Items\\PowerManagement\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Static property FOG\\Pages\\DashboardPage\:\:\$_tftp is never read, only written\.$#' + identifier: property.onlyWritten count: 1 - path: packages/web/src/Items/PowerManagement.php + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Method FOG\\Items\\Printer\:\:destroy\(\) should return bool but returns object\.$#' - identifier: return.type + message: '#^Variable \$pendingMACs might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/Printer.php + path: packages/web/src/Pages/DashboardPage.php - - message: '#^Method FOG\\Items\\Printer\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Call to function unset\(\) contains undefined variable \$findWhere\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Items/Printer.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Return type \(bool\) of method FOG\\Items\\Printer\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' - identifier: method.childReturnType + message: '#^Call to function unset\(\) contains undefined variable \$setWhere\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Items/Printer.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Method FOG\\Items\\Role\:\:destroy\(\) should return bool but returns object\.$#' - identifier: return.type + message: '#^Call to function unset\(\) contains undefined variable \$val\.$#' + identifier: unset.variable count: 1 - path: packages/web/src/Items/Role.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Method FOG\\Items\\Role\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Constructor of class FOG\\Pages\\FOGConfigurationPage has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Role.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Return type \(bool\) of method FOG\\Items\\Role\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' - identifier: method.childReturnType + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Items/Role.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Ternary operator condition is always false\.$#' - identifier: ternary.alwaysFalse + message: '#^Offset ''FOG_PXE_HIDDENMENU…''\|''FOG_PXE_MENU_TIMEOUT'' on array\{FOG_PXE_HIDDENMENU_TIMEOUT\: true, FOG_PXE_MENU_TIMEOUT\: true\} in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset count: 1 - path: packages/web/src/Items/Role.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Method FOG\\Items\\ScheduledTask\:\:cancel\(\) should return bool but returns object\.$#' - identifier: return.type + message: '#^Offset ''refresh'' does not exist on array\{checkbox\: array, numeric\: array, ip\: array\}\.$#' + identifier: offsetAccess.notFound count: 1 - path: packages/web/src/Items/ScheduledTask.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^PHPDoc tag @return has invalid value \(object\.\)\: Unexpected token "\.", expected TOKEN_HORIZONTAL_WS at offset 81 on line 4$#' - identifier: phpDoc.parseError + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type + count: 2 + path: packages/web/src/Pages/FOGConfigurationPage.php + + - + message: '#^Ternary operator condition is always true\.$#' + identifier: ternary.alwaysTrue count: 1 - path: packages/web/src/Items/ScheduledTask.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Call to function is_bool\(\) with bool will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^Variable \$objGetter might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^Default value of the parameter \#2 \$table \(array\) of method FOG\\Items\\Schema\:\:dropDuplicateData\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue - count: 1 - path: packages/web/src/Items/Schema.php + message: '#^Variable \$set might not be defined\.$#' + identifier: variable.undefined + count: 11 + path: packages/web/src/Pages/FOGConfigurationPage.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue + message: '#^Constructor of class FOG\\Pages\\GroupManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Method FOG\\Items\\Schema\:\:dropDuplicateData\(\) with return type void returns array\ but should not return anything\.$#' + message: '#^Method FOG\\Pages\\GroupManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Method FOG\\Items\\Schema\:\:exportdb\(\) should return string but empty return statement found\.$#' - identifier: return.empty + message: '#^Parameter \#1 \$array \(array\, mixed\>\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' + identifier: arrayFilter.same count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Parameter \#1 \$seconds of function set_time_limit expects int, string given\.$#' + message: '#^Parameter \#3 \$body of static method FOG\\Base\\FOGPage\:\:makeModal\(\) expects string, null given\.$#' identifier: argument.type - count: 2 - path: packages/web/src/Items/Schema.php + count: 1 + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Parameter \#1 \$var of function count expects array\|Countable, string given\.$#' - identifier: argument.type + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Strict comparison using \!\=\= between null and mixed will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: packages/web/src/Items/Schema.php + message: '#^Result of method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) \(void\) is used\.$#' + identifier: method.void + count: 2 + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Strict comparison using \=\=\= between 1 and 1 will always evaluate to true\.$#' - identifier: identical.alwaysTrue + message: '#^Variable \$printers might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/Schema.php + path: packages/web/src/Pages/GroupManagement.php - - message: '#^Call to function unset\(\) contains undefined variable \$viewop\.$#' - identifier: unset.variable - count: 1 - path: packages/web/src/Items/Setting.php + message: '#^Access to an undefined property FOG\\Pages\\HostManagement\:\:\$exitEfi\.$#' + identifier: property.notFound + count: 5 + path: packages/web/src/Pages/HostManagement.php - - message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Items/Setting.php + message: '#^Access to an undefined property FOG\\Pages\\HostManagement\:\:\$exitNorm\.$#' + identifier: property.notFound + count: 5 + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\Site\:\:destroy\(\) should return bool but returns object\.$#' - identifier: return.type + message: '#^Constructor of class FOG\\Pages\\HostManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/Site.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\Site\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Method FOG\\Pages\\HostManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Site.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Return type \(bool\) of method FOG\\Items\\Site\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' - identifier: method.childReturnType + message: '#^Method FOG\\Pages\\HostManagement\:\:getModulesList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Site.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Comparison operation "\<" between 1 and 1 is always false\.$#' - identifier: smaller.alwaysFalse + message: '#^Method FOG\\Pages\\HostManagement\:\:getPrintersList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/Snapin.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\Snapin\:\:loadPath\(\) with return type void returns \$this\(FOG\\Items\\Snapin\) but should not return anything\.$#' + message: '#^Method FOG\\Pages\\HostManagement\:\:getSnapinsList\(\) with return type void returns null but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Items/Snapin.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\Snapin\:\:setPrimaryGroup\(\) should return array but return statement is missing\.$#' - identifier: return.missing + message: '#^Method FOG\\Pages\\HostManagement\:\:pending\(\) should return false but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Items/Snapin.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/Snapin.php + message: '#^Method FOG\\Pages\\HostManagement\:\:pending\(\) should return false but return statement is missing\.$#' + identifier: return.missing + count: 1 + path: packages/web/src/Pages/HostManagement.php - - message: '#^Parameter \#1 \$str of function trim expects string, object given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\HostManagement\:\:pendingMacs\(\) should return false but empty return statement found\.$#' + identifier: return.empty count: 1 - path: packages/web/src/Items/Snapin.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' - identifier: argument.type - count: 4 - path: packages/web/src/Items/Snapin.php + message: '#^Method FOG\\Pages\\HostManagement\:\:pendingMacs\(\) should return false but return statement is missing\.$#' + identifier: return.missing + count: 1 + path: packages/web/src/Pages/HostManagement.php - - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + message: '#^Parameter \#1 \$macs of method FOG\\Managers\\HostManager\:\:getHostByMacAddresses\(\) expects array, string given\.$#' identifier: argument.type count: 1 - path: packages/web/src/Items/Snapin.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Call to function unset\(\) contains undefined variable \$node\.$#' - identifier: unset.variable + message: '#^Parameter \#3 \$body of static method FOG\\Base\\FOGPage\:\:makeModal\(\) expects string, null given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/StorageGroup.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Loose comparison using \=\= between null and null will always evaluate to true\.$#' - identifier: equal.alwaysTrue + message: '#^Parameter \#4 \$filter of method FOG\\Base\\FOGManagerController\:\:buildSelectBox\(\) expects string, list\<\(int\|string\)\> given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/StorageGroup.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\StorageGroup\:\:save\(\) should return object but returns false\.$#' - identifier: return.type - count: 1 - path: packages/web/src/Items/StorageGroup.php + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void + count: 3 + path: packages/web/src/Pages/HostManagement.php - - message: '#^Unreachable statement \- code above always terminates\.$#' - identifier: deadCode.unreachable + message: '#^Result of method FOG\\Base\\FOGPage\:\:newPMDisplay\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Items/StorageGroup.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Comparison operation "\<" between object and 1 results in an error\.$#' - identifier: smaller.invalid + message: '#^Ternary operator condition is always false\.$#' + identifier: ternary.alwaysFalse count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\StorageNode\:\:_getData\(\) with return type void returns array but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$code might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\StorageNode\:\:_getData\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$msg might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\StorageNode\:\:get\(\) should return object but returns string\.$#' - identifier: return.type + message: '#^Variable \$val might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/HostManagement.php - - message: '#^Method FOG\\Items\\StorageNode\:\:getNodeFailure\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Binary operation "\*" between array\|string and 60 results in an error\.$#' + identifier: binaryOp.invalid count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Method FOG\\Items\\StorageNode\:\:getNodeFailure\(\) should return object but returns true\.$#' - identifier: return.type + message: '#^Constructor of class FOG\\Pages\\ImageManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/StorageNode.php - - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 2 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^PHPDoc tag @return has invalid value \(void;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 76 on line 4$#' - identifier: phpDoc.parseError + message: '#^Expression on left side of \?\? is not nullable\.$#' + identifier: nullCoalesce.expr count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#1 \$array \(null\) to function array_filter is empty, call has no effect\.$#' - identifier: arrayFilter.empty + message: '#^Method FOG\\Pages\\ImageManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#1 \$array_arg of function natcasesort expects array, null given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\ImageManagement\:\:getSessionsList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#1 \$input of function array_filter expects array, null given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\ImageManagement\:\:getStoragegroupsList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Items/StorageNode.php - - - - message: '#^Parameter \#1 \$str of function urlencode expects string, object given\.$#' - identifier: argument.type - count: 2 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#2 \$haystack of function array_search expects array, object given\.$#' + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' identifier: argument.type count: 2 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#3 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' + message: '#^Parameter \#4 \$filter of method FOG\\Base\\FOGManagerController\:\:buildSelectBox\(\) expects string, list\<\(int\|string\)\> given\.$#' identifier: argument.type - count: 2 - path: packages/web/src/Items/StorageNode.php + count: 1 + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Parameter \#4 \.\.\.\$values of function sprintf expects bool\|float\|int\|string\|null, object given\.$#' - identifier: argument.type + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Items/StorageNode.php + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Result of method FOG\\Items\\StorageNode\:\:_getData\(\) \(void\) is used\.$#' - identifier: method.void - count: 3 - path: packages/web/src/Items/StorageNode.php + message: '#^Variable \$msgSuccess might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Strict comparison using \=\=\= between null and false will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 2 - path: packages/web/src/Items/StorageNode.php + message: '#^Variable \$storagegroups might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 4 - path: packages/web/src/Items/Task.php + message: '#^Variable \$titleFail might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Pages/ImageManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/Task.php + message: '#^Variable \$titleSuccess might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Pages/ImageManagement.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue - count: 2 - path: packages/web/src/Items/TaskLog.php + message: '#^Constructor of class FOG\\Pages\\ImpersonateManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter + count: 1 + path: packages/web/src/Pages/ImpersonateManagement.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^Constructor of class FOG\\Pages\\IpxeManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/IpxeManagement.php - - message: '#^Method FOG\\Items\\User\:\:isLoggedIn\(\) should return bool but returns \$this\(FOG\\Items\\User\)\|FOG\\Items\\User\.$#' - identifier: return.type + message: '#^Constructor of class FOG\\Pages\\ModuleManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/ModuleManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Items\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Items/User.php + message: '#^Method FOG\\Pages\\ModuleManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void + count: 1 + path: packages/web/src/Pages/ModuleManagement.php - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/ModuleManagement.php - - message: '#^Strict comparison using \!\=\= between true and false will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue + message: '#^Constructor of class FOG\\Pages\\PluginManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Variable \$displayName in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Variable \$lastactivity in isset\(\) is never defined\.$#' - identifier: isset.variable + message: '#^PHPDoc tag @return has invalid value \(false;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 121 on line 6$#' + identifier: phpDoc.parseError count: 1 - path: packages/web/src/Items/User.php + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Method FOG\\Items\\UserGroup\:\:destroy\(\) should return bool but returns object\.$#' - identifier: return.type + message: '#^Parameter \#1 \$main of static method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/UserGroup.php + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Method FOG\\Items\\UserGroup\:\:save\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Parameter \#1 \(array\) of echo cannot be converted to string\.$#' + identifier: echo.nonString count: 1 - path: packages/web/src/Items/UserGroup.php + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Return type \(bool\) of method FOG\\Items\\UserGroup\:\:destroy\(\) should be compatible with return type \(object\) of method FOG\\Base\\FOGController\:\:destroy\(\)$#' - identifier: method.childReturnType + message: '#^Parameter \#2 \$hookMain of static method FOG\\Base\\FOGPage\:\:buildMainMenuItems\(\) expects array, string given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Items/UserGroup.php + path: packages/web/src/Pages/PluginManagement.php - message: '#^Ternary operator condition is always false\.$#' identifier: ternary.alwaysFalse - count: 1 - path: packages/web/src/Items/UserGroup.php + count: 4 + path: packages/web/src/Pages/PluginManagement.php - - message: '#^Call to method get\(\) on an unknown class FOG\\Managers\\APIToken\.$#' - identifier: class.notFound + message: '#^Constructor of class FOG\\Pages\\PrinterManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Managers/APITokenManager.php + path: packages/web/src/Pages/PrinterManagement.php - - message: '#^Method FOG\\Managers\\APITokenManager\:\:visibleToken\(\) has invalid return type FOG\\Managers\\APIToken\.$#' - identifier: class.notFound + message: '#^Method FOG\\Pages\\PrinterManagement\:\:getHostsDefaultList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Managers/APITokenManager.php + path: packages/web/src/Pages/PrinterManagement.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Method FOG\\Pages\\PrinterManagement\:\:getHostsList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/PrinterManagement.php - - message: '#^Comparison operation "\>" between int\<1, max\> and 0 is always true\.$#' - identifier: greater.alwaysTrue + message: '#^Property FOG\\Pages\\PrinterManagement\:\:\$_config is unused\.$#' + identifier: property.unused count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/PrinterManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Managers\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Call to function is_array\(\) with \*NEVER\* will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(string\)\: bool\)\|null, ''strlen'' given\.$#' - identifier: argument.type + message: '#^Empty array passed to foreach\.$#' + identifier: foreach.emptyArray count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type + message: '#^If condition is always true\.$#' + identifier: if.alwaysTrue count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Variable \$MACHost on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable - count: 2 - path: packages/web/src/Managers/HostManager.php + message: '#^Method FOG\\Pages\\ProcessLogin\:\:processMainLogin\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void + count: 3 + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Variable \$hostID might not be defined\.$#' - identifier: variable.undefined + message: '#^Offset ''icon'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Variable \$macs on left side of \?\? always exists and is not nullable\.$#' - identifier: nullCoalesce.variable + message: '#^Offset ''label'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset count: 1 - path: packages/web/src/Managers/HostManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Static property FOG\\Base\\FOGBase\:\:\$selected \(bool\|int\) does not accept string\.$#' - identifier: assign.propertyType + message: '#^Offset ''url'' on \*NEVER\* in isset\(\) always exists and is not nullable\.$#' + identifier: isset.offset count: 1 - path: packages/web/src/Managers/PXEMenuOptionsManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Method FOG\\Managers\\ScheduledTaskManager\:\:cancel\(\) should return bool but returns null\.$#' - identifier: return.type + message: '#^Property FOG\\Pages\\ProcessLogin\:\:\$_langMenu is unused\.$#' + identifier: property.unused count: 1 - path: packages/web/src/Managers/ScheduledTaskManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Result of static method FOG\\Router\\Route\:\:deletemass\(\) \(void\) is used\.$#' + message: '#^Result of static method FOG\\Pages\\ProcessLogin\:\:mainLoginForm\(\) \(void\) is used\.$#' identifier: staticMethod.void + count: 3 + path: packages/web/src/Pages/ProcessLogin.php + + - + message: '#^Strict comparison using \=\=\= between 0 and 0 will always evaluate to true\.$#' + identifier: identical.alwaysTrue count: 1 - path: packages/web/src/Managers/ScheduledTaskManager.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$host\.$#' - identifier: property.notFound + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ProcessLogin.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$mode\.$#' - identifier: property.notFound + message: '#^Constructor of class FOG\\Pages\\ReportManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ReportManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$passive\.$#' - identifier: property.notFound + message: '#^Constructor of class FOG\\Pages\\RoleManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/RoleManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$password\.$#' - identifier: property.notFound + message: '#^Method FOG\\Pages\\RoleManagement\:\:getSitesList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/RoleManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$port\.$#' - identifier: property.notFound + message: '#^Method FOG\\Pages\\RoleManagement\:\:getUserGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/RoleManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$timeout\.$#' - identifier: property.notFound + message: '#^Method FOG\\Pages\\RoleManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/RoleManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGFTP\:\:\$username\.$#' + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void + count: 3 + path: packages/web/src/Pages/RoleManagement.php + + - + message: '#^Access to an undefined property FOG\\Pages\\SchemaUpdaterPage\:\:\$schema\.$#' identifier: property.notFound + count: 4 + path: packages/web/src/Pages/SchemaUpdaterPage.php + + - + message: '#^Constant FOG_SCHEMA_INSTALL_TOKEN not found\.$#' + identifier: constant.notFound count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SchemaUpdaterPage.php - - message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:nlist\(\)\.$#' - identifier: method.notFound + message: '#^Left side of && is always true\.$#' + identifier: booleanAnd.leftAlwaysTrue count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SchemaUpdaterPage.php - - message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:pasv\(\)\.$#' - identifier: method.notFound + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SchemaUpdaterPage.php + + - + message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' + identifier: argument.type + count: 4 + path: packages/web/src/Pages/SchemaUpdaterPage.php - - message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:put\(\)\.$#' - identifier: method.notFound + message: '#^Path in include\(\) "/commons/schema\.php" is not a file or it does not exist\.$#' + identifier: include.fileNotFound count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SchemaUpdaterPage.php - - message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:rawlist\(\)\.$#' - identifier: method.notFound - count: 3 - path: packages/web/src/Net/FOGFTP.php + message: '#^Constructor of class FOG\\Pages\\ServerInfo has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter + count: 1 + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Call to an undefined method FOG\\Net\\FOGFTP\:\:rmdir\(\)\.$#' - identifier: method.notFound + message: '#^Parameter \#1 \$size of static method FOG\\Base\\FOGBase\:\:formatByteSize\(\) expects float\|int, string given\.$#' + identifier: argument.type count: 2 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Call to function is_object\(\) with resource will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Variable \$NICDro might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^Variable \$NICDropInfo might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:__set\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Variable \$NICErr might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:connect\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Variable \$NICErrInfo might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:rename\(\) has invalid return type FOG\\Net\\ftp_rename\.$#' - identifier: class.notFound + message: '#^Variable \$NICMac might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:rename\(\) should return FOG\\Net\\ftp_rename but returns \$this\(FOG\\Net\\FOGFTP\)\.$#' - identifier: return.type + message: '#^Variable \$NICRec might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) has invalid return type FOG\\Net\\ftp_size\.$#' - identifier: class.notFound + message: '#^Variable \$NICRecSized might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) should return FOG\\Net\\ftp_size but returns float\.$#' - identifier: return.type + message: '#^Variable \$NICTrans might not be defined\.$#' + identifier: variable.undefined + count: 2 + path: packages/web/src/Pages/ServerInfo.php + + - + message: '#^Variable \$NICTransSized might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServerInfo.php - - message: '#^Method FOG\\Net\\FOGFTP\:\:size\(\) should return FOG\\Net\\ftp_size but returns int\.$#' - identifier: return.type + message: '#^Constructor of class FOG\\Pages\\ServiceConfigurationPage has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServiceConfigurationPage.php - - message: '#^PHPDoc tag @return has invalid value \(\$this;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 172 on line 7$#' - identifier: phpDoc.parseError + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/ServiceConfigurationPage.php - - message: '#^PHPDoc tag @return has invalid value \(array;\)\: Unexpected token ";", expected TOKEN_HORIZONTAL_WS at offset 121 on line 6$#' - identifier: phpDoc.parseError + message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, int given\.$#' + identifier: argument.type + count: 4 + path: packages/web/src/Pages/ServiceConfigurationPage.php + + - + message: '#^Variable \$Module might not be defined\.$#' + identifier: variable.undefined + count: 4 + path: packages/web/src/Pages/ServiceConfigurationPage.php + + - + message: '#^Constructor of class FOG\\Pages\\SiteManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 2 - path: packages/web/src/Net/FOGFTP.php + message: '#^Method FOG\\Pages\\SiteManagement\:\:getGrantRolesList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void + count: 1 + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Parameter \#1 \$password of function password_hash expects string, int given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\SiteManagement\:\:getGrantUserGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Parameter \#1 \$password of function password_hash expects string, true given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\SiteManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Parameter \#2 \$mode of function ftp_chmod expects int, string given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\SiteManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\SiteManagement\:\:getUserGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Ternary operator condition is always true\.$#' - identifier: ternary.alwaysTrue + message: '#^Method FOG\\Pages\\SiteManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGFTP.php + path: packages/web/src/Pages/SiteManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$host\.$#' - identifier: property.notFound + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void + count: 6 + path: packages/web/src/Pages/SiteManagement.php + + - + message: '#^Method FOG\\Pages\\SnapinManagement\:\:_maker\(\) with return type void returns string\|false but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$password\.$#' - identifier: property.notFound + message: '#^Method FOG\\Pages\\SnapinManagement\:\:getHostsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$port\.$#' - identifier: property.notFound + message: '#^Method FOG\\Pages\\SnapinManagement\:\:getStoragegroupsList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGSSH\:\:\$username\.$#' - identifier: property.notFound + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:auth_password\(\)\.$#' - identifier: method.notFound + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:sftp_rmdir\(\)\.$#' - identifier: method.notFound - count: 2 - path: packages/web/src/Net/FOGSSH.php + message: '#^Result of method FOG\\Pages\\SnapinManagement\:\:_maker\(\) \(void\) is used\.$#' + identifier: method.void + count: 1 + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Call to an undefined method FOG\\Net\\FOGSSH\:\:sftp_unlink\(\)\.$#' - identifier: method.notFound - count: 2 - path: packages/web/src/Net/FOGSSH.php + message: '#^Static property FOG\\Base\\FOGBase\:\:\$selected \(bool\|int\) does not accept string\.$#' + identifier: assign.propertyType + count: 3 + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Call to function is_object\(\) with resource will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Static property FOG\\Pages\\SnapinManagement\:\:\$_template2 \(string\) does not accept null\.$#' + identifier: assign.propertyType count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^If condition is always true\.$#' - identifier: if.alwaysTrue + message: '#^Variable \$storagegroups might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/SnapinManagement.php - - message: '#^Left side of && is always true\.$#' - identifier: booleanAnd.leftAlwaysTrue + message: '#^Constructor of class FOG\\Pages\\StorageGroupManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^Method FOG\\Net\\FOGSSH\:\:__set\(\) with return type void returns mixed but should not return anything\.$#' + message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getImagesList\(\) with return type void returns mixed but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^Method FOG\\Net\\FOGSSH\:\:connect\(\) should return object but returns false\.$#' - identifier: return.type + message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getSnapinsList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 2 - path: packages/web/src/Net/FOGSSH.php + message: '#^Method FOG\\Pages\\StorageGroupManagement\:\:getStorageNodesList\(\) with return type void returns mixed but should not return anything\.$#' + identifier: return.void + count: 1 + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable - count: 3 - path: packages/web/src/Net/FOGSSH.php + message: '#^Variable \$StorageGroup might not be defined\.$#' + identifier: variable.undefined + count: 1 + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^Parameter \#1 \$password of function password_hash expects string, int given\.$#' - identifier: argument.type + message: '#^Variable \$storagenodes might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageGroupManagement.php - - message: '#^Parameter \#1 \$password of function password_hash expects string, true given\.$#' - identifier: argument.type + message: '#^Constructor of class FOG\\Pages\\StorageNodeManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Parameter \#2 \$return of function print_r expects bool, int given\.$#' - identifier: argument.type + message: '#^If condition is always false\.$#' + identifier: if.alwaysFalse count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Property FOG\\Net\\FOGSSH\:\:\$_link \(resource\) does not accept null\.$#' - identifier: assign.propertyType + message: '#^Method FOG\\Pages\\StorageNodeManagement\:\:storagenodeGeneralPost\(\) with return type void returns string but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Property FOG\\Net\\FOGSSH\:\:\$_sftp \(resource\) does not accept null\.$#' - identifier: assign.propertyType - count: 1 - path: packages/web/src/Net/FOGSSH.php + message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' + identifier: argument.type + count: 3 + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Property FOG\\Net\\FOGSSH\:\:\$_sftp \(resource\) in isset\(\) is not nullable\.$#' - identifier: isset.property + message: '#^Result of method FOG\\Pages\\StorageNodeManagement\:\:storagenodeGeneralPost\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Result of && is always false\.$#' - identifier: booleanAnd.alwaysFalse + message: '#^Variable \$StorageNode might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGSSH.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Access to an undefined property FOG\\Net\\FOGURLRequests\:\:\$headers\.$#' - identifier: property.notFound + message: '#^Variable \$warning might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/StorageNodeManagement.php - - message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Constructor of class FOG\\Pages\\TaskManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/TaskManagement.php - - message: '#^Default value of the parameter \#6 \$callback \(false\) of method FOG\\Net\\FOGURLRequests\:\:process\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Unreachable statement \- code above always terminates\.$#' + identifier: deadCode.unreachable count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/TaskManagement.php - - message: '#^Default value of the parameter \#7 \$file \(false\) of method FOG\\Net\\FOGURLRequests\:\:process\(\) is incompatible with type string\.$#' - identifier: parameter.defaultValue + message: '#^Variable \$columns might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/TaskManagement.php - - message: '#^Method FOG\\Net\\FOGURLRequests\:\:__construct\(\) has invalid return type FOG\\Base\\this\.$#' - identifier: class.notFound + message: '#^Constructor of class FOG\\Pages\\UserGroupManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserGroupManagement.php - - message: '#^Method FOG\\Net\\FOGURLRequests\:\:__set\(\) with return type void returns \$this\(FOG\\Net\\FOGURLRequests\) but should not return anything\.$#' + message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getRolesList\(\) with return type void returns null but should not return anything\.$#' identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserGroupManagement.php - - message: '#^Method FOG\\Net\\FOGURLRequests\:\:execute\(\) should return object but returns array\\.$#' - identifier: return.type + message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getSitesList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserGroupManagement.php - - message: '#^Method FOG\\Net\\FOGURLRequests\:\:process\(\) should return array but returns object\.$#' - identifier: return.type + message: '#^Method FOG\\Pages\\UserGroupManagement\:\:getUsersList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserGroupManagement.php - - message: '#^Parameter \#1 \$obj of function spl_object_id expects object, \(resource\|false\) given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Net/FOGURLRequests.php + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void + count: 3 + path: packages/web/src/Pages/UserGroupManagement.php - - message: '#^Parameter \#2 \$callback of function array_filter expects \(callable\(lowercase\-string\)\: bool\)\|null, ''strlen'' given\.$#' - identifier: argument.type + message: '#^Constructor of class FOG\\Pages\\UserManagement has an unused parameter \$name\.$#' + identifier: constructor.unusedParameter count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserManagement.php - - message: '#^Parameter \#2 \$mode of function stream_set_blocking expects bool, int given\.$#' - identifier: argument.type + message: '#^Method FOG\\Pages\\UserManagement\:\:__construct\(\) with return type void returns \$this\(FOG\\Pages\\UserManagement\) but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserManagement.php - - message: '#^Parameter &\$url by\-ref type of method FOG\\Net\\FOGURLRequests\:\:_validUrl\(\) expects string, string\|false given\.$#' - identifier: parameterByRef.type + message: '#^Method FOG\\Pages\\UserManagement\:\:getGroupsList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserManagement.php - - message: '#^Variable \$url in isset\(\) always exists and is not nullable\.$#' - identifier: isset.variable + message: '#^Method FOG\\Pages\\UserManagement\:\:getRolesList\(\) with return type void returns null but should not return anything\.$#' + identifier: return.void count: 1 - path: packages/web/src/Net/FOGURLRequests.php + path: packages/web/src/Pages/UserManagement.php - - message: '#^Call to function is_numeric\(\) with int\\|int\<1, max\> will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 7 - path: packages/web/src/Net/Ping.php + message: '#^Result of method FOG\\Base\\FOGPage\:\:assocItemsList\(\) \(void\) is used\.$#' + identifier: method.void + count: 2 + path: packages/web/src/Pages/UserManagement.php - - message: '#^Method FOG\\Net\\Ping\:\:execSend\(\) has invalid return type FOG\\Net\\error\.$#' - identifier: class.notFound + message: '#^Variable \$User might not be defined\.$#' + identifier: variable.undefined count: 1 - path: packages/web/src/Net/Ping.php - - - - message: '#^Method FOG\\Net\\Ping\:\:execSend\(\) should return FOG\\Net\\error but returns int\.$#' - identifier: return.type - count: 2 - path: packages/web/src/Net/Ping.php + path: packages/web/src/Pages/UserManagement.php - - message: '#^Method FOG\\Net\\Ping\:\:execute\(\) should return int but returns FOG\\Net\\error\.$#' - identifier: return.type + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Net/Ping.php + path: packages/web/src/Reports/File_Deleter.php - - message: '#^PHPDoc tag @throws with type FOG\\Net\\Exception is not subtype of Throwable$#' - identifier: throws.notThrowable + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Net/Ping.php + path: packages/web/src/Reports/History_Report.php - - message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Router/HTTPResponseCodes.php + path: packages/web/src/Reports/Hosts_And_Users.php - - message: '#^Call to function method_exists\(\) with ''Authorization'' and ''resolveApiPermission'' will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Router/OpenAPI.php + path: packages/web/src/Reports/Product_Keys.php - - message: '#^Call to function method_exists\(\) with ''Route'' and ''sensitiveFieldMap'' will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Router/OpenAPI.php + path: packages/web/src/Reports/Run_History.php - - message: '#^Call to function method_exists\(\) with ''Route'' and ''serverOwnedFields'' will always evaluate to false\.$#' - identifier: function.impossibleType + message: '#^Result of method FOG\\Base\\FOGPage\:\:render\(\) \(void\) is used\.$#' + identifier: method.void count: 1 - path: packages/web/src/Router/OpenAPI.php + path: packages/web/src/Reports/Snapin_List.php - - message: '#^Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class\-string\\|FOGManagerController, string given\.$#' - identifier: argument.type + message: '#^Call to function is_numeric\(\) with int will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType count: 1 - path: packages/web/src/Router/OpenAPI.php + path: packages/web/src/Router/HTTPResponseCodes.php - - message: '#^Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class\-string\\|Route, string given\.$#' + message: '#^Parameter \#1 \$objectOrClass of class ReflectionClass constructor expects class\-string\\|FOGManagerController, string given\.$#' identifier: argument.type count: 1 path: packages/web/src/Router/OpenAPI.php @@ -4458,30 +4182,12 @@ parameters: count: 1 path: packages/web/src/Router/Route.php - - - message: '#^Anonymous function has an unused use \$tmpcolumns\.$#' - identifier: closure.unusedUse - count: 5 - path: packages/web/src/Router/Route.php - - message: '#^Call to function is_array\(\) with array will always evaluate to true\.$#' identifier: function.alreadyNarrowedType count: 2 path: packages/web/src/Router/Route.php - - - message: '#^Call to function is_string\(\) with int will always evaluate to false\.$#' - identifier: function.impossibleType - count: 2 - path: packages/web/src/Router/Route.php - - - - message: '#^Default value of the parameter \#2 \$msg \(false\) of method FOG\\Router\\Route\:\:sendResponse\(\) is incompatible with type int\.$#' - identifier: parameter.defaultValue - count: 1 - path: packages/web/src/Router/Route.php - - message: '#^Default value of the parameter \#2 \$whereItems \(array\) of method FOG\\Router\\Route\:\:names\(\) is incompatible with type string\.$#' identifier: parameter.defaultValue @@ -4497,7 +4203,7 @@ parameters: - message: '#^If condition is always true\.$#' identifier: if.alwaysTrue - count: 2 + count: 1 path: packages/web/src/Router/Route.php - @@ -4519,20 +4225,14 @@ parameters: path: packages/web/src/Router/Route.php - - message: '#^Method FOG\\Router\\Route\:\:delete\(\) with return type void returns null but should not return anything\.$#' - identifier: return.void - count: 1 - path: packages/web/src/Router/Route.php - - - - message: '#^Method FOG\\Router\\Route\:\:deletemass\(\) with return type void returns mixed but should not return anything\.$#' - identifier: return.void + message: '#^Method FOG\\Router\\Route\:\:getsearchbody\(\) should return array but return statement is missing\.$#' + identifier: return.missing count: 1 path: packages/web/src/Router/Route.php - - message: '#^Method FOG\\Router\\Route\:\:getsearchbody\(\) should return array but return statement is missing\.$#' - identifier: return.missing + message: '#^Negated boolean expression is always false\.$#' + identifier: booleanNot.alwaysFalse count: 1 path: packages/web/src/Router/Route.php @@ -4560,24 +4260,6 @@ parameters: count: 1 path: packages/web/src/Router/Route.php - - - message: '#^Parameter \#2 \$msg of static method FOG\\Router\\HTTPResponseCodes\:\:breakHead\(\) expects string, int given\.$#' - identifier: argument.type - count: 1 - path: packages/web/src/Router/Route.php - - - - message: '#^Parameter \#2 \$msg of static method FOG\\Router\\Route\:\:sendResponse\(\) expects int, string given\.$#' - identifier: argument.type - count: 8 - path: packages/web/src/Router/Route.php - - - - message: '#^Parameter \#2 \$msg of static method FOG\\Router\\Route\:\:sendResponse\(\) expects int, string\|false given\.$#' - identifier: argument.type - count: 8 - path: packages/web/src/Router/Route.php - - message: '#^Parameter \#2 \$whereItems of static method FOG\\Router\\Route\:\:getIds\(\) expects array, false given\.$#' identifier: argument.type @@ -4596,24 +4278,6 @@ parameters: count: 1 path: packages/web/src/Router/Route.php - - - message: '#^Result of static method FOG\\Router\\Route\:\:deletemass\(\) \(void\) is used\.$#' - identifier: staticMethod.void - count: 1 - path: packages/web/src/Router/Route.php - - - - message: '#^Strict comparison using \!\=\= between '''' and \*NEVER\* will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: packages/web/src/Router/Route.php - - - - message: '#^Strict comparison using \!\=\= between \*NEVER\* and '''' will always evaluate to true\.$#' - identifier: notIdentical.alwaysTrue - count: 1 - path: packages/web/src/Router/Route.php - - message: '#^Strict comparison using \!\=\= between null and object will always evaluate to true\.$#' identifier: notIdentical.alwaysTrue @@ -4629,17 +4293,11 @@ parameters: - message: '#^Unreachable statement \- code above always terminates\.$#' identifier: deadCode.unreachable - count: 2 - path: packages/web/src/Router/Route.php - - - - message: '#^Variable \$id might not be defined\.$#' - identifier: variable.undefined count: 1 path: packages/web/src/Router/Route.php - - message: '#^Variable \$pass_vars might not be defined\.$#' + message: '#^Variable \$id might not be defined\.$#' identifier: variable.undefined count: 1 path: packages/web/src/Router/Route.php @@ -4812,12 +4470,6 @@ parameters: count: 1 path: packages/web/src/Service/FileDeleter.php - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: packages/web/src/Service/FileDeleter.php - - message: '#^PHPDoc tag @throws with type FOG\\Service\\Exception is not subtype of Throwable$#' identifier: throws.notThrowable @@ -4992,12 +4644,6 @@ parameters: count: 1 path: packages/web/src/Service/PingHosts.php - - - message: '#^Right side of && is always true\.$#' - identifier: booleanAnd.rightAlwaysTrue - count: 1 - path: packages/web/src/Service/PingHosts.php - - message: '#^Static property FOG\\Service\\PingHosts\:\:\$_pingOn \(int\) does not accept array\|string\.$#' identifier: assign.propertyType @@ -5022,18 +4668,6 @@ parameters: count: 1 path: packages/web/src/Service/TaskScheduler.php - - - message: '#^Call to method set\(\) on an unknown class FOG\\TaskHandling\\Task\.$#' - identifier: class.notFound - count: 1 - path: packages/web/src/TaskHandling/TaskError.php - - - - message: '#^Parameter \$Task of method FOG\\TaskHandling\\TaskError\:\:_markFailed\(\) has invalid type FOG\\TaskHandling\\Task\.$#' - identifier: class.notFound - count: 1 - path: packages/web/src/TaskHandling/TaskError.php - - message: ''' #^Array has 2 duplicate keys with value ' @@ -5049,12 +4683,6 @@ parameters: count: 1 path: packages/web/src/TaskHandling/TaskQueue.php - - - message: '#^Negated boolean expression is always false\.$#' - identifier: booleanNot.alwaysFalse - count: 1 - path: packages/web/src/TaskHandling/TaskQueue.php - - message: '#^Negated boolean expression is always true\.$#' identifier: booleanNot.alwaysTrue diff --git a/phpstan.neon b/phpstan.neon index 694ee42977..54172fc857 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -120,5 +120,14 @@ parameters: # The constants FOG defines at runtime -- see the file's own header. - build/phpstan/constants.stub.php +services: + # Resolves getClass('Name') to its FOG class so a method that does not + # exist on the result is a finding, not a fatal on a live server. See + # the extension's header for the failure it closes. + - + class: FOG\Build\PhpStan\GetClassReturnTypeExtension + tags: + - phpstan.broker.dynamicStaticMethodReturnTypeExtension + includes: - phpstan-baseline.neon diff --git a/tests/agent-principal.test.php b/tests/agent-principal.test.php new file mode 100644 index 0000000000..48a25efb5a --- /dev/null +++ b/tests/agent-principal.test.php @@ -0,0 +1,129 @@ + agent CA -> leaf chain with the + * openssl CLI under a temp dir and removes it after. + * + * Usage: php tests/agent-principal.test.php + * Exit status 0 = pass, 1 = fail. + */ + +use FOG\Agent\Principal; + +$root = dirname(__DIR__); +require_once $root . '/packages/web/src/Agent/Principal.php'; + +$failures = 0; +$checks = 0; +$check = function ($name, $expected, $actual) use (&$failures, &$checks) { + $checks++; + if ($expected === $actual) { + return; + } + $failures++; + fwrite(STDERR, sprintf(" FAIL %s\n expected %s\n got %s\n", $name, var_export($expected, true), var_export($actual, true))); +}; + +$dir = sys_get_temp_dir() . '/fog-agent-principal-' . getmypid(); +mkdir($dir, 0700); +$run = function ($cmd) use ($dir) { + $out = []; + $rc = 0; + exec(sprintf('cd %s && %s 2>&1', escapeshellarg($dir), $cmd), $out, $rc); + if (0 !== $rc) { + fwrite(STDERR, "openssl failed: $cmd\n" . implode("\n", $out) . "\n"); + exit(1); + } +}; +file_put_contents($dir . '/ext.cnf', implode("\n", [ + '[ca]', + 'basicConstraints = critical, CA:TRUE, pathlen:0', + 'keyUsage = critical, keyCertSign, cRLSign', + 'subjectKeyIdentifier = hash', + 'authorityKeyIdentifier = keyid:always', + '[client]', + 'basicConstraints = CA:FALSE', + 'keyUsage = digitalSignature', + 'extendedKeyUsage = clientAuth', + '[server]', + 'basicConstraints = CA:FALSE', + 'keyUsage = digitalSignature', + 'extendedKeyUsage = serverAuth', + '', +])); +// Two roots: ours, and one the vhost might also trust. +foreach (['root', 'rogue'] as $name) { + $run("openssl ecparam -name prime256v1 -genkey -noout -out $name.key"); + $run("openssl req -x509 -new -key $name.key -sha256 -days 2 -subj '/CN=$name' -out $name.pem"); +} +// Our agent CA under our root; a rogue "agent CA" under the rogue root. +foreach (['root' => 'agentca', 'rogue' => 'rogueca'] as $issuer => $name) { + $run("openssl ecparam -name prime256v1 -genkey -noout -out $name.key"); + $run("openssl req -new -key $name.key -subj '/CN=$name' -out $name.csr"); + $run("openssl x509 -req -in $name.csr -CA $issuer.pem -CAkey $issuer.key -CAcreateserial -days 2 -sha256 -extfile ext.cnf -extensions ca -out $name.pem"); +} +// Leaves: a proper agent, one from the rogue CA, one from our CA but for a server. +foreach (['good' => ['agentca', 'client'], 'rogue' => ['rogueca', 'client'], 'server' => ['agentca', 'server']] as $name => list($ca, $ext)) { + $run("openssl ecparam -name prime256v1 -genkey -noout -out $name.key"); + $run("openssl req -new -key $name.key -subj '/CN=fog-agent host 1' -out $name.csr"); + $run("openssl x509 -req -in $name.csr -CA $ca.pem -CAkey $ca.key -CAcreateserial -days 2 -sha256 -extfile ext.cnf -extensions $ext -out $name.pem"); +} +$run('cat agentca.pem root.pem > bundle.pem'); +$run('openssl pkey -in good.key -pubout -out good.pub'); + +$bundle = $dir . '/bundle.pem'; +$good = file_get_contents($dir . '/good.pem'); +$expectedFp = hash('sha256', file_get_contents($dir . '/good.pub')); + +$verified = Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => $good], $bundle); +$check('good chain, plain PEM (Apache): fingerprint is sha256 of the SPKI PEM', $expectedFp, $verified['fingerprint'] ?? null); +$check('good chain: not_after is a future unix time', true, ($verified['not_after'] ?? 0) > time()); + +$escaped = Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => rawurlencode($good)], $bundle); +$check('good chain, URL-escaped PEM (nginx): same fingerprint', $expectedFp, $escaped['fingerprint'] ?? null); + +$check('server said NONE: refused before cryptography', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'NONE', 'SSL_CLIENT_CERT' => $good], $bundle)); +$check('server said nothing at all: refused', null, Principal::verify([], $bundle)); +$check('SUCCESS with no certificate: refused', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS'], $bundle)); + +$rogue = file_get_contents($dir . '/rogue.pem'); +$check('SUCCESS but issued under a CA that is not ours: refused', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => $rogue], $bundle)); + +$server = file_get_contents($dir . '/server.pem'); +$check('SUCCESS, our CA, but a serverAuth certificate: refused', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => $server], $bundle)); + +$check('garbage where the PEM should be: refused', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => 'not a certificate'], $bundle)); +$check('bundle missing on this server (a storage node): refused', null, Principal::verify(['SSL_CLIENT_VERIFY' => 'SUCCESS', 'SSL_CLIENT_CERT' => $good], $dir . '/absent.pem')); + +foreach (glob($dir . '/*') as $f) { + unlink($f); +} +rmdir($dir); + +if ($failures > 0) { + fwrite(STDERR, "FAIL: agent-principal ($failures of $checks checks)\n"); + exit(1); +} +echo "PASS agent-principal ($checks checks)\n"; diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index 3769cb4792..bdf4c5a742 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -92,11 +92,15 @@ host 35 hostExitBios biosexit - - host 36 hostExitEfi efiexit - - host 37 hostEnforce enforce - - host 38 hostInfoLock tokenlock - - -host 39 imageName imagename - - -host 40 - groups f (none) -host 41 - groups_list f - -host 42 hmMAC primac - - -host 43 hmMAC primac_vendor f (none) +host 39 hostAgentFingerprint agentFingerprint - - +host 40 hostAgentNotAfter agentNotAfter - - +host 41 hostAgentVersion agentVersion - - +host 42 hostAgentCheckin agentCheckin - - +host 43 imageName imagename - - +host 44 - groups f (none) +host 45 - groups_list f - +host 46 hmMAC primac - - +host 47 hmMAC primac_vendor f (none) hostautologout 0 haloID id - - hostautologout 1 haloID DT_RowId f - hostautologout 2 haloHostID hostID - - diff --git a/tests/foreign-key-map.test.php b/tests/foreign-key-map.test.php index 8577abc627..29fcaa2f66 100644 --- a/tests/foreign-key-map.test.php +++ b/tests/foreign-key-map.test.php @@ -356,6 +356,7 @@ // an orphan schedule left against a reused group id would silently // start shutting down every host that inherited the number. 'groupPowerManagement.gpmGroupID', + 'agentEnrollment.aeHostID', // Plugin groups, named for the plugin rather than numbered. Each // lands in that plugin's own repo, in an appended step of its // manager's schema(); see fog-plugins tests/foreign-keys.test.php, From 6c81701b9f870a53830df7927920f7835fb8f15c Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 14:09:34 +0000 Subject: [PATCH 002/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e430ff298d..b8f05f88f2 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -9904,6 +9904,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index fe570c2e1c..cb6cd9a813 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -9913,6 +9913,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index f29a312e24..1311bf2dd0 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10074,6 +10074,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index db4d2d09a5..3d287c0b1e 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -9905,6 +9905,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 7957662da1..34b656c34d 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -9898,6 +9898,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 62a8cf48b4..5f85db4939 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9618,6 +9618,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 292c2b7afb..ccf5b1f62c 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9565,6 +9565,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 855bca1e94..f7ba7b6d00 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8488,6 +8488,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index ef8b00d40d..fd7c0c9bfe 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -9900,6 +9900,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index c939f3a128..02b62f2b49 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -9900,6 +9900,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 7acf137ec10b78ef3780b3589574cb54f19dda0d Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 09:22:39 -0500 Subject: [PATCH 003/117] Pending Agents page, and two gates the first commit missed Hosts > Pending Agents: the admin side of fog-agent enrollment, sibling of Pending Hosts and Pending MACs and built on the Pending MACs shape (HostManagement::pendingAgents / pendingAgentsAjax / getPendingAgentList, fog.host.pendingAgents.js). Select rows, Approve or Deny with a confirm modal; each decision runs through FOG\Agent\Enrollment, the same code the JSON route uses. The dashboard gets a "Pending agents" alert beside the pending hosts and MACs ones. The grid is client-side over the same whitelisted payload GET /agent/enrollments serves, not Route::listem(): agentenrollment is deliberately not an API class, since every row carries a CSR and, once approved, a certificate. The list is bounded by what an admin has not yet looked at, never by the fleet. Permissions fall out of Authorization::_subToAction unchanged: the page is host.view, the POST is host.edit, the list source is host.view. Also: the first commit left two suite gates red that the earlier run did not cover. psr4-scan now places Agent\Enrollment and Agent\Principal (both extend FOGBase directly, so ancestry cannot), and all-classes-load skips build/, which is PHPStan tooling loaded by the root composer autoload-dev and implements interfaces FOG's own autoloader has no way to declare. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- bin/psr4-scan.php | 7 + .../js/fog/host/fog.host.pendingAgents.js | 129 ++++++++++++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../en_US.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../es_ES.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../it_IT.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 82 +++++++- .../web/management/languages/messages.pot | 64 ++++++ .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 78 +++++++ packages/web/src/Base/FOGPage.php | 6 + packages/web/src/Pages/DashboardPage.php | 27 +++ packages/web/src/Pages/HostManagement.php | 198 ++++++++++++++++++ tests/all-classes-load.test.php | 7 +- 16 files changed, 1140 insertions(+), 4 deletions(-) create mode 100644 packages/web/management/js/fog/host/fog.host.pendingAgents.js diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 0646248589..016ba1c0f7 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -200,6 +200,13 @@ // and the task-completion report. Util is for things belonging to no // subsystem at all. 'SecureBootState' => 'Boot', + // Agent, not Boot: fog-agent is the management client, not the netboot + // path. Both extend FOGBase directly -- Enrollment is the policy for who + // gets a client certificate, Principal is the pure verifier that turns a + // presented certificate back into a host -- so ancestry cannot place + // them, and they are one subsystem the way the Boot classes are. + 'Enrollment' => 'Agent', + 'Principal' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/packages/web/management/js/fog/host/fog.host.pendingAgents.js b/packages/web/management/js/fog/host/fog.host.pendingAgents.js new file mode 100644 index 0000000000..e149f4ab43 --- /dev/null +++ b/packages/web/management/js/fog/host/fog.host.pendingAgents.js @@ -0,0 +1,129 @@ +(function($) { + // Approve + var approveSelected = $('#approve'), + approveModal = $('#approveModal'), + confirmApprove = $('#confirmApproveModal'), + // Deny + denySelected = $('#deny'), + denyModal = $('#denyModal'), + confirmDeny = $('#confirmDenyModal'), + // Form to work with. + pendingForm = $('#agent-pending-form'), + method = pendingForm.attr('method'), + action = pendingForm.attr('action'); + + function disableButtons (disable) { + approveSelected.prop('disabled', disable); + denySelected.prop('disabled', disable); + } + function onSelect (selected) { + var disabled = selected.count() == 0; + disableButtons(disabled); + } + function esc (s) { + return $('
').text(s == null ? '' : String(s)).html(); + } + + disableButtons(true); + // Client-side table: the rows come from the same whitelisted payload + // the admin JSON route serves (see HostManagement::getPendingAgentList), + // fetched once and paged in the browser. There is no server-side + // listem() for this class on purpose -- its rows carry key material. + var table = $('#dataTable').registerTable(onSelect, { + order: [ + [6, 'desc'] + ], + columns: [ + {data: 'hostname'}, + {data: 'reason'}, + {data: 'os'}, + {data: 'agentVersion'}, + {data: 'remoteIP'}, + {data: 'identity'}, + {data: 'created'} + ], + columnDefs: [ + { + // A request bound to an existing host links to it; an + // unknown machine shows only the name it reported. + render: function (data, type, row) { + if (type !== 'display') { + return data; + } + if (row.hostID > 0) { + return '' + esc(data) + ''; + } + return esc(data); + }, + targets: 0 + }, + { + render: function (data, type, row) { + return esc(data) + (row.arch ? '/' + esc(row.arch) : ''); + }, + targets: 2 + }, + { + // What the machine said it is. The serial is what an admin + // can check against a label; the UUID is what the server + // matched on. + render: function (data, type) { + var id = data || {}; + if (type !== 'display') { + return (id.system_serial || '') + ' ' + (id.system_uuid || ''); + } + var parts = []; + if (id.system_serial) { + parts.push(esc(id.system_serial)); + } + if (id.system_uuid) { + parts.push('' + esc(id.system_uuid) + ''); + } + return parts.join('
'); + }, + targets: 5 + } + ], + rowId: 'id', + processing: true, + serverSide: false, + ajax: { + url: '../management/index.php?node=' + + Common.node + + '&sub=getPendingAgentList', + type: 'post' + } + }); + + if (Common.search && Common.search.length > 0) { + table.search(Common.search).draw(); + } + + function decide (which, modal, button) { + disableButtons(true); + var opts = {pending: $.getSelectedIds(table)}; + opts[which] = 1; + $.apiCall(method, action, opts, function(err) { + modal.modal('hide'); + disableButtons(false); + // Redraw whatever happened: a partial failure has still + // decided some rows, and the error toast names the rest. + table.ajax.reload(null, false); + }); + button.prop('disabled', false); + } + + approveSelected.on('click', function() { + approveModal.modal('show'); + }); + confirmApprove.on('click', function() { + decide('approvepending', approveModal, confirmApprove); + }); + denySelected.on('click', function() { + denyModal.modal('show'); + }); + confirmDeny.on('click', function() { + decide('denypending', denyModal, confirmDeny); + }); +})(jQuery); diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index b8f05f88f2..bf7d9ce6e6 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -120,6 +120,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -415,6 +419,9 @@ msgstr "Ein Broadcast mit diesem Namen ist bereits vorhanden!" msgid "A client ID is required" msgstr "Ein Gruppenname ist erforderlich!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Schlüsselfeldname muss eine Zeichenfolge sein." @@ -1012,6 +1019,17 @@ msgstr "Erweitert" msgid "Advanced Tasks" msgstr "Erweitert" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "Host erfolgreich erstellt" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Drucker aktualisiert!" + msgid "Ago must be boolean" msgstr "Ago muss boolean sein" @@ -1028,6 +1046,10 @@ msgstr "" msgid "All Hosts" msgstr "Alle Hosts" +#, fuzzy +msgid "All Pending Agents" +msgstr "Ausstehende MACs" + #, fuzzy msgid "All Pending Hosts" msgstr "Ausstehende Hosts" @@ -1215,10 +1237,18 @@ msgstr "" msgid "Approve" msgstr "freigeben" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "freigeben" + #, fuzzy msgid "Approve MAC Fail" msgstr "freigeben" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Ausstehende Hosts" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Ausstehende Hosts" @@ -1233,6 +1263,10 @@ msgstr "Ausgewählten MAcs freigeben" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Ausgewählten MAcs freigeben" + #, fuzzy msgid "Approved selected hosts!" msgstr "Ausgewählten MAcs freigeben" @@ -2569,6 +2603,25 @@ msgstr "Remotedatei wird gelöscht" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Ausgewählten MAcs freigeben" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Löschen fehlgeschlagen" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "Ausstehende MACs" + +#, fuzzy +msgid "Deny selected" +msgstr "Ausgewählte löschen" + msgid "Deploy" msgstr "Verteilung" @@ -2752,6 +2805,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Bearbeiten" @@ -4250,6 +4306,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "Server-Shell" + msgid "Identity Provider" msgstr "" @@ -6889,6 +6949,10 @@ msgstr "" msgid "Pending" msgstr "Ausstehend..." +#, fuzzy +msgid "Pending Agents" +msgstr "Ausstehende MACs" + msgid "Pending Hosts" msgstr "Ausstehende Hosts" @@ -6913,10 +6977,18 @@ msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "Ausstehende Hosts" + #, fuzzy msgid "Pending agent enrollments" msgstr "Ausstehende registrierte Hosts" +#, fuzzy +msgid "Pending agents" +msgstr "Ausstehende MACs" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6969,6 +7041,9 @@ msgstr "Status" msgid "Ping cycle complete" msgstr "wurde abgeschlossen" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Bitte wählen Sie eine Option" @@ -7541,6 +7616,9 @@ msgstr "Ausgewählte löschen" msgid "Real Time" msgstr "Datum und Zeit" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Neustarten" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index cb6cd9a813..2f9571aaba 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -125,6 +125,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -420,6 +424,9 @@ msgstr "An image already exists with this name!" msgid "A client ID is required" msgstr "An image name is required!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Event must be a string" @@ -1016,6 +1023,17 @@ msgstr "Advanced" msgid "Advanced Tasks" msgstr "Advanced" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "Host Created" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Printer updated!" + msgid "Ago must be boolean" msgstr "" @@ -1032,6 +1050,10 @@ msgstr "" msgid "All Hosts" msgstr "All Hosts" +#, fuzzy +msgid "All Pending Agents" +msgstr "Pending MACs" + #, fuzzy msgid "All Pending Hosts" msgstr "Pending Hosts" @@ -1219,10 +1241,18 @@ msgstr "" msgid "Approve" msgstr "Host approved" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "Host approved" + #, fuzzy msgid "Approve MAC Fail" msgstr "Host approved" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Pending Hosts" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Pending Hosts" @@ -1237,6 +1267,10 @@ msgstr "Approve selected Hosts" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Approve selected Hosts" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approve selected Hosts" @@ -2572,6 +2606,25 @@ msgstr "Menu create failed" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Approve selected Hosts" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Delete file data" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "Pending MACs" + +#, fuzzy +msgid "Deny selected" +msgstr "Delete Selected" + msgid "Deploy" msgstr "Deploy" @@ -2755,6 +2808,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Edit" @@ -4252,6 +4308,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "Server Shell" + msgid "Identity Provider" msgstr "" @@ -6900,6 +6960,10 @@ msgstr "" msgid "Pending" msgstr "Pending..." +#, fuzzy +msgid "Pending Agents" +msgstr "Pending MACs" + msgid "Pending Hosts" msgstr "Pending Hosts" @@ -6924,10 +6988,18 @@ msgstr "Pending Registration created by FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "Pending Registration created by FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "Pending hosts" + #, fuzzy msgid "Pending agent enrollments" msgstr "Pending Registered Hosts" +#, fuzzy +msgid "Pending agents" +msgstr "Pending macs" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6980,6 +7052,9 @@ msgstr "Status" msgid "Ping cycle complete" msgstr "has been destroyed" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Please Select an option" @@ -7552,6 +7627,9 @@ msgstr "Delete Selected" msgid "Real Time" msgstr "Host Update Failed" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Reboot" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 1311bf2dd0..750405ba28 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -123,6 +123,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -418,6 +422,9 @@ msgstr "Una imagen ya existe con este nombre!" msgid "A client ID is required" msgstr "Se requiere un nombre de imagen!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Evento debe ser una cadena" @@ -1028,6 +1035,17 @@ msgstr "Avanzado" msgid "Advanced Tasks" msgstr "Avanzado" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "Creado" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Impresora actualiza!" + msgid "Ago must be boolean" msgstr "" @@ -1045,6 +1063,10 @@ msgstr "" msgid "All Hosts" msgstr "Hospedadores" +#, fuzzy +msgid "All Pending Agents" +msgstr "macs pendientes" + #, fuzzy msgid "All Pending Hosts" msgstr "anfitriones pendientes" @@ -1233,10 +1255,18 @@ msgstr "" msgid "Approve" msgstr "Creado" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "Creado" + #, fuzzy msgid "Approve MAC Fail" msgstr "Creado" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "anfitriones pendientes" + #, fuzzy msgid "Approve Pending Hosts" msgstr "anfitriones pendientes" @@ -1251,6 +1281,10 @@ msgstr "retirar" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "retirar" + #, fuzzy msgid "Approved selected hosts!" msgstr "retirar" @@ -2598,6 +2632,25 @@ msgstr "Menú Error de creación" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "retirar" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Eliminar los datos del archivo" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "macs pendientes" + +#, fuzzy +msgid "Deny selected" +msgstr "Eliminar seleccionado" + #, fuzzy msgid "Deploy" msgstr "última Desplegado" @@ -2787,6 +2840,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Editar" @@ -4320,6 +4376,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "servidor TFTP" + msgid "Identity Provider" msgstr "" @@ -7012,6 +7072,10 @@ msgstr "" msgid "Pending" msgstr "macs pendientes" +#, fuzzy +msgid "Pending Agents" +msgstr "macs pendientes" + #, fuzzy msgid "Pending Hosts" msgstr "anfitriones pendientes" @@ -7038,10 +7102,18 @@ msgstr "" msgid "Pending Registration created by FOG_CLIENT" msgstr "" +#, fuzzy +msgid "Pending agent" +msgstr "anfitriones pendientes" + #, fuzzy msgid "Pending agent enrollments" msgstr "anfitriones pendientes" +#, fuzzy +msgid "Pending agents" +msgstr "macs pendientes" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -7095,6 +7167,9 @@ msgstr "Estado" msgid "Ping cycle complete" msgstr "Deben estar cifrados" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Por favor seleccione una opción" @@ -7674,6 +7749,9 @@ msgstr "Eliminar seleccionado" msgid "Real Time" msgstr "Tiempo de actividad del sistema" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Reiniciar" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 3d287c0b1e..e5bef961d6 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -120,6 +120,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -415,6 +419,9 @@ msgstr "Ein Broadcast mit diesem Namen ist bereits vorhanden!" msgid "A client ID is required" msgstr "Ein Gruppenname ist erforderlich!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Schlüsselfeldname muss eine Zeichenfolge sein." @@ -1012,6 +1019,17 @@ msgstr "Erweitert" msgid "Advanced Tasks" msgstr "Erweitert" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "Host erfolgreich erstellt" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Drucker aktualisiert!" + msgid "Ago must be boolean" msgstr "Ago muss boolean sein" @@ -1028,6 +1046,10 @@ msgstr "" msgid "All Hosts" msgstr "Alle Hosts" +#, fuzzy +msgid "All Pending Agents" +msgstr "Ausstehende MACs" + #, fuzzy msgid "All Pending Hosts" msgstr "Ausstehende Hosts" @@ -1215,10 +1237,18 @@ msgstr "" msgid "Approve" msgstr "freigeben" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "freigeben" + #, fuzzy msgid "Approve MAC Fail" msgstr "freigeben" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Ausstehende Hosts" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Ausstehende Hosts" @@ -1233,6 +1263,10 @@ msgstr "Ausgewählten MAcs freigeben" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Ausgewählten MAcs freigeben" + #, fuzzy msgid "Approved selected hosts!" msgstr "Ausgewählten MAcs freigeben" @@ -2569,6 +2603,25 @@ msgstr "Remotedatei wird gelöscht" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Ausgewählten MAcs freigeben" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Löschen fehlgeschlagen" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "Ausstehende MACs" + +#, fuzzy +msgid "Deny selected" +msgstr "Ausgewählte löschen" + msgid "Deploy" msgstr "Verteilung" @@ -2752,6 +2805,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Bearbeiten" @@ -4250,6 +4306,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "Server-Shell" + msgid "Identity Provider" msgstr "" @@ -6890,6 +6950,10 @@ msgstr "" msgid "Pending" msgstr "Ausstehend..." +#, fuzzy +msgid "Pending Agents" +msgstr "Ausstehende MACs" + msgid "Pending Hosts" msgstr "Ausstehende Hosts" @@ -6914,10 +6978,18 @@ msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "Ausstehende Registrierung erstellt von FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "Ausstehende Hosts" + #, fuzzy msgid "Pending agent enrollments" msgstr "Ausstehende registrierte Hosts" +#, fuzzy +msgid "Pending agents" +msgstr "Ausstehende MACs" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6970,6 +7042,9 @@ msgstr "Status" msgid "Ping cycle complete" msgstr "wurde abgeschlossen" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Bitte wählen Sie eine Option" @@ -7542,6 +7617,9 @@ msgstr "Ausgewählte löschen" msgid "Real Time" msgstr "Datum und Zeit" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Neustarten" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 34b656c34d..6f9006a6bc 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -126,6 +126,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -421,6 +425,9 @@ msgstr "Une image existe déjà avec ce nom!" msgid "A client ID is required" msgstr "Un nom de l'image est nécessaire!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Événement doit être une chaîne" @@ -1017,6 +1024,17 @@ msgstr "Avancée" msgid "Advanced Tasks" msgstr "Avancée" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "hôte Créé" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Imprimante mis à jour!" + msgid "Ago must be boolean" msgstr "" @@ -1033,6 +1051,10 @@ msgstr "" msgid "All Hosts" msgstr "Tous les hôtes" +#, fuzzy +msgid "All Pending Agents" +msgstr "en attente MACs" + #, fuzzy msgid "All Pending Hosts" msgstr "Les hôtes en attente" @@ -1220,10 +1242,18 @@ msgstr "" msgid "Approve" msgstr "hôte approuvé" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "hôte approuvé" + #, fuzzy msgid "Approve MAC Fail" msgstr "hôte approuvé" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Les hôtes en attente" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Les hôtes en attente" @@ -1238,6 +1268,10 @@ msgstr "Approuver hôtes sélectionnés" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Approuver hôtes sélectionnés" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approuver hôtes sélectionnés" @@ -2573,6 +2607,25 @@ msgstr "Menu create a échoué" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Approuver hôtes sélectionnés" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Supprimer les données de fichiers" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "en attente MACs" + +#, fuzzy +msgid "Deny selected" +msgstr "Supprimer sélectionnée" + msgid "Deploy" msgstr "Déployer" @@ -2756,6 +2809,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "modifier" @@ -4253,6 +4309,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "serveur Shell" + msgid "Identity Provider" msgstr "" @@ -6887,6 +6947,10 @@ msgstr "" msgid "Pending" msgstr "En attendant..." +#, fuzzy +msgid "Pending Agents" +msgstr "en attente MACs" + msgid "Pending Hosts" msgstr "Les hôtes en attente" @@ -6911,10 +6975,18 @@ msgstr "Inscription en attente créée par FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "Inscription en attente créée par FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "hôtes en attente" + #, fuzzy msgid "Pending agent enrollments" msgstr "Dans l'attente des hôtes enregistrés" +#, fuzzy +msgid "Pending agents" +msgstr "macs en attente" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6967,6 +7039,9 @@ msgstr "statut" msgid "Ping cycle complete" msgstr "a été détruit" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Veuillez sélectionner une option" @@ -7539,6 +7614,9 @@ msgstr "Supprimer sélectionnée" msgid "Real Time" msgstr "Mise à jour de l'hôte a échoué" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Réinitialiser" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5f85db4939..ecbf274aea 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -125,6 +125,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -417,6 +421,9 @@ msgstr "Un broadcast esiste già con questo nome!" msgid "A client ID is required" msgstr "È richiesto un nome di gruppo!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Campo chiave deve essere una stringa" @@ -990,6 +997,17 @@ msgstr "Avanzate" msgid "Advanced Tasks" msgstr "Avanzate" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "Creazione Host con successo" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Aggiornamento stampante riuscito" + msgid "Ago must be boolean" msgstr "Fa deve essere boolean" @@ -1006,6 +1024,10 @@ msgstr "" msgid "All Hosts" msgstr "tutti gli host" +#, fuzzy +msgid "All Pending Agents" +msgstr "sospeso MAC" + #, fuzzy msgid "All Pending Hosts" msgstr "Host in sospeso" @@ -1189,10 +1211,18 @@ msgstr "" msgid "Approve" msgstr "Approva" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "Approva" + #, fuzzy msgid "Approve MAC Fail" msgstr "Approva" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Host in sospeso" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Host in sospeso" @@ -1207,6 +1237,10 @@ msgstr "Approvare MAC selezionati" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Approvare MAC selezionati" + #, fuzzy msgid "Approved selected hosts!" msgstr "Approvare MAC selezionati" @@ -2507,6 +2541,25 @@ msgstr "Eliminazione di file remoti" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Approvare MAC selezionati" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Cancellare i file" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "sospeso MAC" + +#, fuzzy +msgid "Deny selected" +msgstr "Cancella selezionato" + msgid "Deploy" msgstr "Distribuisci" @@ -2688,6 +2741,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Modifica" @@ -4146,6 +4202,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "Server Shell" + msgid "Identity Provider" msgstr "" @@ -6693,6 +6753,10 @@ msgstr "" msgid "Pending" msgstr "In attesa di..." +#, fuzzy +msgid "Pending Agents" +msgstr "sospeso MAC" + msgid "Pending Hosts" msgstr "Host in sospeso" @@ -6717,10 +6781,18 @@ msgstr "In attesa di registrazione creato da FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "In attesa di registrazione creato da FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "host in sospeso" + #, fuzzy msgid "Pending agent enrollments" msgstr "In attesa di host registrati" +#, fuzzy +msgid "Pending agents" +msgstr "Mac in attesa" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6773,6 +6845,9 @@ msgstr "Stato" msgid "Ping cycle complete" msgstr "è stato completato" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Per favore selezionate un'opzione" @@ -7332,6 +7407,9 @@ msgstr "Cancella selezionato" msgid "Real Time" msgstr "Data e Ora" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "Riavvio" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index ccf5b1f62c..c5203a365d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -116,6 +116,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -408,6 +412,9 @@ msgstr "この名前のブロードキャストは既に存在します!" msgid "A client ID is required" msgstr "名前は必須です!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "名前を設定する必要があります" @@ -971,6 +978,17 @@ msgstr "詳細" msgid "Advanced Tasks" msgstr "詳細" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "承認に成功しました" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "プラグインをインストールしました!" + msgid "Ago must be boolean" msgstr "Ago はブール値である必要があります" @@ -988,6 +1006,10 @@ msgstr "メニュー項目を削除" msgid "All Hosts" msgstr "すべてのホスト" +#, fuzzy +msgid "All Pending Agents" +msgstr "保留中 MAC アドレス" + #, fuzzy msgid "All Pending Hosts" msgstr "保留中ホスト" @@ -1173,10 +1195,18 @@ msgstr "" msgid "Approve" msgstr "承認" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "MAC アドレスを承認" + #, fuzzy msgid "Approve MAC Fail" msgstr "MAC アドレスを承認" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "保留中ホスト" + #, fuzzy msgid "Approve Pending Hosts" msgstr "保留中ホスト" @@ -1191,6 +1221,10 @@ msgstr "選択したホストを承認" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "選択したホストを承認" + #, fuzzy msgid "Approved selected hosts!" msgstr "選択したホストを承認" @@ -2492,6 +2526,25 @@ msgstr "リモートファイルを削除しています" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "選択したホストを承認" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "削除に失敗しました" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "保留中 MAC アドレス" + +#, fuzzy +msgid "Deny selected" +msgstr "選択項目を削除" + msgid "Deploy" msgstr "展開" @@ -2673,6 +2726,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "編集" @@ -4118,6 +4174,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "サーバーシェル" + msgid "Identity Provider" msgstr "" @@ -6664,6 +6724,10 @@ msgstr "" msgid "Pending" msgstr "保留中..." +#, fuzzy +msgid "Pending Agents" +msgstr "保留中 MAC アドレス" + msgid "Pending Hosts" msgstr "保留中ホスト" @@ -6688,10 +6752,18 @@ msgstr "FOG_CLIENT により保留中の登録が作成されました" msgid "Pending Registration created by FOG_CLIENT" msgstr "FOG_CLIENT により保留中の登録が作成されました" +#, fuzzy +msgid "Pending agent" +msgstr "保留中ホスト" + #, fuzzy msgid "Pending agent enrollments" msgstr "保留中の登録済みホスト" +#, fuzzy +msgid "Pending agents" +msgstr "保留中 MAC アドレス" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6745,6 +6817,10 @@ msgstr "状態" msgid "Ping cycle complete" msgstr "完了しました" +#, fuzzy +msgid "Platform" +msgstr "クロスプラットフォーム" + msgid "Please Select an option" msgstr "オプションを選択してください" @@ -7304,6 +7380,9 @@ msgstr "関連付けを削除" msgid "Real Time" msgstr "日時" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "再起動" @@ -12175,9 +12254,6 @@ msgstr "" #~ msgid "Create and associate" #~ msgstr "関連付けられたノードがありません" -#~ msgid "Cross platform" -#~ msgstr "クロスプラットフォーム" - #~ msgid "Current Associations" #~ msgstr "現在の関連付け" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index f7ba7b6d00..590396a7d7 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -101,6 +101,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -385,6 +389,9 @@ msgstr "" msgid "A client ID is required" msgstr "" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + msgid "A dmi field must be set!" msgstr "" @@ -880,6 +887,15 @@ msgstr "" msgid "Advanced Tasks" msgstr "" +msgid "Agent" +msgstr "" + +msgid "Agent Approval Success" +msgstr "" + +msgid "Agent Denial Success" +msgstr "" + msgid "Ago must be boolean" msgstr "" @@ -896,6 +912,9 @@ msgstr "" msgid "All Hosts" msgstr "" +msgid "All Pending Agents" +msgstr "" + msgid "All Pending Hosts" msgstr "" @@ -1064,9 +1083,15 @@ msgstr "" msgid "Approve" msgstr "" +msgid "Approve Agent Fail" +msgstr "" + msgid "Approve MAC Fail" msgstr "" +msgid "Approve Pending Agents" +msgstr "" + msgid "Approve Pending Hosts" msgstr "" @@ -1079,6 +1104,9 @@ msgstr "" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +msgid "Approved selected agents!" +msgstr "" + msgid "Approved selected hosts!" msgstr "" @@ -2219,6 +2247,21 @@ msgstr "" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +msgid "Denied selected agents!" +msgstr "" + +msgid "Deny" +msgstr "" + +msgid "Deny Agent Fail" +msgstr "" + +msgid "Deny Pending Agents" +msgstr "" + +msgid "Deny selected" +msgstr "" + msgid "Deploy" msgstr "" @@ -2376,6 +2419,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "" @@ -3658,6 +3704,9 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +msgid "Identity" +msgstr "" + msgid "Identity Provider" msgstr "" @@ -5911,6 +5960,9 @@ msgstr "" msgid "Pending" msgstr "" +msgid "Pending Agents" +msgstr "" + msgid "Pending Hosts" msgstr "" @@ -5932,9 +5984,15 @@ msgstr "" msgid "Pending Registration created by FOG_CLIENT" msgstr "" +msgid "Pending agent" +msgstr "" + msgid "Pending agent enrollments" msgstr "" +msgid "Pending agents" +msgstr "" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -5980,6 +6038,9 @@ msgstr "" msgid "Ping cycle complete" msgstr "" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "" @@ -6463,6 +6524,9 @@ msgstr "" msgid "Real Time" msgstr "" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index fd7c0c9bfe..1cda26cd10 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -125,6 +125,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -420,6 +424,9 @@ msgstr "Uma imagem já existe com este nome!" msgid "A client ID is required" msgstr "Um nome de imagem é necessário!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "Evento deve ser uma string" @@ -1016,6 +1023,17 @@ msgstr "avançado" msgid "Advanced Tasks" msgstr "avançado" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "host criado" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "Impressora atualizado!" + msgid "Ago must be boolean" msgstr "" @@ -1032,6 +1050,10 @@ msgstr "" msgid "All Hosts" msgstr "Todos os hosts" +#, fuzzy +msgid "All Pending Agents" +msgstr "pendentes MACs" + #, fuzzy msgid "All Pending Hosts" msgstr "Anfitriões pendentes" @@ -1219,10 +1241,18 @@ msgstr "" msgid "Approve" msgstr "hospedar aprovado" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "hospedar aprovado" + #, fuzzy msgid "Approve MAC Fail" msgstr "hospedar aprovado" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "Anfitriões pendentes" + #, fuzzy msgid "Approve Pending Hosts" msgstr "Anfitriões pendentes" @@ -1237,6 +1267,10 @@ msgstr "Aprovar Hosts selecionados" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "Aprovar Hosts selecionados" + #, fuzzy msgid "Approved selected hosts!" msgstr "Aprovar Hosts selecionados" @@ -2572,6 +2606,25 @@ msgstr "Menu Criar falhou" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "Aprovar Hosts selecionados" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "Apagar dados de arquivo" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "pendentes MACs" + +#, fuzzy +msgid "Deny selected" +msgstr "Delete Selected" + msgid "Deploy" msgstr "implantar" @@ -2755,6 +2808,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "Editar" @@ -4252,6 +4308,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "Shell servidor" + msgid "Identity Provider" msgstr "" @@ -6887,6 +6947,10 @@ msgstr "" msgid "Pending" msgstr "Pendente..." +#, fuzzy +msgid "Pending Agents" +msgstr "pendentes MACs" + msgid "Pending Hosts" msgstr "Anfitriões pendentes" @@ -6911,10 +6975,18 @@ msgstr "Na pendência de Registro criado por FOG_CLIENT" msgid "Pending Registration created by FOG_CLIENT" msgstr "Na pendência de Registro criado por FOG_CLIENT" +#, fuzzy +msgid "Pending agent" +msgstr "anfitriões pendentes" + #, fuzzy msgid "Pending agent enrollments" msgstr "Enquanto se aguarda hosts registrados" +#, fuzzy +msgid "Pending agents" +msgstr "macs pendentes" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6967,6 +7039,9 @@ msgstr "estado" msgid "Ping cycle complete" msgstr "foi destruído" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "Por favor selecione uma opção" @@ -7539,6 +7614,9 @@ msgstr "Delete Selected" msgid "Real Time" msgstr "Anfitrião Update Failed" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "reinicialização" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 02b62f2b49..0fc319dada 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -125,6 +125,10 @@ msgid_plural "%d days left" msgstr[0] "" msgstr[1] "" +#, php-format +msgid "%d decided, %d not: %s" +msgstr "" + #, php-format msgid "%d host(s) are assigned an image they cannot run" msgstr "" @@ -420,6 +424,9 @@ msgstr "图像已经存在具有此名称!" msgid "A client ID is required" msgstr "图像名是必需的!" +msgid "A denied agent keeps asking and keeps being refused until it is enrolled with a new key." +msgstr "" + #, fuzzy msgid "A dmi field must be set!" msgstr "事件必须是字符串" @@ -1016,6 +1023,17 @@ msgstr "高级" msgid "Advanced Tasks" msgstr "高级" +msgid "Agent" +msgstr "" + +#, fuzzy +msgid "Agent Approval Success" +msgstr "主机创建" + +#, fuzzy +msgid "Agent Denial Success" +msgstr "打印机更新!" + msgid "Ago must be boolean" msgstr "" @@ -1032,6 +1050,10 @@ msgstr "" msgid "All Hosts" msgstr "所有主机" +#, fuzzy +msgid "All Pending Agents" +msgstr "待定的MAC" + #, fuzzy msgid "All Pending Hosts" msgstr "待主机" @@ -1219,10 +1241,18 @@ msgstr "" msgid "Approve" msgstr "主机批准" +#, fuzzy +msgid "Approve Agent Fail" +msgstr "主机批准" + #, fuzzy msgid "Approve MAC Fail" msgstr "主机批准" +#, fuzzy +msgid "Approve Pending Agents" +msgstr "待主机" + #, fuzzy msgid "Approve Pending Hosts" msgstr "待主机" @@ -1237,6 +1267,10 @@ msgstr "批准选定主机" msgid "Approved but the signer is unavailable; the agent retries." msgstr "" +#, fuzzy +msgid "Approved selected agents!" +msgstr "批准选定主机" + #, fuzzy msgid "Approved selected hosts!" msgstr "批准选定主机" @@ -2572,6 +2606,25 @@ msgstr "菜单创建失败" msgid "Denied by an admin. The agent backs off to hourly." msgstr "" +#, fuzzy +msgid "Denied selected agents!" +msgstr "批准选定主机" + +msgid "Deny" +msgstr "" + +#, fuzzy +msgid "Deny Agent Fail" +msgstr "删除的文件数据" + +#, fuzzy +msgid "Deny Pending Agents" +msgstr "待定的MAC" + +#, fuzzy +msgid "Deny selected" +msgstr "删除所选" + msgid "Deploy" msgstr "部署" @@ -2755,6 +2808,9 @@ msgstr "" msgid "Each of these is a public certificate. Downloading one is how you add this server to another machine's trust store, hand an auditor the chain, or check what a client is actually being offered; the SHA-256 is what to compare against a client's trust store when working out why one stopped authenticating. Private keys are deliberately absent -- nothing on this page can read one, and nothing on it can hand one out." msgstr "" +msgid "Each selected agent is issued a certificate and collects it on its next check-in." +msgstr "" + msgid "Edit" msgstr "编辑" @@ -4252,6 +4308,10 @@ msgstr "" msgid "Id of the storage group whose master receives the file." msgstr "" +#, fuzzy +msgid "Identity" +msgstr "服务器外壳" + msgid "Identity Provider" msgstr "" @@ -6887,6 +6947,10 @@ msgstr "" msgid "Pending" msgstr "待..." +#, fuzzy +msgid "Pending Agents" +msgstr "待定的MAC" + msgid "Pending Hosts" msgstr "待主机" @@ -6911,10 +6975,18 @@ msgstr "通过创建FOG_CLIENT登记待定" msgid "Pending Registration created by FOG_CLIENT" msgstr "通过创建FOG_CLIENT登记待定" +#, fuzzy +msgid "Pending agent" +msgstr "待主机" + #, fuzzy msgid "Pending agent enrollments" msgstr "待注册主机" +#, fuzzy +msgid "Pending agents" +msgstr "待淅淅沥沥" + msgid "Pending an admin decision. Poll again after retry_after seconds." msgstr "" @@ -6967,6 +7039,9 @@ msgstr "状态" msgid "Ping cycle complete" msgstr "已被破坏" +msgid "Platform" +msgstr "" + msgid "Please Select an option" msgstr "请选择一个选项" @@ -7539,6 +7614,9 @@ msgstr "删除所选" msgid "Real Time" msgstr "主机更新失败" +msgid "Reason" +msgstr "" + msgid "Reboot" msgstr "重启" diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index af706089fa..751393ebba 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -1267,6 +1267,12 @@ private static function _buildSubMenuItems($refNode = '') 'pendingMacs', _('Pending MACs') ); + self::arrayInsertBefore( + 'export', + $menu, + 'pendingAgents', + _('Pending Agents') + ); break; case 'report': // Two kinds of screen under one menu, labeled as two. diff --git a/packages/web/src/Pages/DashboardPage.php b/packages/web/src/Pages/DashboardPage.php index 2ce5a58cca..69382baf0a 100644 --- a/packages/web/src/Pages/DashboardPage.php +++ b/packages/web/src/Pages/DashboardPage.php @@ -199,6 +199,33 @@ public function index(...$args) $title = $pendingMACs . ' ' . _('Pending macs'); self::displayAlert($title, $macPend, 'warning', true, true); } + // fog-agent installs waiting for an admin (Pending Agents). The + // table arrives with schema step 416; before it, there is nothing + // to count. + if (DatabaseManager::getColumns('agentEnrollment', 'aeState')) { + $pendingAgents = Route::getCount( + 'agentenrollment', + ['state' => 'pending'] + ); + if ($pendingAgents > 0) { + $title = $pendingAgents + . ' ' + . ( + $pendingAgents != 1 ? + _('Pending agents') : + _('Pending agent') + ); + $agentPend = sprintf( + $pendingInfo, + _('Click'), + 'host', + 'pendingAgents', + _('here'), + _('to review.') + ); + self::displayAlert($title, $agentPend, 'warning', true, true); + } + } $pluginsNeedingUpdate = self::getClass('PluginManager') ->getPluginsNeedingUpdate(); $pluginUpdateCount = count($pluginsNeedingUpdate); diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index c76e27205e..8e96db0f97 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -538,6 +538,187 @@ public function pendingMacsAjax() } $this->jsonSend($code, $msg); } + /** + * The fog-agent installs waiting for an admin: Pending Agents. + * + * Sibling of Pending Hosts and Pending MACs, and the reason the + * enrollment flow pends anything at all -- an admin looks at who is + * asking before a machine gets a credential (fog-agent design decision: + * admins should know who is doing what). The grid shows the identity + * the machine reported and where the request came from; the CSR and + * the certificate never reach the page. + * + * @return void + */ + public function pendingAgents() + { + if (false === self::$showhtml) { + return; + } + $this->title = _('All Pending Agents'); + + $this->headerData = [ + _('Host'), + _('Reason'), + _('Platform'), + _('Agent'), + _('From'), + _('Identity'), + _('Requested') + ]; + $this->attributes = [ + [], + [], + [], + [], + [], + [], + [] + ]; + + $buttons = self::makeButton( + 'approve', + _('Approve selected'), + 'btn btn-primary float-end' + ); + $buttons .= self::makeButton( + 'deny', + _('Deny selected'), + 'btn btn-danger float-start' + ); + + $modalApprovalBtns = self::makeButton( + 'confirmApproveModal', + _('Approve'), + 'btn btn-outline-secondary float-end' + ); + $modalApprovalBtns .= self::makeButton( + 'cancelApprovalModal', + _('Cancel'), + 'btn btn-outline-secondary float-start', + 'data-bs-dismiss="modal"' + ); + $approvalModal = self::makeModal( + 'approveModal', + _('Approve Pending Agents'), + _('Each selected agent is issued a certificate and collects it on its next check-in.'), + $modalApprovalBtns, + '', + 'success' + ); + + $modalDenyBtns = self::makeButton( + 'confirmDenyModal', + _('Deny'), + 'btn btn-outline-secondary float-end' + ); + $modalDenyBtns .= self::makeButton( + 'cancelDenyModal', + _('Cancel'), + 'btn btn-outline-secondary float-start', + 'data-bs-dismiss="modal"' + ); + $denyModal = self::makeModal( + 'denyModal', + _('Deny Pending Agents'), + _('A denied agent keeps asking and keeps being refused until it is enrolled with a new key.'), + $modalDenyBtns, + '', + 'danger' + ); + + echo self::makeFormTag( + '', + 'agent-pending-form', + $this->formAction, + 'post', + 'application/x-www-form-urlencoded', + true + ); + echo '
'; + echo '
'; + echo '

'; + echo $this->title; + echo '

'; + echo '
'; + echo '
'; + $this->render(12, 'dataTable', $buttons); + echo '
'; + echo ''; + echo '
'; + echo ''; + } + /** + * Approves or denies the selected pending agents. + * + * One decision per row through FOG\Agent\Enrollment, the same code the + * JSON route agentEnrollmentDecide runs, so a row approved here and a + * row approved over the API are indistinguishable afterward. A row + * that can no longer be decided -- already decided from elsewhere, + * deleted, unbound -- is reported and the rest still go through. + * + * @return void + */ + public function pendingAgentsAjax() + { + header('Content-type: application/json'); + + $flags = ['flags' => FILTER_REQUIRE_ARRAY]; + $items = filter_input_array( + INPUT_POST, + ['pending' => $flags] + ); + $pending = array_map('intval', (array)($items['pending'] ?? [])); + $by = (string)self::$FOGUser->get('name'); + $approve = isset($_POST['approvepending']); + $errt = $approve ? _('Approve Agent Fail') : _('Deny Agent Fail'); + $failed = []; + $done = 0; + foreach ($pending as $id) { + try { + if ($approve) { + \FOG\Agent\Enrollment::approve($id, $by); + } else { + \FOG\Agent\Enrollment::deny($id, $by); + } + $done++; + } catch (\RuntimeException $e) { + $failed[] = sprintf('%d: %s', $id, $e->getMessage()); + } + } + if (count($failed)) { + $msg = json_encode( + [ + 'error' => sprintf( + _('%d decided, %d not: %s'), + $done, + count($failed), + implode('; ', $failed) + ), + 'title' => $errt + ] + ); + $code = $done > 0 + ? HTTPResponseCodes::HTTP_ACCEPTED + : HTTPResponseCodes::HTTP_BAD_REQUEST; + } else { + $msg = json_encode( + [ + 'msg' => $approve + ? _('Approved selected agents!') + : _('Denied selected agents!'), + 'title' => $approve + ? _('Agent Approval Success') + : _('Agent Denial Success') + ] + ); + $code = HTTPResponseCodes::HTTP_ACCEPTED; + } + $this->jsonSend($code, $msg); + } /** * Builds the enforce checkbox together with its explanatory help text. * @@ -5632,6 +5813,23 @@ public function getPendingMacList() echo Route::getData(); exit; } + /** + * The pending agents grid's rows. + * + * Not Route::listem(): agentenrollment is deliberately not an API + * class -- every row carries a CSR and, once approved, a certificate -- + * so the page takes the same whitelisted shape the admin JSON route + * serves and the table pages it client-side. The list is bounded by + * what an admin has not yet looked at, never by the fleet. + * + * @return void + */ + public function getPendingAgentList() + { + Route::agentEnrollments(); + echo Route::getData(); + exit; + } /** * Gets the current list of power management tasks. * diff --git a/tests/all-classes-load.test.php b/tests/all-classes-load.test.php index 972ec7c0cd..f9175bb712 100644 --- a/tests/all-classes-load.test.php +++ b/tests/all-classes-load.test.php @@ -213,7 +213,12 @@ function ($f) { return '' !== $f && is_readable($f) && 0 !== strpos($f, 'packages/web/vendor/') - && 0 !== strpos($f, 'tests/'); + && 0 !== strpos($f, 'tests/') + // Analysis tooling, not product: build/ is loaded by the + // root composer autoload-dev and implements PHPStan + // interfaces that exist only under the root vendor/. FOG's + // own autoloader cannot declare it and is not meant to. + && 0 !== strpos($f, 'build/'); } ); From 0cfcf64088860d7cc28c6d74e2c5a158b864d179 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 10:04:20 -0500 Subject: [PATCH 004/117] Agent certificate renewal and enrollment tokens Renewal: POST /agent/v1/renew, over the certificate being renewed. The same gate as poll binds the caller to its host; the body carries a CSR for the same key, and the answer is the enroll "issued" shape. Same key only: a different key is a new claim on the machine and goes through enroll and an admin. Enrollment::renew() signs through the existing helper, moves hostAgentNotAfter and audits. Tokens: FOG\Agent\Token mints, lists and revokes enrollment tokens (the credential that lets a machine enroll without an admin clicking, design 0001 agent-based registration). The token is a 48-hex-character secret shown exactly once; only its sha256 is stored. An expiry is required; uses count down, or -1 is unlimited until expiry. Routes GET /agent/tokens (host.view), POST /agent/token (host.create), DELETE /agent/token/{id} (host.delete). Page Hosts > Agent Tokens, on the Pending Agents shape, with the mint modal handing the token over once and the ajax subs named create*/delete* so the permissions derive from the names. Audited as agent.token. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- bin/psr4-scan.php | 1 + .../js/fog/host/fog.host.agentTokens.js | 120 +++++++++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 147 ++++++++++- .../web/management/languages/messages.pot | 121 ++++++++- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 143 ++++++++++- packages/web/src/Agent/Enrollment.php | 61 +++++ packages/web/src/Agent/Token.php | 160 ++++++++++++ packages/web/src/Auth/Authorization.php | 6 + packages/web/src/Base/FOGPage.php | 6 + packages/web/src/Pages/HostManagement.php | 231 ++++++++++++++++++ packages/web/src/Router/OpenAPI.php | 113 +++++++++ packages/web/src/Router/Route.php | 101 ++++++++ 19 files changed, 2162 insertions(+), 49 deletions(-) create mode 100644 packages/web/management/js/fog/host/fog.host.agentTokens.js create mode 100644 packages/web/src/Agent/Token.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 016ba1c0f7..8d8bd8dbdf 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -207,6 +207,7 @@ // them, and they are one subsystem the way the Boot classes are. 'Enrollment' => 'Agent', 'Principal' => 'Agent', + 'Token' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/packages/web/management/js/fog/host/fog.host.agentTokens.js b/packages/web/management/js/fog/host/fog.host.agentTokens.js new file mode 100644 index 0000000000..b821f18c9c --- /dev/null +++ b/packages/web/management/js/fog/host/fog.host.agentTokens.js @@ -0,0 +1,120 @@ +(function($) { + var mintBtn = $('#mint'), + mintModal = $('#mintModal'), + confirmMint = $('#confirmMintModal'), + showTokenModal = $('#showTokenModal'), + mintedToken = $('#mintedToken'), + copyToken = $('#copyMintedToken'), + revokeBtn = $('#revoke'), + revokeModal = $('#revokeModal'), + confirmRevoke = $('#confirmRevokeModal'), + unlimited = $('#tokenUnlimited'), + uses = $('#tokenUses'), + tokenForm = $('#agent-token-form'), + method = tokenForm.attr('method'), + base = '../management/index.php?node=' + Common.node + '&sub='; + + function esc (s) { + return $('
').text(s == null ? '' : String(s)).html(); + } + function onSelect (selected) { + revokeBtn.prop('disabled', selected.count() == 0); + } + + revokeBtn.prop('disabled', true); + // Client-side table over the whitelisted payload (see + // HostManagement::getAgentTokenList); the hash never leaves the server. + var table = $('#dataTable').registerTable(onSelect, { + order: [ + [5, 'desc'] + ], + columns: [ + {data: 'name'}, + {data: 'state'}, + {data: 'uses'}, + {data: 'expires'}, + {data: 'createdBy'}, + {data: 'created'} + ], + columnDefs: [ + { + render: function (data, type) { + if (type !== 'display') { + return data; + } + var cls = data === 'active' ? 'success' : 'secondary'; + return '' + esc(data) + ''; + }, + targets: 1 + }, + { + render: function (data, type) { + if (type !== 'display') { + return data; + } + return data < 0 ? esc('unlimited') : esc(data); + }, + targets: 2 + } + ], + rowId: 'id', + processing: true, + serverSide: false, + ajax: { + url: base + 'getAgentTokenList', + type: 'post' + } + }); + + unlimited.on('change', function() { + uses.prop('disabled', unlimited.prop('checked')); + }); + + mintBtn.on('click', function() { + mintModal.modal('show'); + }); + confirmMint.on('click', function() { + confirmMint.prop('disabled', true); + var opts = { + tokenName: $('#tokenName').val(), + tokenUses: uses.val(), + tokenExpires: $('#tokenExpires').val() + }; + if (unlimited.prop('checked')) { + opts.tokenUnlimited = 1; + } + $.apiCall(method, base + 'createAgentToken', opts, function(err, data) { + confirmMint.prop('disabled', false); + if (err) { + return; + } + mintModal.modal('hide'); + // The only time the token exists on screen. + mintedToken.val(data.token); + showTokenModal.modal('show'); + table.ajax.reload(null, false); + }); + }); + copyToken.on('click', function() { + mintedToken.trigger('select'); + if (navigator.clipboard) { + navigator.clipboard.writeText(mintedToken.val()); + } else { + document.execCommand('copy'); + } + }); + showTokenModal.on('hidden.bs.modal', function() { + mintedToken.val(''); + }); + + revokeBtn.on('click', function() { + revokeModal.modal('show'); + }); + confirmRevoke.on('click', function() { + revokeBtn.prop('disabled', true); + $.apiCall(method, base + 'deleteAgentTokens', {tokens: $.getSelectedIds(table)}, function(err) { + revokeModal.modal('hide'); + table.ajax.reload(null, false); + }); + }); +})(jQuery); diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index bf7d9ce6e6..423f4f8f87 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -485,6 +485,9 @@ msgstr "Dieser Benutzername ist bereits vorhanden!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Eine Rolle mit diesem Namen ist bereits vorhanden!" @@ -528,6 +531,9 @@ msgstr "Ein Drucker mit diesem Namen ist bereits vorhanden!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Eine Rolle mit diesem Namen ist bereits vorhanden!" @@ -1030,6 +1036,18 @@ msgstr "Host erfolgreich erstellt" msgid "Agent Denial Success" msgstr "Drucker aktualisiert!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "wurde abgebrochen" + +#, fuzzy +msgid "Agent Tokens" +msgstr "API-Zugangstoken" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "wurde abgebrochen" + msgid "Ago must be boolean" msgstr "Ago muss boolean sein" @@ -1160,6 +1178,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Dieser Benutzername ist bereits vorhanden!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Ein Image mit diesem Namen ist bereits vorhanden!" @@ -2026,10 +2047,16 @@ msgstr "Fehler: Herunterladen des Kernels fehlgeschlagen" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "Kopie von bereits existierenden" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2203,6 +2230,9 @@ msgstr "" msgid "Create" msgstr "Erstellen" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2358,6 +2388,10 @@ msgstr "Benutzer erfolgreich erstellt" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Neue %s erstellen" + #, fuzzy msgid "Created" msgstr "Erstellen" @@ -2369,6 +2403,10 @@ msgstr "Erstellt von" msgid "Created Time" msgstr "Erstellt von" +#, fuzzy +msgid "Created by" +msgstr "Erstellt von" + msgid "Created by FOG Reg on" msgstr "Erstellt von FOG Reg am" @@ -2904,6 +2942,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet Accounts" + msgid "Enrollment kit" msgstr "" @@ -3010,6 +3052,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Keine Datei wurde hochgeladen" @@ -3112,6 +3157,10 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Agent enrollment" msgstr "wurde abgebrochen" @@ -5898,6 +5947,10 @@ msgstr "Mitternacht" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "Ausstehende registrierte Hosts" + msgid "Minute value is not valid" msgstr "Minutenwert ist nicht gültig" @@ -6451,6 +6504,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6544,6 +6600,9 @@ msgstr "Icon-Datei nicht gefunden" msgid "Not Registered Hosts" msgstr "Nicht registrierte Hosts" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Keine Zahl" @@ -6865,6 +6924,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7887,9 +7949,33 @@ msgstr "Rückgabewert" msgid "Returning value of key" msgstr "Wert des Schlüssels zurückgeben" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "Ausstehende registrierte Hosts" + +#, fuzzy +msgid "Revoke selected" +msgstr "Ausgewählte entfernen " + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Ausgewählten MAcs freigeben" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "Rollenname" @@ -9924,6 +10010,10 @@ msgstr "" msgid "The record could not be written." msgstr "Temporäre Datei konnte nicht gelesen werden." +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Neuen Standort erstellen" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9933,6 +10023,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9966,6 +10059,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "Es gibt keine Gruppen auf diesem Server." + #, fuzzy msgid "The signing request could not be generated" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9982,10 +10079,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "CPU-Anzahl" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10341,9 +10441,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Regel Zuordnung" +#, fuzzy +msgid "Token Create Fail" +msgstr "Drucker erstellen fehlgeschlagen!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Drucker hinzufügen erfolgreich." + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "FTP-Verbindung fehlgeschlagen" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Drucker hinzufügen erfolgreich." + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "zu viele MACs" @@ -10613,6 +10735,9 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Ausgewählten MAcs freigeben" @@ -10984,6 +11109,12 @@ msgstr "Benutzer" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "Verwenden der Gruppenübereinstimmungsfunktion," @@ -11097,6 +11228,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11205,6 +11339,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Jährlich" @@ -13063,10 +13200,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Dieser Host ist bereits vorhanden." -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Neuen Standort erstellen" - #, fuzzy #~ msgid "There are no " #~ msgstr "Es gibt" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 2f9571aaba..abcf792290 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -490,6 +490,9 @@ msgstr "An image already exists with this name!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "An image already exists with this name!" @@ -533,6 +536,9 @@ msgstr "An image already exists with this name!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "An image already exists with this name!" @@ -1034,6 +1040,18 @@ msgstr "Host Created" msgid "Agent Denial Success" msgstr "Printer updated!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "has been successfully updated" + +#, fuzzy +msgid "Agent Tokens" +msgstr "Access Token" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "has been successfully updated" + msgid "Ago must be boolean" msgstr "" @@ -1164,6 +1182,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "An image already exists with this name!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "An image already exists with this name!" @@ -2028,10 +2049,16 @@ msgstr "Error: Failed to download kernel" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "Could not create printer" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2205,6 +2232,9 @@ msgstr "" msgid "Create" msgstr "Create" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2361,6 +2391,10 @@ msgstr "User created" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Create New %s" + #, fuzzy msgid "Created" msgstr "Create" @@ -2372,6 +2406,10 @@ msgstr "Created By" msgid "Created Time" msgstr "Created By" +#, fuzzy +msgid "Created by" +msgstr "Created By" + msgid "Created by FOG Reg on" msgstr "Created by FOG Reg on" @@ -2906,6 +2944,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet Accounts" + msgid "Enrollment kit" msgstr "" @@ -3012,6 +3054,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "No file was uploaded" @@ -3114,6 +3159,10 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "No file was uploaded" + #, fuzzy msgid "FOG Agent enrollment" msgstr "has been successfully updated" @@ -5910,6 +5959,10 @@ msgstr "Midnight" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "Pending Registered Hosts" + msgid "Minute value is not valid" msgstr "Minute value is not valid" @@ -6463,6 +6516,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6556,6 +6612,9 @@ msgstr "Icon File not found" msgid "Not Registered Hosts" msgstr "Not Registered Hosts" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Not a number" @@ -6876,6 +6935,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7898,9 +7960,33 @@ msgstr "Return Code" msgid "Returning value of key" msgstr "Returning value of key" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "Pending Registered Hosts" + +#, fuzzy +msgid "Revoke selected" +msgstr "Remove selected snapins" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Approve selected Hosts" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "Module Name" @@ -9933,6 +10019,10 @@ msgstr "" msgid "The record could not be written." msgstr "Could not read temp file" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Create New %s" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9942,6 +10032,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9975,6 +10068,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "There are no groups on this server." + #, fuzzy msgid "The signing request could not be generated" msgstr "Could not read temp file" @@ -9991,10 +10088,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "CPU Count" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10350,9 +10450,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Image Association" +#, fuzzy +msgid "Token Create Fail" +msgstr "Printer update failed!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Printer already exists" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "FTP Connection has failed" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Printer already exists" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "Host Primary MAC" @@ -10621,6 +10743,9 @@ msgstr "Unknown upload error occurred. Return code: " msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Approve selected Hosts" @@ -10992,6 +11117,12 @@ msgstr "Users" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -11105,6 +11236,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11213,6 +11347,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Yearly" @@ -13026,10 +13163,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Printer already exists" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Create New %s" - #, fuzzy #~ msgid "There are no " #~ msgstr "There are" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 750405ba28..54cb0ea25e 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -488,6 +488,9 @@ msgstr "Una imagen ya existe con este nombre!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Una imagen ya existe con este nombre!" @@ -531,6 +534,9 @@ msgstr "Una imagen ya existe con este nombre!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Una imagen ya existe con este nombre!" @@ -1046,6 +1052,18 @@ msgstr "Creado" msgid "Agent Denial Success" msgstr "Impresora actualiza!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "se ha actualizado correctamente" + +#, fuzzy +msgid "Agent Tokens" +msgstr "Nombre de usuario" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "se ha actualizado correctamente" + msgid "Ago must be boolean" msgstr "" @@ -1178,6 +1196,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Una imagen ya existe con este nombre!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Una imagen ya existe con este nombre!" @@ -2049,10 +2070,16 @@ msgstr "Error: No se pudo descargar kernel" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "No se pudo crear la impresora" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2225,6 +2252,9 @@ msgstr "" msgid "Create" msgstr "Crear" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2380,6 +2410,10 @@ msgstr "creado por el usuario" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Crear nuevo grupo" + #, fuzzy msgid "Created" msgstr "Crear" @@ -2392,6 +2426,10 @@ msgstr "Creado" msgid "Created Time" msgstr "Creado" +#, fuzzy +msgid "Created by" +msgstr "Creado" + msgid "Created by FOG Reg on" msgstr "" @@ -2939,6 +2977,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Gestión de usuarios" + msgid "Enrollment kit" msgstr "" @@ -3046,6 +3088,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Ningún archivo fue subido" @@ -3150,6 +3195,10 @@ msgstr "" msgid "FOG" msgstr "En " +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Ningún archivo fue subido" + #, fuzzy msgid "FOG Agent enrollment" msgstr "se ha actualizado correctamente" @@ -6010,6 +6059,10 @@ msgstr "Medianoche" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "anfitriones pendientes" + #, fuzzy msgid "Minute value is not valid" msgstr "tipo de tarea no es válida" @@ -6572,6 +6625,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6668,6 +6724,9 @@ msgstr "Icono de archivo no encontrado" msgid "Not Registered Hosts" msgstr "Registrado" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "No un número" @@ -6988,6 +7047,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -8025,9 +8087,33 @@ msgstr "Código de retorno" msgid "Returning value of key" msgstr "Volviendo valor de clave" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "anfitriones pendientes" + +#, fuzzy +msgid "Revoke selected" +msgstr "Remoto" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "retirar" + +msgid "Revoked." +msgstr "" + msgid "Role" msgstr "Rol" @@ -10095,6 +10181,10 @@ msgstr "" msgid "The record could not be written." msgstr "No se pudo leer el archivo temporal" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Crear nuevo grupo" + msgid "The resource is not in a cancellable state." msgstr "" @@ -10104,6 +10194,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -10136,6 +10229,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "No hay grupos en este servidor." + #, fuzzy msgid "The signing request could not be generated" msgstr "No se pudo leer el archivo temporal" @@ -10152,10 +10249,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "Contador de la CPU" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10510,9 +10610,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Asociación para la imagen" +#, fuzzy +msgid "Token Create Fail" +msgstr "actualización de la impresora ha fallado!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Impresora ya existe" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "Añadir fallidos SNAPin!" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Impresora ya existe" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + msgid "Too many MACs" msgstr "" @@ -10780,6 +10902,9 @@ msgstr "Se produjo un error de carga desconocida. Código de retorno: " msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "retirar" @@ -11156,6 +11281,12 @@ msgstr "Usuario" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -11268,6 +11399,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11376,6 +11510,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Anual" @@ -13184,10 +13321,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Impresora ya existe" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Crear nuevo grupo" - #, fuzzy #~ msgid "There are no " #~ msgstr "Existen" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index e5bef961d6..59a0ad78fb 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -485,6 +485,9 @@ msgstr "Dieser Benutzername ist bereits vorhanden!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Eine Rolle mit diesem Namen ist bereits vorhanden!" @@ -528,6 +531,9 @@ msgstr "Ein Drucker mit diesem Namen ist bereits vorhanden!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Eine Rolle mit diesem Namen ist bereits vorhanden!" @@ -1030,6 +1036,18 @@ msgstr "Host erfolgreich erstellt" msgid "Agent Denial Success" msgstr "Drucker aktualisiert!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "wurde abgebrochen" + +#, fuzzy +msgid "Agent Tokens" +msgstr "API-Zugangstoken" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "wurde abgebrochen" + msgid "Ago must be boolean" msgstr "Ago muss boolean sein" @@ -1160,6 +1178,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Dieser Benutzername ist bereits vorhanden!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Ein Image mit diesem Namen ist bereits vorhanden!" @@ -2026,10 +2047,16 @@ msgstr "Fehler: Herunterladen des Kernels fehlgeschlagen" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "Kopie von bereits existierenden" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2203,6 +2230,9 @@ msgstr "" msgid "Create" msgstr "Erstellen" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2358,6 +2388,10 @@ msgstr "Benutzer erfolgreich erstellt" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Neue %s erstellen" + #, fuzzy msgid "Created" msgstr "Erstellen" @@ -2369,6 +2403,10 @@ msgstr "Erstellt von" msgid "Created Time" msgstr "Erstellt von" +#, fuzzy +msgid "Created by" +msgstr "Erstellt von" + msgid "Created by FOG Reg on" msgstr "Erstellt von FOG Reg am" @@ -2904,6 +2942,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet Accounts" + msgid "Enrollment kit" msgstr "" @@ -3010,6 +3052,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Keine Datei wurde hochgeladen" @@ -3112,6 +3157,10 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Agent enrollment" msgstr "wurde abgebrochen" @@ -5899,6 +5948,10 @@ msgstr "Mitternacht" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "Ausstehende registrierte Hosts" + msgid "Minute value is not valid" msgstr "Minutenwert ist nicht gültig" @@ -6452,6 +6505,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6545,6 +6601,9 @@ msgstr "Icon-Datei nicht gefunden" msgid "Not Registered Hosts" msgstr "Nicht registrierte Hosts" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Keine Zahl" @@ -6866,6 +6925,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7888,9 +7950,33 @@ msgstr "Rückgabewert" msgid "Returning value of key" msgstr "Wert des Schlüssels zurückgeben" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "Ausstehende registrierte Hosts" + +#, fuzzy +msgid "Revoke selected" +msgstr "Ausgewählte entfernen " + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Ausgewählten MAcs freigeben" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "Rollenname" @@ -9925,6 +10011,10 @@ msgstr "" msgid "The record could not be written." msgstr "Temporäre Datei konnte nicht gelesen werden." +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Neuen Standort erstellen" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9934,6 +10024,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9967,6 +10060,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "Es gibt keine Gruppen auf diesem Server." + #, fuzzy msgid "The signing request could not be generated" msgstr "Temporäre Datei konnte nicht gelesen werden." @@ -9983,10 +10080,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "CPU-Anzahl" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10342,9 +10442,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Regel Zuordnung" +#, fuzzy +msgid "Token Create Fail" +msgstr "Drucker erstellen fehlgeschlagen!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Drucker hinzufügen erfolgreich." + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "FTP-Verbindung fehlgeschlagen" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Drucker hinzufügen erfolgreich." + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "zu viele MACs" @@ -10614,6 +10736,9 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Ausgewählten MAcs freigeben" @@ -10985,6 +11110,12 @@ msgstr "Benutzer" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "Verwenden der Gruppenübereinstimmungsfunktion," @@ -11098,6 +11229,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11206,6 +11340,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Jährlich" @@ -13064,10 +13201,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Dieser Host ist bereits vorhanden." -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Neuen Standort erstellen" - #, fuzzy #~ msgid "There are no " #~ msgstr "Es gibt" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 6f9006a6bc..4c0bf9b11e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -491,6 +491,9 @@ msgstr "Une image existe déjà avec ce nom!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Une image existe déjà avec ce nom!" @@ -534,6 +537,9 @@ msgstr "Une image existe déjà avec ce nom!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Une image existe déjà avec ce nom!" @@ -1035,6 +1041,18 @@ msgstr "hôte Créé" msgid "Agent Denial Success" msgstr "Imprimante mis à jour!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "a été mis à jour avec succès" + +#, fuzzy +msgid "Agent Tokens" +msgstr "Jeton d'accès" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "a été mis à jour avec succès" + msgid "Ago must be boolean" msgstr "" @@ -1165,6 +1183,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Une image existe déjà avec ce nom!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Une image existe déjà avec ce nom!" @@ -2030,10 +2051,16 @@ msgstr "Erreur: Impossible de télécharger le noyau" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "Impossible de créer l'imprimante" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2207,6 +2234,9 @@ msgstr "" msgid "Create" msgstr "Créer" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2362,6 +2392,10 @@ msgstr "utilisateur créé" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Créer un nouveau %s" + #, fuzzy msgid "Created" msgstr "Créer" @@ -2373,6 +2407,10 @@ msgstr "Créé par" msgid "Created Time" msgstr "Créé par" +#, fuzzy +msgid "Created by" +msgstr "Créé par" + msgid "Created by FOG Reg on" msgstr "Créé par FOG Reg sur" @@ -2907,6 +2945,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Comptes Pushbullet" + msgid "Enrollment kit" msgstr "" @@ -3013,6 +3055,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Aucun fichier a été téléchargé" @@ -3115,6 +3160,10 @@ msgstr "" msgid "FOG" msgstr "BROUILLARD" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Aucun fichier a été téléchargé" + #, fuzzy msgid "FOG Agent enrollment" msgstr "a été mis à jour avec succès" @@ -5898,6 +5947,10 @@ msgstr "Minuit" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "Dans l'attente des hôtes enregistrés" + msgid "Minute value is not valid" msgstr "valeur de la minute est pas valide" @@ -6450,6 +6503,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6543,6 +6599,9 @@ msgstr "Icône Fichier introuvable" msgid "Not Registered Hosts" msgstr "Hosts Pas encore inscrit" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Pas un certain nombre" @@ -6863,6 +6922,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7885,9 +7947,33 @@ msgstr "code de retour" msgid "Returning value of key" msgstr "De retour valeur de clé" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "Dans l'attente des hôtes enregistrés" + +#, fuzzy +msgid "Revoke selected" +msgstr "Retirer snapins sélectionnés" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Approuver hôtes sélectionnés" + +msgid "Revoked." +msgstr "" + msgid "Role" msgstr "Rôle" @@ -9918,6 +10004,10 @@ msgstr "" msgid "The record could not be written." msgstr "Impossible de lire le fichier temporaire" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Créer un nouveau %s" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9927,6 +10017,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9960,6 +10053,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "Il n'y a pas de groupes sur ce serveur." + #, fuzzy msgid "The signing request could not be generated" msgstr "Impossible de lire le fichier temporaire" @@ -9976,10 +10073,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "Nombre de CPU" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10335,9 +10435,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Association Image" +#, fuzzy +msgid "Token Create Fail" +msgstr "mise à jour de l'imprimante a échoué!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Imprimante existe déjà" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "Connexion FTP a échoué" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Imprimante existe déjà" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "Hôte MAC primaire" @@ -10607,6 +10729,9 @@ msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Approuver hôtes sélectionnés" @@ -10978,6 +11103,12 @@ msgstr "Utilisateurs" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -11091,6 +11222,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11199,6 +11333,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Annuel" @@ -13016,10 +13153,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Imprimante existe déjà" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Créer un nouveau %s" - #, fuzzy #~ msgid "There are no " #~ msgstr "Il y a" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index ecbf274aea..0dffafe9a9 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -482,6 +482,9 @@ msgstr "Esiste già un utente con questo nome!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Esiste già un ruolo con questo nome!" @@ -524,6 +527,9 @@ msgstr "Nome stampante già esistente on questo nome!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Esiste già un ruolo con questo nome!" @@ -1008,6 +1014,18 @@ msgstr "Creazione Host con successo" msgid "Agent Denial Success" msgstr "Aggiornamento stampante riuscito" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "è stato cancellato" + +#, fuzzy +msgid "Agent Tokens" +msgstr "API Utente Token" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "è stato cancellato" + msgid "Ago must be boolean" msgstr "Fa deve essere boolean" @@ -1136,6 +1154,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Esiste già un utente con questo nome!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Un'immagine esiste già con questo nome!" @@ -1977,9 +1998,15 @@ msgstr "Errore: Impossibile scaricare il kernel" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + msgid "Copy from existing" msgstr "Copia da esistenti" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2147,6 +2174,9 @@ msgstr "" msgid "Create" msgstr "Creare" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2301,6 +2331,10 @@ msgstr "Creazione utente riuscita" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Crea nuovo %s" + #, fuzzy msgid "Created" msgstr "Creare" @@ -2312,6 +2346,10 @@ msgstr "Creato da" msgid "Created Time" msgstr "Creato da" +#, fuzzy +msgid "Created by" +msgstr "Creato da" + msgid "Created by FOG Reg on" msgstr "Creato da FOG Reg su" @@ -2840,6 +2878,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet Conti" + msgid "Enrollment kit" msgstr "" @@ -2944,6 +2986,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Nessun file è stato caricato" @@ -3041,6 +3086,10 @@ msgstr "" msgid "FOG" msgstr "NEBBIA" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Nessun file è stato caricato" + #, fuzzy msgid "FOG Agent enrollment" msgstr "è stato cancellato" @@ -5730,6 +5779,10 @@ msgstr "Mezzanotte" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "In attesa di host registrati" + msgid "Minute value is not valid" msgstr "valore dei minuti non è valido" @@ -6270,6 +6323,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6358,6 +6414,9 @@ msgstr "File Icona non trovato" msgid "Not Registered Hosts" msgstr "Host non registrati" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Non è un numero" @@ -6674,6 +6733,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7670,9 +7732,33 @@ msgstr "Codice di ritorno" msgid "Returning value of key" msgstr "Tornando valore della chiave" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "In attesa di host registrati" + +#, fuzzy +msgid "Revoke selected" +msgstr "Rimuovi i selezionati" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Approvare MAC selezionati" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "Nome regola" @@ -9638,6 +9724,10 @@ msgstr "" msgid "The record could not be written." msgstr "Impossibile leggere il file temporaneo" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Crea nuova posizione" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9647,6 +9737,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9680,6 +9773,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "Non ci sono gruppi su questo server" + #, fuzzy msgid "The signing request could not be generated" msgstr "Impossibile leggere il file temporaneo" @@ -9696,10 +9793,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "Conte CPU" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10046,9 +10146,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Regole di associazione" +#, fuzzy +msgid "Token Create Fail" +msgstr "Creazione stampante fallita" + +#, fuzzy +msgid "Token Create Success" +msgstr "Creazione stampante riuscita" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "Connessione FTP non è riuscita" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Creazione stampante riuscita" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + msgid "Too many MACs" msgstr "Troppi MAC" @@ -10310,6 +10432,9 @@ msgstr "Si è verificato errore di caricamento sconosciuto" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Approvare MAC selezionati" @@ -10670,6 +10795,12 @@ msgstr "utenti" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "Utilizzo della funzione di corrispondenza gruppo" @@ -10777,6 +10908,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -10882,6 +11016,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Annuale" @@ -12672,10 +12809,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Questo host esiste già" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Crea nuova posizione" - #, fuzzy #~ msgid "There are no " #~ msgstr "Ci sono" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index c5203a365d..8714f059d7 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -472,6 +472,9 @@ msgstr "この名前のユーザーは既に存在します" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "この名前のロールは既に存在します!" @@ -514,6 +517,9 @@ msgstr "この名前のプリンターは既に存在します!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + msgid "A role already exists with this name!" msgstr "この名前のロールは既に存在します!" @@ -989,6 +995,18 @@ msgstr "承認に成功しました" msgid "Agent Denial Success" msgstr "プラグインをインストールしました!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "強制終了されました" + +#, fuzzy +msgid "Agent Tokens" +msgstr "ユーザー API トークン" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "強制終了されました" + msgid "Ago must be boolean" msgstr "Ago はブール値である必要があります" @@ -1120,6 +1138,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "この名前のプリンターは既に存在します!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "この名前のイメージは既に存在します!" @@ -1961,9 +1982,15 @@ msgstr "エラー: カーネルのダウンロードに失敗しました" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + msgid "Copy from existing" msgstr "既存の項目からコピー" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2131,6 +2158,9 @@ msgstr "" msgid "Create" msgstr "作成" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2286,6 +2316,10 @@ msgstr "タスク状態を作成" msgid "Create tasking succeeded" msgstr "タスク状態を作成" +#, fuzzy +msgid "Create token" +msgstr "新しい %s を作成" + #, fuzzy msgid "Created" msgstr "作成" @@ -2297,6 +2331,10 @@ msgstr "作成者" msgid "Created Time" msgstr "ジョブ作成時刻" +#, fuzzy +msgid "Created by" +msgstr "作成者" + msgid "Created by FOG Reg on" msgstr "FOG Reg により作成:" @@ -2826,6 +2864,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet アカウント" + msgid "Enrollment kit" msgstr "" @@ -2929,6 +2971,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "ファイルはアップロードされませんでした" @@ -3026,6 +3071,10 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "ファイルはアップロードされませんでした" + #, fuzzy msgid "FOG Agent enrollment" msgstr "強制終了されました" @@ -5696,6 +5745,10 @@ msgstr "午前 0 時" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "保留中の登録済みホスト" + msgid "Minute value is not valid" msgstr "分の値が無効です" @@ -6237,6 +6290,10 @@ msgstr "サイトがありません" msgid "No such object." msgstr "" +#, fuzzy +msgid "No such token." +msgstr "サイトがありません" + msgid "No such user." msgstr "" @@ -6324,6 +6381,9 @@ msgstr "見つかりません" msgid "Not Registered Hosts" msgstr "未登録ホスト" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "数値ではありません" @@ -6645,6 +6705,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7640,9 +7703,33 @@ msgstr "戻りコード" msgid "Returning value of key" msgstr "キーの値を返しています" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "保留中の登録済みホスト" + +#, fuzzy +msgid "Revoke selected" +msgstr "選択した項目を削除" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "選択したルールを削除" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "ロール" @@ -9585,6 +9672,10 @@ msgstr "" msgid "The record could not be written." msgstr "強制終了できませんでした" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "新しいロケーションを作成" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9594,6 +9685,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9627,6 +9721,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "このサーバーにはグループがありません" + #, fuzzy msgid "The signing request could not be generated" msgstr "強制終了できませんでした" @@ -9644,10 +9742,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "CPU 数" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -9994,9 +10095,31 @@ msgstr "" msgid "Toggle navigation" msgstr "ナビゲーション切り替え" +#, fuzzy +msgid "Token Create Fail" +msgstr "ロールの作成に失敗しました" + +#, fuzzy +msgid "Token Create Success" +msgstr "ロールを作成しました" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "FTP 接続に失敗しました" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "ロールを作成しました" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + msgid "Too many MACs" msgstr "MAC アドレスが多すぎます" @@ -10255,6 +10378,9 @@ msgstr "不明なアップロードエラーが発生しました" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + msgid "Unmark selected client ignore" msgstr "" @@ -10619,6 +10745,12 @@ msgstr "ユーザー" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "グループ照合機能を使用しています" @@ -10725,6 +10857,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -10822,6 +10957,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "年単位" @@ -13571,9 +13709,6 @@ msgstr "" #~ msgid "Remove selected printers" #~ msgstr "選択したプリンターを削除" -#~ msgid "Remove selected rules" -#~ msgstr "選択したルールを削除" - #~ msgid "Remove selected users" #~ msgstr "選択したユーザーを削除" @@ -13856,10 +13991,6 @@ msgstr "" #~ msgid "The below items are only used for the old client." #~ msgstr "以下の項目は旧クライアントでのみ使用されます。" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "新しいロケーションを作成" - #~ msgid "The clients will checkin with the server from time" #~ msgstr "クライアントは一定時間ごとにサーバーへチェックインします" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 590396a7d7..5533fc7dc5 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -443,6 +443,9 @@ msgstr "" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + msgid "A module already exists with this name!" msgstr "" @@ -480,6 +483,9 @@ msgstr "" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + msgid "A role already exists with this name!" msgstr "" @@ -896,6 +902,15 @@ msgstr "" msgid "Agent Denial Success" msgstr "" +msgid "Agent Enrollment Tokens" +msgstr "" + +msgid "Agent Tokens" +msgstr "" + +msgid "Agent enrollment tokens" +msgstr "" + msgid "Ago must be boolean" msgstr "" @@ -1011,6 +1026,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "" @@ -1760,9 +1778,15 @@ msgstr "" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + msgid "Copy from existing" msgstr "" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -1904,6 +1928,9 @@ msgstr "" msgid "Create" msgstr "" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2035,6 +2062,9 @@ msgstr "" msgid "Create tasking succeeded" msgstr "" +msgid "Create token" +msgstr "" + msgid "Created" msgstr "" @@ -2044,6 +2074,9 @@ msgstr "" msgid "Created Time" msgstr "" +msgid "Created by" +msgstr "" + msgid "Created by FOG Reg on" msgstr "" @@ -2507,6 +2540,9 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +msgid "Enrollment Token" +msgstr "" + msgid "Enrollment kit" msgstr "" @@ -2604,6 +2640,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + msgid "Every file was uploaded." msgstr "" @@ -2689,6 +2728,9 @@ msgstr "" msgid "FOG" msgstr "" +msgid "FOG Agent certificate renewal" +msgstr "" + msgid "FOG Agent enrollment" msgstr "" @@ -5045,6 +5087,9 @@ msgstr "" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +msgid "Mint an agent enrollment token" +msgstr "" + msgid "Minute value is not valid" msgstr "" @@ -5523,6 +5568,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -5604,6 +5652,9 @@ msgstr "" msgid "Not Registered Hosts" msgstr "" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "" @@ -5888,6 +5939,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -6764,9 +6818,30 @@ msgstr "" msgid "Returning value of key" msgstr "" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +msgid "Revoke an agent enrollment token" +msgstr "" + +msgid "Revoke selected" +msgstr "" + +msgid "Revoked selected tokens." +msgstr "" + +msgid "Revoked." +msgstr "" + msgid "Role" msgstr "" @@ -8498,6 +8573,9 @@ msgstr "" msgid "The record could not be written." msgstr "" +msgid "The renewed certificate, leaf then chain." +msgstr "" + msgid "The resource is not in a cancellable state." msgstr "" @@ -8507,6 +8585,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -8537,6 +8618,9 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +msgid "The signing helper is not available on this server." +msgstr "" + msgid "The signing request could not be generated" msgstr "" @@ -8552,10 +8636,12 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +msgid "The token." +msgstr "" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -8892,9 +8978,27 @@ msgstr "" msgid "Toggle navigation" msgstr "" +msgid "Token Create Fail" +msgstr "" + +msgid "Token Create Success" +msgstr "" + +msgid "Token Revoke Fail" +msgstr "" + +msgid "Token Revoke Success" +msgstr "" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + msgid "Too many MACs" msgstr "" @@ -9120,6 +9224,9 @@ msgstr "" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + msgid "Unmark selected client ignore" msgstr "" @@ -9435,6 +9542,12 @@ msgstr "" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -9537,6 +9650,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -9630,6 +9746,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 1cda26cd10..601ef08293 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -490,6 +490,9 @@ msgstr "Uma imagem já existe com este nome!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "Uma imagem já existe com este nome!" @@ -533,6 +536,9 @@ msgstr "Uma imagem já existe com este nome!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "Uma imagem já existe com este nome!" @@ -1034,6 +1040,18 @@ msgstr "host criado" msgid "Agent Denial Success" msgstr "Impressora atualizado!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "foi atualizado com sucesso" + +#, fuzzy +msgid "Agent Tokens" +msgstr "token de acesso" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "foi atualizado com sucesso" + msgid "Ago must be boolean" msgstr "" @@ -1164,6 +1182,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "Uma imagem já existe com este nome!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "Uma imagem já existe com este nome!" @@ -2029,10 +2050,16 @@ msgstr "Erro: falha ao baixar do kernel" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "Não foi possível criar impressora" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2206,6 +2233,9 @@ msgstr "" msgid "Create" msgstr "Crio" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2361,6 +2391,10 @@ msgstr "usuário criado" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "Criar novo %s" + #, fuzzy msgid "Created" msgstr "Crio" @@ -2372,6 +2406,10 @@ msgstr "Criado por" msgid "Created Time" msgstr "Criado por" +#, fuzzy +msgid "Created by" +msgstr "Criado por" + msgid "Created by FOG Reg on" msgstr "Criado por FOG Reg em" @@ -2906,6 +2944,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Contas Pushbullet" + msgid "Enrollment kit" msgstr "" @@ -3012,6 +3054,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "Nenhum arquivo foi transferido" @@ -3114,6 +3159,10 @@ msgstr "" msgid "FOG" msgstr "NÉVOA" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "Nenhum arquivo foi transferido" + #, fuzzy msgid "FOG Agent enrollment" msgstr "foi atualizado com sucesso" @@ -5897,6 +5946,10 @@ msgstr "meia-noite" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "Enquanto se aguarda hosts registrados" + msgid "Minute value is not valid" msgstr "valor do minuto não é válido" @@ -6450,6 +6503,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6543,6 +6599,9 @@ msgstr "Ícone do Arquivo não encontrado" msgid "Not Registered Hosts" msgstr "Hosts não registrada" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "Não é um número" @@ -6863,6 +6922,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7885,9 +7947,33 @@ msgstr "Código de retorno" msgid "Returning value of key" msgstr "Retornando valor da chave" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "Enquanto se aguarda hosts registrados" + +#, fuzzy +msgid "Revoke selected" +msgstr "Remover snapins selecionados" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "Aprovar Hosts selecionados" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "Nome do módulo" @@ -9920,6 +10006,10 @@ msgstr "" msgid "The record could not be written." msgstr "Não foi possível ler arquivo temporário" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "Criar novo %s" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9929,6 +10019,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9962,6 +10055,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "Não existem grupos neste servidor." + #, fuzzy msgid "The signing request could not be generated" msgstr "Não foi possível ler arquivo temporário" @@ -9978,10 +10075,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "Contagem de CPU" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10337,9 +10437,31 @@ msgstr "" msgid "Toggle navigation" msgstr "Associação imagem" +#, fuzzy +msgid "Token Create Fail" +msgstr "atualização da impressora falhou!" + +#, fuzzy +msgid "Token Create Success" +msgstr "Impressora já existe" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "Conexão FTP falhou" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "Impressora já existe" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "Hospedeiro primário MAC" @@ -10609,6 +10731,9 @@ msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "Aprovar Hosts selecionados" @@ -10980,6 +11105,12 @@ msgstr "usuários" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -11093,6 +11224,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11201,6 +11335,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "Anual" @@ -13018,10 +13155,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "Impressora já existe" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "Criar novo %s" - #, fuzzy #~ msgid "There are no " #~ msgstr "tem" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 0fc319dada..ab5e1c7455 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -490,6 +490,9 @@ msgstr "图像已经存在具有此名称!" msgid "A migration step failed. The plugin is left activated and not marked installed." msgstr "" +msgid "A missing name, a bad use count, or an expiry that is not in the future." +msgstr "" + #, fuzzy msgid "A module already exists with this name!" msgstr "图像已经存在具有此名称!" @@ -533,6 +536,9 @@ msgstr "图像已经存在具有此名称!" msgid "A request is pending since %s for %s." msgstr "" +msgid "A revoked token can never approve an enrollment again." +msgstr "" + #, fuzzy msgid "A role already exists with this name!" msgstr "图像已经存在具有此名称!" @@ -1034,6 +1040,18 @@ msgstr "主机创建" msgid "Agent Denial Success" msgstr "打印机更新!" +#, fuzzy +msgid "Agent Enrollment Tokens" +msgstr "已成功更新" + +#, fuzzy +msgid "Agent Tokens" +msgstr "访问令牌" + +#, fuzzy +msgid "Agent enrollment tokens" +msgstr "已成功更新" + msgid "Ago must be boolean" msgstr "" @@ -1164,6 +1182,9 @@ msgstr "" msgid "An entry already exists with this name!" msgstr "图像已经存在具有此名称!" +msgid "An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time." +msgstr "" + msgid "An image already exists with this name!" msgstr "图像已经存在具有此名称!" @@ -2029,10 +2050,16 @@ msgstr "错误:无法下载内核" msgid "Converted %1$d `0` reference(s) to NULL across %2$d column(s)" msgstr "" +msgid "Copy" +msgstr "" + #, fuzzy msgid "Copy from existing" msgstr "无法创建打印机" +msgid "Copy it now. It is not stored and cannot be shown again." +msgstr "" + msgid "Copy this token now" msgstr "" @@ -2206,6 +2233,9 @@ msgstr "" msgid "Create" msgstr "创建" +msgid "Create Enrollment Token" +msgstr "" + msgid "Create Immediate Power task" msgstr "" @@ -2361,6 +2391,10 @@ msgstr "用户创建" msgid "Create tasking succeeded" msgstr "" +#, fuzzy +msgid "Create token" +msgstr "新建%s" + #, fuzzy msgid "Created" msgstr "创建" @@ -2372,6 +2406,10 @@ msgstr "由...制作" msgid "Created Time" msgstr "由...制作" +#, fuzzy +msgid "Created by" +msgstr "由...制作" + msgid "Created by FOG Reg on" msgstr "创建者FOG上注册" @@ -2906,6 +2944,10 @@ msgstr "" msgid "Enrolled certificate must be a SHA-256 fingerprint (64 hex characters)" msgstr "" +#, fuzzy +msgid "Enrollment Token" +msgstr "Pushbullet账户" + msgid "Enrollment kit" msgstr "" @@ -3012,6 +3054,9 @@ msgstr "" msgid "Every enrollment still waiting for a decision, without the CSR. What the Pending Agents page reads." msgstr "" +msgid "Every enrollment token with its remaining uses and expiry, never the token itself. What the Agent Tokens page reads." +msgstr "" + #, fuzzy msgid "Every file was uploaded." msgstr "没有文件被上传" @@ -3114,6 +3159,10 @@ msgstr "" msgid "FOG" msgstr "雾" +#, fuzzy +msgid "FOG Agent certificate renewal" +msgstr "没有文件被上传" + #, fuzzy msgid "FOG Agent enrollment" msgstr "已成功更新" @@ -5897,6 +5946,10 @@ msgstr "午夜" msgid "Minimum time limit for Auto Logout to become active is 5 minutes." msgstr "" +#, fuzzy +msgid "Mint an agent enrollment token" +msgstr "待注册主机" + msgid "Minute value is not valid" msgstr "分钟值无效" @@ -6450,6 +6503,9 @@ msgstr "" msgid "No such object." msgstr "" +msgid "No such token." +msgstr "" + msgid "No such user." msgstr "" @@ -6543,6 +6599,9 @@ msgstr "图标文件未找到" msgid "Not Registered Hosts" msgstr "未注册主机" +msgid "Not a certificate request, or one for a key other than the one this certificate proved." +msgstr "" + msgid "Not a number" msgstr "不是一个数字" @@ -6863,6 +6922,9 @@ msgstr "" msgid "Over a year" msgstr "" +msgid "Over the certificate being renewed: the same gate as poll binds the caller to its host, and the body carries a request for the same key. The answer is the enroll \"issued\" shape. A request for any other key is refused; a key change goes through enroll and an admin." +msgstr "" + msgid "PLUGIN OPTIONS" msgstr "" @@ -7885,9 +7947,33 @@ msgstr "返回代码" msgid "Returning value of key" msgstr "返回键的值" +msgid "Returns the token exactly once; only its hash is stored. An expiry is required. uses is how many enrollments it approves, or -1 for unlimited until it expires. Audited as agent.token." +msgstr "" + msgid "Reverse" msgstr "" +msgid "Revoke" +msgstr "" + +msgid "Revoke Enrollment Tokens" +msgstr "" + +#, fuzzy +msgid "Revoke an agent enrollment token" +msgstr "待注册主机" + +#, fuzzy +msgid "Revoke selected" +msgstr "删除选定snapins" + +#, fuzzy +msgid "Revoked selected tokens." +msgstr "批准选定主机" + +msgid "Revoked." +msgstr "" + #, fuzzy msgid "Role" msgstr "模块名称" @@ -9920,6 +10006,10 @@ msgstr "" msgid "The record could not be written." msgstr "无法读取临时文件" +#, fuzzy +msgid "The renewed certificate, leaf then chain." +msgstr "新建%s" + msgid "The resource is not in a cancellable state." msgstr "" @@ -9929,6 +10019,9 @@ msgstr "" msgid "The root CA has been imported, but this host's system trust store could not be updated -- so HTTPS calls made on this server will not accept it yet. Re-run the installer, and check the installation log." msgstr "" +msgid "The row goes, so the token can never match again. Audited as agent.token." +msgstr "" + msgid "The same rules in prose, for a client reading this at runtime." msgstr "" @@ -9962,6 +10055,10 @@ msgstr "" msgid "The signer is unavailable; nothing changed." msgstr "" +#, fuzzy +msgid "The signing helper is not available on this server." +msgstr "有此服务器上没有组。" + #, fuzzy msgid "The signing request could not be generated" msgstr "无法读取临时文件" @@ -9978,10 +10075,13 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" +#, fuzzy +msgid "The token." +msgstr "CPU计数" + msgid "The trust anchor, published as ca.cert.der. Every fog-client pins this one." msgstr "" @@ -10337,9 +10437,31 @@ msgstr "" msgid "Toggle navigation" msgstr "图像协会" +#, fuzzy +msgid "Token Create Fail" +msgstr "打印机更新失败!" + +#, fuzzy +msgid "Token Create Success" +msgstr "打印机已经存在" + +#, fuzzy +msgid "Token Revoke Fail" +msgstr "FTP连接失败" + +#, fuzzy +msgid "Token Revoke Success" +msgstr "打印机已经存在" + +msgid "Token created. Copy it now." +msgstr "" + msgid "Token or user:pass (optional)" msgstr "" +msgid "Token rows." +msgstr "" + #, fuzzy msgid "Too many MACs" msgstr "主机主MAC" @@ -10609,6 +10731,9 @@ msgstr "发生未知上传错误。返回代码:" msgid "Unless it was declined with" msgstr "" +msgid "Unlimited" +msgstr "" + #, fuzzy msgid "Unmark selected client ignore" msgstr "批准选定主机" @@ -10980,6 +11105,12 @@ msgstr "用户" msgid "Users, user groups and roles as id/name pairs. Each section is gated on that entity's own view permission -- user.view, usergroup.view, role.view -- and comes back EMPTY rather than 403 when the caller lacks it, so this route discloses nothing they could not already list. A caller holding none of the three can still save private filters; there is simply nobody they may name." msgstr "" +msgid "Uses" +msgstr "" + +msgid "Uses left" +msgstr "" + msgid "Using the group match function" msgstr "" @@ -11093,6 +11224,9 @@ msgstr "" msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" +msgid "What this token is for" +msgstr "" + msgid "What to do next" msgstr "" @@ -11201,6 +11335,9 @@ msgstr "" msgid "Within 30 days" msgstr "" +msgid "Y-m-d H:i:s, server time." +msgstr "" + msgid "Yearly" msgstr "每年" @@ -13018,10 +13155,6 @@ msgstr "" #~ msgid "That mapping already exists." #~ msgstr "打印机已经存在" -#, fuzzy -#~ msgid "The certificate chain" -#~ msgstr "新建%s" - #, fuzzy #~ msgid "There are no " #~ msgstr "有" diff --git a/packages/web/src/Agent/Enrollment.php b/packages/web/src/Agent/Enrollment.php index 733b892769..0671e7a498 100644 --- a/packages/web/src/Agent/Enrollment.php +++ b/packages/web/src/Agent/Enrollment.php @@ -465,6 +465,67 @@ private static function _createPendingHost(AgentEnrollment $Row, array $identity return $hostID; } + /** + * An enrolled agent renews its certificate over its own mTLS session. + * + * Same key only. The presented certificate proved the caller holds the + * key bound to this host, and the request is signed for that same key, + * so nothing an admin decided changes: the binding, the host, the + * subject. A different key is a new claim on the machine and goes + * through enroll, where it pends as a rebind for an admin. The old + * certificate is not revoked -- it binds to the same key and expires on + * its own -- and there is nothing to revoke it with; the binding is the + * only thing the server checks. + * + * @param Host $Host the principal the gate bound + * @param string $csrPEM the request, for the bound key + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return string the leaf followed by the issuing chain, PEM + */ + public static function renew(Host $Host, $csrPEM) + { + $fingerprint = self::fingerprint((string)$csrPEM); + if (null === $fingerprint) { + throw new \RuntimeException('csr_pem is not a certificate request', 400); + } + if (!hash_equals((string)$Host->get('agentFingerprint'), $fingerprint)) { + throw new \RuntimeException('the request is not for the key this certificate proved', 400); + } + list($leaf, $chain) = self::_sign((string)$csrPEM, (int)$Host->get('id')); + $parsed = openssl_x509_parse($leaf); + $notAfter = is_array($parsed) && isset($parsed['validTo_time_t']) + ? gmdate('Y-m-d H:i:s', (int)$parsed['validTo_time_t']) + : null; + // The manager rather than Host::save(), as agentPoll does: a save + // rewrites the MAC association, and renewal is a routine call. + self::getClass('HostManager')->update( + ['id' => (int)$Host->get('id')], + '', + [ + 'agentNotAfter' => $notAfter, + 'agentCheckin' => self::niceDate()->format('Y-m-d H:i:s') + ] + ); + Audit::record( + [ + 'type' => 'agent.enroll', + 'subjectType' => 'host', + 'subjectID' => (int)$Host->get('id'), + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'text' => sprintf( + 'certificate renewed to %s, key %s', + (string)$notAfter, + substr($fingerprint, 0, 16) + ), + 'authSource' => Audit::SOURCE_ANONYMOUS + ] + ); + return $leaf . $chain; + } + /** * Issues on an automatic path and answers the agent in the same * request. diff --git a/packages/web/src/Agent/Token.php b/packages/web/src/Agent/Token.php new file mode 100644 index 0000000000..357e11d5fe --- /dev/null +++ b/packages/web/src/Agent/Token.php @@ -0,0 +1,160 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\AgentEnrollToken; +use FOG\Router\Route; + +/** + * Mints, lists and revokes enrollment tokens. + * + * Only the sha256 of a token is stored, so the token itself is shown + * exactly once, in the answer to the mint. An expiry is required: a token + * that never lapses is a standing credential sitting in an image or a + * runbook, and the design's own golden-image case (0001 section 4.2) is + * served by a token that outlives the rollout and not the year. Uses + * count down to zero; -1 is unlimited until the expiry. + * + * Consumption lives in Enrollment::_consumeToken(), which is the only + * reader of the hash; this class is the admin's side. + * + * @category Token + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class Token extends FOGBase +{ + const UNLIMITED = -1; + + /** + * Mints a token and returns it, once. + * + * @param string $name what the admin calls it (a rollout, a site) + * @param int $uses how many enrollments it approves; -1 unlimited + * @param string $expires 'Y-m-d H:i:s', must be in the future + * @param string $by the minting user's name + * + * @throws \RuntimeException 400 on a bad field + * + * @return array ['token' => the secret, 'row' => AgentEnrollToken] + */ + public static function mint($name, $uses, $expires, $by) + { + $name = trim((string)$name); + if ('' === $name || strlen($name) > 191) { + throw new \RuntimeException('name is required, at most 191 characters', 400); + } + $uses = (int)$uses; + if ($uses < 1 && self::UNLIMITED !== $uses) { + throw new \RuntimeException('uses must be at least 1, or -1 for unlimited', 400); + } + $expires = trim((string)$expires); + if (!self::validDate($expires) || strtotime($expires) <= time()) { + throw new \RuntimeException('expires must be a date and time in the future', 400); + } + $expires = date('Y-m-d H:i:s', strtotime($expires)); + // 24 random bytes as hex: 48 characters that survive every shell, + // every clipboard and every unattended-install file unquoted. + $secret = bin2hex(random_bytes(24)); + $Row = new AgentEnrollToken(); + $Row->set('name', $name) + ->set('hash', hash('sha256', $secret)) + ->set('uses', $uses) + ->set('expires', $expires) + ->set('createdBy', (string)$by) + ->set('created', self::niceDate()->format('Y-m-d H:i:s')); + if (!$Row->save()) { + throw new \RuntimeException('could not store the token', 500); + } + Audit::record( + [ + 'type' => 'agent.token', + 'subjectType' => 'agentenrolltoken', + 'subjectID' => (int)$Row->get('id'), + 'subjectLabel' => $name, + 'renderable' => 1, + 'text' => sprintf( + 'minted, %s, expires %s', + self::UNLIMITED === $uses ? 'unlimited uses' : $uses . ' use(s)', + $expires + ) + ] + ); + return ['token' => $secret, 'row' => $Row]; + } + + /** + * Revokes a token: the row goes, so the hash can never match again. + * + * @param int $id the token row + * @param string $by the revoking user's name + * + * @throws \RuntimeException 404 when there is no such token + * + * @return void + */ + public static function revoke($id, $by) + { + $Row = new AgentEnrollToken((int)$id); + if (!$Row->isValid()) { + throw new \RuntimeException('no such token', 404); + } + $name = (string)$Row->get('name'); + $Row->destroy(); + Audit::record( + [ + 'type' => 'agent.token', + 'subjectType' => 'agentenrolltoken', + 'subjectID' => (int)$id, + 'subjectLabel' => $name, + 'renderable' => 1, + 'text' => 'revoked by ' . $by + ] + ); + } + + /** + * Every token, for the admin's list. Never the hash. + * + * @return array + */ + public static function rows() + { + $now = time(); + $rows = []; + foreach ((array)Route::getList('agentenrolltoken', [], 'AND', 'id') as $row) { + $row = (array)$row; + $uses = (int)($row['uses'] ?? 0); + $expires = (string)($row['expires'] ?? ''); + $rows[] = [ + 'id' => (int)($row['id'] ?? 0), + 'name' => (string)($row['name'] ?? ''), + 'uses' => $uses, + 'expires' => $expires, + 'createdBy' => (string)($row['createdBy'] ?? ''), + 'created' => (string)($row['created'] ?? ''), + // What the list shows at a glance: a token that can no + // longer approve anything, and why. + 'state' => 0 === $uses ? 'spent' + : (self::validDate($expires) && strtotime($expires) < $now ? 'expired' : 'active') + ]; + } + return $rows; + } +} diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 20395e9b9f..49021f3776 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -340,8 +340,14 @@ class Authorization extends FOGBase // edits hosts, so it carries the host permissions. 'agentenroll' => null, 'agentpoll' => null, // fog-agent: gated by the client certificate in Route, not by a token + 'agentrenew' => null, // fog-agent: same gate 'agentenrollments' => 'host.view', 'agentenrollmentdecide' => 'host.edit', + // Tokens approve machines the admin has not seen, which creates + // hosts; minting one is host.create and pulling one is host.delete. + 'agenttokens' => 'host.view', + 'agenttokenmint' => 'host.create', + 'agenttokenrevoke' => 'host.delete', 'export' => 'system.export', 'kernelUpdate' => 'settings.view', 'initrdUpdate' => 'settings.view', diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index 751393ebba..f3210e9110 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -1273,6 +1273,12 @@ private static function _buildSubMenuItems($refNode = '') 'pendingAgents', _('Pending Agents') ); + self::arrayInsertBefore( + 'export', + $menu, + 'agentTokens', + _('Agent Tokens') + ); break; case 'report': // Two kinds of screen under one menu, labeled as two. diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 8e96db0f97..94e5c3c394 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -719,6 +719,225 @@ public function pendingAgentsAjax() } $this->jsonSend($code, $msg); } + /** + * Agent enrollment tokens: mint, list, revoke. + * + * A token lets a machine enroll without an admin clicking (design 0001 + * agent-based registration: the token goes onto the disk before first + * boot). It is shown exactly once, in the modal that answers the mint; + * the list only ever shows name, uses left and expiry. + * + * @return void + */ + public function agentTokens() + { + if (false === self::$showhtml) { + return; + } + $this->title = _('Agent Enrollment Tokens'); + + $this->headerData = [ + _('Name'), + _('State'), + _('Uses left'), + _('Expires'), + _('Created by'), + _('Created') + ]; + $this->attributes = [ + [], + [], + [], + [], + [], + [] + ]; + + $buttons = self::makeButton( + 'mint', + _('Create token'), + 'btn btn-primary float-end' + ); + $buttons .= self::makeButton( + 'revoke', + _('Revoke selected'), + 'btn btn-danger float-start' + ); + + $labelClass = 'col-sm-3 col-form-label'; + $default = self::niceDate()->modify('+7 days')->format('Y-m-d\\TH:i'); + $fields = [ + self::makeLabel($labelClass, 'tokenName', _('Name')) + => self::makeInput('form-control', 'tokenName', _('What this token is for'), 'text', 'tokenName', '', true, false, -1, 191), + self::makeLabel($labelClass, 'tokenUses', _('Uses')) + => '
' + . self::makeInput('form-control', 'tokenUses', '', 'number', 'tokenUses', '1', true, false, -1, -1, 'min="1"') + . '
' + . self::makeInput('form-check-input mt-0', 'tokenUnlimited', '', 'checkbox', 'tokenUnlimited', '1') + . ' ' + . '
', + self::makeLabel($labelClass, 'tokenExpires', _('Expires')) + => self::makeInput('form-control', 'tokenExpires', '', 'datetime-local', 'tokenExpires', $default, true) + ]; + $mintBody = ''; + foreach ($fields as $label => $input) { + $mintBody .= '
' . $label . '
' . $input . '
'; + } + $mintBody .= '

' + . _('An expiry is required. The token approves enrollments until it is spent or expires; revoke it here at any time.') + . '

'; + $modalMintBtns = self::makeButton( + 'confirmMintModal', + _('Create'), + 'btn btn-outline-secondary float-end' + ); + $modalMintBtns .= self::makeButton( + 'cancelMintModal', + _('Cancel'), + 'btn btn-outline-secondary float-start', + 'data-bs-dismiss="modal"' + ); + $mintModal = self::makeModal( + 'mintModal', + _('Create Enrollment Token'), + $mintBody, + $modalMintBtns, + '', + 'primary' + ); + + // The one time the token is on screen. Nothing on the server can + // show it again, and the modal says so. + $showBody = '

' . _('Copy it now. It is not stored and cannot be shown again.') . '

' + . '
' + . self::makeInput('form-control font-monospace', 'mintedToken', '', 'text', 'mintedToken', '', false, false, -1, -1, 'readonly') + . self::makeButton('copyMintedToken', _('Copy'), 'btn btn-outline-secondary') + . '
' + . '

fog-agent enroll --server <url> --ca <bundle> --token <token>

'; + $showModal = self::makeModal( + 'showTokenModal', + _('Enrollment Token'), + $showBody, + self::makeButton('closeShowTokenModal', _('Done'), 'btn btn-outline-secondary float-end', 'data-bs-dismiss="modal"'), + '', + 'success' + ); + + $modalRevokeBtns = self::makeButton( + 'confirmRevokeModal', + _('Revoke'), + 'btn btn-outline-secondary float-end' + ); + $modalRevokeBtns .= self::makeButton( + 'cancelRevokeModal', + _('Cancel'), + 'btn btn-outline-secondary float-start', + 'data-bs-dismiss="modal"' + ); + $revokeModal = self::makeModal( + 'revokeModal', + _('Revoke Enrollment Tokens'), + _('A revoked token can never approve an enrollment again.'), + $modalRevokeBtns, + '', + 'danger' + ); + + echo self::makeFormTag( + '', + 'agent-token-form', + $this->formAction, + 'post', + 'application/x-www-form-urlencoded', + true + ); + echo '
'; + echo '
'; + echo '

'; + echo $this->title; + echo '

'; + echo '
'; + echo '
'; + $this->render(12, 'dataTable', $buttons); + echo '
'; + echo ''; + echo '
'; + echo ''; + } + /** + * Mints a token from the modal. Named create* so the permission is + * host.create: a token creates hosts. + * + * @return void + */ + public function createAgentTokenAjax() + { + header('Content-type: application/json'); + $uses = filter_input(INPUT_POST, 'tokenUnlimited') + ? \FOG\Agent\Token::UNLIMITED + : (int)filter_input(INPUT_POST, 'tokenUses'); + // datetime-local sends 'Y-m-d\TH:i'; mint() wants a space. + $expires = str_replace('T', ' ', (string)filter_input(INPUT_POST, 'tokenExpires')); + try { + $minted = \FOG\Agent\Token::mint( + (string)filter_input(INPUT_POST, 'tokenName'), + $uses, + $expires, + (string)self::$FOGUser->get('name') + ); + $code = HTTPResponseCodes::HTTP_SUCCESS; + $msg = json_encode( + [ + 'msg' => _('Token created. Copy it now.'), + 'title' => _('Token Create Success'), + 'token' => $minted['token'], + 'name' => (string)$minted['row']->get('name') + ] + ); + } catch (\RuntimeException $e) { + $code = HTTPResponseCodes::HTTP_BAD_REQUEST; + $msg = json_encode( + [ + 'error' => $e->getMessage(), + 'title' => _('Token Create Fail') + ] + ); + } + $this->jsonSend($code, $msg); + } + /** + * Revokes the selected tokens. Named delete* so the permission is + * host.delete. + * + * @return void + */ + public function deleteAgentTokensAjax() + { + header('Content-type: application/json'); + $flags = ['flags' => FILTER_REQUIRE_ARRAY]; + $items = filter_input_array(INPUT_POST, ['tokens' => $flags]); + $by = (string)self::$FOGUser->get('name'); + $failed = []; + foreach (array_map('intval', (array)($items['tokens'] ?? [])) as $id) { + try { + \FOG\Agent\Token::revoke($id, $by); + } catch (\RuntimeException $e) { + $failed[] = sprintf('%d: %s', $id, $e->getMessage()); + } + } + if (count($failed)) { + $code = HTTPResponseCodes::HTTP_BAD_REQUEST; + $msg = json_encode(['error' => implode('; ', $failed), 'title' => _('Token Revoke Fail')]); + } else { + $code = HTTPResponseCodes::HTTP_SUCCESS; + $msg = json_encode(['msg' => _('Revoked selected tokens.'), 'title' => _('Token Revoke Success')]); + } + $this->jsonSend($code, $msg); + } /** * Builds the enforce checkbox together with its explanatory help text. * @@ -5830,6 +6049,18 @@ public function getPendingAgentList() echo Route::getData(); exit; } + /** + * The agent token grid's rows: the same whitelisted shape the admin + * JSON route serves, paged client-side. + * + * @return void + */ + public function getAgentTokenList() + { + Route::agentTokens(); + echo Route::getData(); + exit; + } /** * Gets the current list of power management tasks. * diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index 2ee0b31a1a..f0a1e1e350 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2603,6 +2603,46 @@ private static function _fixedPaths() ] ) ], + '/agent/v1/renew' => [ + 'post' => self::_op( + '', + 'agentrenew', + _('FOG Agent certificate renewal'), + _('Over the certificate being renewed: the same gate as ' + . 'poll binds the caller to its host, and the body ' + . 'carries a request for the same key. The answer is ' + . 'the enroll "issued" shape. A request for any other ' + . 'key is refused; a key change goes through enroll ' + . 'and an admin.'), + [ + '200' => [ + 'description' => _('The renewed certificate, leaf then chain.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string', 'enum' => ['issued']], + 'host_id' => ['type' => 'integer'], + 'certificate_pem' => ['type' => 'string'], + 'not_after' => ['type' => 'string'] + ] + ]]] + ], + '400' => ['description' => _('Not a certificate request, or one for a key other than the one this certificate proved.')], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')], + '503' => ['description' => _('The signing helper is not available on this server.')] + ], + [], + [ + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'required' => ['csr_pem'], + 'properties' => [ + 'csr_pem' => ['type' => 'string'] + ] + ]]] + ] + ) + ], '/agent/enrollments' => [ 'get' => self::_op( '', @@ -2622,6 +2662,79 @@ private static function _fixedPaths() ) ) ], + '/agent/tokens' => [ + 'get' => self::_op( + '', + 'agenttokens', + _('Agent enrollment tokens'), + _('Every enrollment token with its remaining uses and ' + . 'expiry, never the token itself. What the Agent ' + . 'Tokens page reads.'), + $json( + [ + 'type' => 'object', + 'properties' => [ + 'data' => ['type' => 'array', 'items' => ['type' => 'object']], + 'msg' => ['type' => 'string'] + ] + ], + _('Token rows.') + ) + ) + ], + '/agent/token' => [ + 'post' => self::_op( + '', + 'agenttokenmint', + _('Mint an agent enrollment token'), + _('Returns the token exactly once; only its hash is ' + . 'stored. An expiry is required. uses is how many ' + . 'enrollments it approves, or -1 for unlimited ' + . 'until it expires. Audited as agent.token.'), + [ + '200' => [ + 'description' => _('The token.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'id' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'token' => ['type' => 'string'], + 'expires' => ['type' => 'string'], + 'msg' => ['type' => 'string'] + ] + ]]] + ], + '400' => ['description' => _('A missing name, a bad use count, or an expiry that is not in the future.')] + ], + [], + [ + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'required' => ['name', 'expires'], + 'properties' => [ + 'name' => ['type' => 'string'], + 'uses' => ['type' => 'integer', 'default' => 1], + 'expires' => ['type' => 'string', 'description' => _('Y-m-d H:i:s, server time.')] + ] + ]]] + ] + ) + ], + '/agent/token/{id}' => [ + 'delete' => self::_op( + '', + 'agenttokenrevoke', + _('Revoke an agent enrollment token'), + _('The row goes, so the token can never match again. ' + . 'Audited as agent.token.'), + [ + '200' => ['description' => _('Revoked.')], + '404' => ['description' => _('No such token.')] + ], + [self::_idParameter()] + ) + ], '/agent/enrollment/{id}/{action}' => [ 'post' => self::_op( '', diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 36b5621595..c02e385b05 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -1551,8 +1551,12 @@ protected static function defineRoutes(\FastRoute\RouteCollector $r) // $unauthexact above; the other two are the admin's side of it. self::_registerRoute($r, 'POST', '/agent/v1/enroll', [__CLASS__, 'agentEnroll'], 'agentenroll'); self::_registerRoute($r, 'POST', '/agent/v1/poll', [__CLASS__, 'agentPoll'], 'agentpoll'); + self::_registerRoute($r, 'POST', '/agent/v1/renew', [__CLASS__, 'agentRenew'], 'agentrenew'); self::_registerRoute($r, 'GET', '/agent/enrollments', [__CLASS__, 'agentEnrollments'], 'agentenrollments'); self::_registerRoute($r, 'POST', '/agent/enrollment/[i:id]/[*:action]', [__CLASS__, 'agentEnrollmentDecide'], 'agentenrollmentdecide'); + self::_registerRoute($r, 'GET', '/agent/tokens', [__CLASS__, 'agentTokens'], 'agenttokens'); + self::_registerRoute($r, 'POST', '/agent/token', [__CLASS__, 'agentTokenMint'], 'agenttokenmint'); + self::_registerRoute($r, 'DELETE', '/agent/token/[i:id]', [__CLASS__, 'agentTokenRevoke'], 'agenttokenrevoke'); // Alias. swagger.json is the filename people and tooling reach // for first -- Swagger UI predates the OpenAPI rename and the // habit stuck. Same handler, same document, so neither name is @@ -2902,6 +2906,45 @@ public static function agentPoll() ) ); } + /** + * fog-agent's certificate renewal, over the certificate being renewed. + * + * The gate has already bound the caller to a host; the body carries a + * request for the same key and the answer is the enroll "issued" shape, + * so the agent stores it exactly as it stored the first one. Refusals + * are JSON with the reason: 400 for a request that is not for the bound + * key, 503 when the signing helper is not available. + * + * @return void + */ + public static function agentRenew() + { + $Host = self::$agentHost; + $body = self::_jsonBody(); + try { + $cert = \FOG\Agent\Enrollment::renew($Host, (string)($body['csr_pem'] ?? '')); + } catch (\RuntimeException $e) { + $code = (int)$e->getCode(); + HTTPResponseCodes::breakHead( + $code >= 400 && $code <= 599 ? $code : HTTPResponseCodes::HTTP_INTERNAL_SERVER_ERROR, + json_encode(['status' => 'error', 'error' => $e->getMessage()]) + ); + return; + } + // Re-read: renew() wrote the new expiry through the manager. + $Host = new Host((int)$Host->get('id')); + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_OK, + json_encode( + [ + 'status' => 'issued', + 'host_id' => (int)$Host->get('id'), + 'certificate_pem' => $cert, + 'not_after' => (string)$Host->get('agentNotAfter') + ] + ) + ); + } /** * The pending fog-agent enrollments, for the admin's list. * @@ -2976,6 +3019,64 @@ public static function agentEnrollmentDecide($id, $action) 'msg' => _('success') ]; } + /** + * The enrollment tokens, for the admin's list. Never the hash. + * + * @return void + */ + public static function agentTokens() + { + self::$data = ['data' => \FOG\Agent\Token::rows(), 'msg' => _('success')]; + } + /** + * Mints an enrollment token. The token is in this answer and nowhere + * else, ever: only its hash is stored. + * + * @return void + */ + public static function agentTokenMint() + { + $body = self::_jsonBody(); + try { + $minted = \FOG\Agent\Token::mint( + (string)($body['name'] ?? ''), + (int)($body['uses'] ?? 1), + (string)($body['expires'] ?? ''), + (string)self::$FOGUser->get('name') + ); + } catch (\RuntimeException $e) { + $code = (int)$e->getCode(); + self::sendResponse( + $code >= 400 && $code <= 599 ? $code : HTTPResponseCodes::HTTP_INTERNAL_SERVER_ERROR, + json_encode(['error' => $e->getMessage()]) + ); + return; + } + self::$data = [ + 'id' => (int)$minted['row']->get('id'), + 'name' => (string)$minted['row']->get('name'), + 'token' => $minted['token'], + 'expires' => (string)$minted['row']->get('expires'), + 'msg' => _('success') + ]; + } + /** + * Revokes an enrollment token. + * + * @param int $id the token row + * + * @return void + */ + public static function agentTokenRevoke($id) + { + try { + \FOG\Agent\Token::revoke((int)$id, (string)self::$FOGUser->get('name')); + } catch (\RuntimeException $e) { + self::sendResponse(HTTPResponseCodes::HTTP_NOT_FOUND, json_encode(['error' => $e->getMessage()])); + return; + } + self::$data = ['id' => (int)$id, 'msg' => _('success')]; + } /** * Serves an OpenAPI description of this server's API. * From 09e57792a757e03ba221c3dc34968a97169bcd16 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 15:05:10 +0000 Subject: [PATCH 005/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 423f4f8f87..8dcb144077 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10079,6 +10079,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index abcf792290..0f395d2869 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10088,6 +10088,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 54cb0ea25e..3c7925050c 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10249,6 +10249,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 59a0ad78fb..f7e3629518 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10080,6 +10080,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 4c0bf9b11e..bcd7b7434f 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10073,6 +10073,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 0dffafe9a9..b82e33fc0b 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9793,6 +9793,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 8714f059d7..750c40af70 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9742,6 +9742,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 5533fc7dc5..b79b321d09 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8636,6 +8636,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 601ef08293..f04470c9c5 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ab5e1c7455..ab54291480 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From a8b673ba676ecceef378c5196a6857d38ccce225 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 10:21:25 -0500 Subject: [PATCH 006/117] First capability: desired state, results, and the hostname block The convergence half of the protocol (design 0001 section 2). The poll now lists the capabilities this server offers the host and the revision of its desired state; GET /agent/v1/state returns that state and POST /agent/v1/result records what a provider did with it. Same certificate gate as poll. A capability is listed when its legacy module is on for the host: the global FOG_CLIENT_*_ENABLED setting and the host's resolved module set, the two checks the old client's endpoints make, so existing per-host and per-group module choices carry over unchanged. The first capability is hostname: the host record's name and its enforce flag. The revision is a digest of the state, so "anything changed?" costs the poll one compare. Results are agent.result audit rows on the host, where FOG already shows what happened to a host; no table until inventory needs one. Writes the client certificate authorized carry authSource 'agent', not anonymous (renewal corrected to match). Also: the Agent Tokens page's mint and revoke posts fell through to the host list, because FOGPageManager appends the Ajax suffix only after method_exists() passes for the bare name. Both handlers now have their bare twin. Found by the first browser run. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- bin/psr4-scan.php | 1 + .../de_DE.UTF-8/LC_MESSAGES/messages.po | 30 +++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 26 ++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 26 ++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 30 +++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 26 ++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 30 +++- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 30 +++- .../web/management/languages/messages.pot | 22 ++- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 26 ++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 26 ++- packages/web/src/Agent/Enrollment.php | 2 +- packages/web/src/Agent/Principal.php | 6 + packages/web/src/Agent/State.php | 160 ++++++++++++++++++ packages/web/src/Auth/Authorization.php | 2 + packages/web/src/Pages/HostManagement.php | 22 +++ packages/web/src/Router/OpenAPI.php | 59 +++++++ packages/web/src/Router/Route.php | 50 +++++- 18 files changed, 544 insertions(+), 30 deletions(-) create mode 100644 packages/web/src/Agent/State.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 8d8bd8dbdf..217f744346 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -208,6 +208,7 @@ 'Enrollment' => 'Agent', 'Principal' => 'Agent', 'Token' => 'Agent', + 'State' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 8dcb144077..77d0c2dcbe 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -3157,10 +3157,18 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Keine Datei wurde hochgeladen" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "wurde abgebrochen" + #, fuzzy msgid "FOG Agent enrollment" msgstr "wurde abgebrochen" @@ -7709,6 +7717,10 @@ msgstr "" msgid "Recorded in range" msgstr "Datensatz wurde nicht gefunden, Fehler: %s" +#, fuzzy +msgid "Recorded." +msgstr "(empfohlen)" + #, fuzzy msgid "Records" msgstr "Aktuelle Datensätze" @@ -9836,6 +9848,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Fehler beim Erstellen eines Tasks" + #, fuzzy msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" @@ -10079,7 +10095,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10706,6 +10721,9 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unknown action" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11223,9 +11241,15 @@ msgstr "wurde abgebrochen" msgid "What it does" msgstr "wurde abgebrochen" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" @@ -13078,10 +13102,6 @@ msgstr "" #~ msgid "Product Keys" #~ msgstr "Host Produktschlüssel" -#, fuzzy -#~ msgid "Recorded" -#~ msgstr "(empfohlen)" - #~ msgid "Register must be managed from hooks or events" #~ msgstr "Register muss von Haken oder Ereignissen gemanagt werden" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 0f395d2869..3c62115db7 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -3159,10 +3159,18 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "No file was uploaded" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "No file was uploaded" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "has been successfully updated" + #, fuzzy msgid "FOG Agent enrollment" msgstr "has been successfully updated" @@ -7720,6 +7728,10 @@ msgstr "" msgid "Recorded in range" msgstr "Record not found, Error: %s" +#, fuzzy +msgid "Recorded." +msgstr "Current Records" + #, fuzzy msgid "Records" msgstr "Current Records" @@ -9845,6 +9857,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Failed to create task" + #, fuzzy msgid "The enrollment is no longer pending." msgstr " no longer exists" @@ -10088,7 +10104,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10715,6 +10730,9 @@ msgstr "Unknown upload error occurred. Return code: " msgid "Unknown action" msgstr "Unknown upload error occurred. Return code: " +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11231,9 +11249,15 @@ msgstr "has been successfully updated" msgid "What it does" msgstr "has been successfully updated" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 3c7925050c..9c91c2b963 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -3195,10 +3195,18 @@ msgstr "" msgid "FOG" msgstr "En " +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Ningún archivo fue subido" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Ningún archivo fue subido" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "se ha actualizado correctamente" + #, fuzzy msgid "FOG Agent enrollment" msgstr "se ha actualizado correctamente" @@ -7843,6 +7851,10 @@ msgstr "" msgid "Recorded in range" msgstr "Registro no encontrado, error: %s" +#, fuzzy +msgid "Recorded." +msgstr "Registros actuales" + #, fuzzy msgid "Records" msgstr "Registros actuales" @@ -10006,6 +10018,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "No se pudo crear la tarea" + msgid "The enrollment is no longer pending." msgstr "" @@ -10249,7 +10265,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10873,6 +10888,9 @@ msgstr "Se produjo un error de carga desconocida. Código de retorno: " msgid "Unknown action" msgstr "Se produjo un error de carga desconocida. Código de retorno: " +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11394,9 +11412,15 @@ msgstr "se ha actualizado correctamente" msgid "What it does" msgstr "se ha actualizado correctamente" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index f7e3629518..7503aa7adb 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -3157,10 +3157,18 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Keine Datei wurde hochgeladen" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "wurde abgebrochen" + #, fuzzy msgid "FOG Agent enrollment" msgstr "wurde abgebrochen" @@ -7710,6 +7718,10 @@ msgstr "" msgid "Recorded in range" msgstr "Datensatz wurde nicht gefunden, Fehler: %s" +#, fuzzy +msgid "Recorded." +msgstr "(empfohlen)" + #, fuzzy msgid "Records" msgstr "Aktuelle Datensätze" @@ -9837,6 +9849,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Fehler beim Erstellen eines Tasks" + #, fuzzy msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" @@ -10080,7 +10096,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10707,6 +10722,9 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unknown action" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11224,9 +11242,15 @@ msgstr "wurde abgebrochen" msgid "What it does" msgstr "wurde abgebrochen" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" @@ -13079,10 +13103,6 @@ msgstr "" #~ msgid "Product Keys" #~ msgstr "Host Produktschlüssel" -#, fuzzy -#~ msgid "Recorded" -#~ msgstr "(empfohlen)" - #~ msgid "Register must be managed from hooks or events" #~ msgstr "Register muss von Haken oder Ereignissen gemanagt werden" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index bcd7b7434f..908d094785 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -3160,10 +3160,18 @@ msgstr "" msgid "FOG" msgstr "BROUILLARD" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Aucun fichier a été téléchargé" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Aucun fichier a été téléchargé" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "a été mis à jour avec succès" + #, fuzzy msgid "FOG Agent enrollment" msgstr "a été mis à jour avec succès" @@ -7707,6 +7715,10 @@ msgstr "" msgid "Recorded in range" msgstr "Enregistrement non trouvé, Erreur: %s" +#, fuzzy +msgid "Recorded." +msgstr "Enregistrements courants" + #, fuzzy msgid "Records" msgstr "Enregistrements courants" @@ -9830,6 +9842,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Impossible de créer la tâche" + #, fuzzy msgid "The enrollment is no longer pending." msgstr " n'existe plus" @@ -10073,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10700,6 +10715,9 @@ msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " msgid "Unknown action" msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11217,9 +11235,15 @@ msgstr "a été mis à jour avec succès" msgid "What it does" msgstr "a été mis à jour avec succès" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b82e33fc0b..a80cc0c333 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -3086,10 +3086,18 @@ msgstr "" msgid "FOG" msgstr "NEBBIA" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Nessun file è stato caricato" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Nessun file è stato caricato" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "è stato cancellato" + #, fuzzy msgid "FOG Agent enrollment" msgstr "è stato cancellato" @@ -7499,6 +7507,10 @@ msgstr "" msgid "Recorded in range" msgstr "Record non trovato" +#, fuzzy +msgid "Recorded." +msgstr "Consigliato" + #, fuzzy msgid "Records" msgstr "Current Records" @@ -9551,6 +9563,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Impossibile creare un'attività" + #, fuzzy msgid "The enrollment is no longer pending." msgstr "non è più in esecuzione" @@ -9793,7 +9809,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10404,6 +10419,9 @@ msgstr "Si è verificato errore di caricamento sconosciuto" msgid "Unknown action" msgstr "Si è verificato errore di caricamento sconosciuto" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -10903,9 +10921,15 @@ msgstr "è stato cancellato" msgid "What it does" msgstr "è stato cancellato" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" @@ -12693,10 +12717,6 @@ msgstr "" #~ msgid "Product Keys" #~ msgstr "Host Product Key" -#, fuzzy -#~ msgid "Recorded" -#~ msgstr "Consigliato" - #~ msgid "Register must be managed from hooks or events" #~ msgstr "Registro deve essere gestito da hook o eventi" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 750c40af70..41149ef67e 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -3071,10 +3071,18 @@ msgstr "" msgid "FOG" msgstr "FOG" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "ファイルはアップロードされませんでした" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "ファイルはアップロードされませんでした" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "強制終了されました" + #, fuzzy msgid "FOG Agent enrollment" msgstr "強制終了されました" @@ -7472,6 +7480,10 @@ msgstr "" msgid "Recorded in range" msgstr "レコードが見つかりません" +#, fuzzy +msgid "Recorded." +msgstr "保護されていません" + #, fuzzy msgid "Records" msgstr "現在のレコード" @@ -9497,6 +9509,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "タスクの作成に失敗しました" + #, fuzzy msgid "The enrollment is no longer pending." msgstr "選択したプリンターを追加" @@ -9742,7 +9758,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10351,6 +10366,9 @@ msgstr "不明なデータベースエラー" msgid "Unknown action" msgstr "不明なデータベースエラー" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -10852,9 +10870,15 @@ msgstr "強制終了されました" msgid "What it does" msgstr "強制終了されました" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" @@ -13669,10 +13693,6 @@ msgstr "" #~ msgid "Rebranding element has been successfully updated!" #~ msgstr "ブランド設定を更新しました!" -#, fuzzy -#~ msgid "Recorded" -#~ msgstr "保護されていません" - #~ msgid "Register must be managed from hooks or events" #~ msgstr "登録はフックまたはイベントから管理する必要があります" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index b79b321d09..43156920ec 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -2728,9 +2728,15 @@ msgstr "" msgid "FOG" msgstr "" +msgid "FOG Agent capability result" +msgstr "" + msgid "FOG Agent certificate renewal" msgstr "" +msgid "FOG Agent desired state" +msgstr "" + msgid "FOG Agent enrollment" msgstr "" @@ -6605,6 +6611,9 @@ msgstr "" msgid "Recorded in range" msgstr "" +msgid "Recorded." +msgstr "" + msgid "Records" msgstr "" @@ -8412,6 +8421,9 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +msgid "The desired state." +msgstr "" + msgid "The enrollment is no longer pending." msgstr "" @@ -8636,7 +8648,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -9199,6 +9210,9 @@ msgstr "" msgid "Unknown action" msgstr "" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -9645,9 +9659,15 @@ msgstr "" msgid "What it does" msgstr "" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index f04470c9c5..68e5dd5b23 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -3159,10 +3159,18 @@ msgstr "" msgid "FOG" msgstr "NÉVOA" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "Nenhum arquivo foi transferido" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "Nenhum arquivo foi transferido" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "foi atualizado com sucesso" + #, fuzzy msgid "FOG Agent enrollment" msgstr "foi atualizado com sucesso" @@ -7707,6 +7715,10 @@ msgstr "" msgid "Recorded in range" msgstr "Registro não encontrado, erro: %s" +#, fuzzy +msgid "Recorded." +msgstr "Registros atuais" + #, fuzzy msgid "Records" msgstr "Registros atuais" @@ -9832,6 +9844,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "Falha ao criar tarefa" + #, fuzzy msgid "The enrollment is no longer pending." msgstr " não existe mais" @@ -10075,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10702,6 +10717,9 @@ msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " msgid "Unknown action" msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11219,9 +11237,15 @@ msgstr "foi atualizado com sucesso" msgid "What it does" msgstr "foi atualizado com sucesso" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ab54291480..ce590f9901 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -3159,10 +3159,18 @@ msgstr "" msgid "FOG" msgstr "雾" +#, fuzzy +msgid "FOG Agent capability result" +msgstr "没有文件被上传" + #, fuzzy msgid "FOG Agent certificate renewal" msgstr "没有文件被上传" +#, fuzzy +msgid "FOG Agent desired state" +msgstr "已成功更新" + #, fuzzy msgid "FOG Agent enrollment" msgstr "已成功更新" @@ -7707,6 +7715,10 @@ msgstr "" msgid "Recorded in range" msgstr "未发现记录,错误: %s" +#, fuzzy +msgid "Recorded." +msgstr "当前记录" + #, fuzzy msgid "Records" msgstr "当前记录" @@ -9832,6 +9844,10 @@ msgstr "" msgid "The default printer for hosts in this group. A host that has its own default keeps it." msgstr "" +#, fuzzy +msgid "The desired state." +msgstr "无法创建任务" + #, fuzzy msgid "The enrollment is no longer pending." msgstr "不复存在" @@ -10075,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10702,6 +10717,9 @@ msgstr "发生未知上传错误。返回代码:" msgid "Unknown action" msgstr "发生未知上传错误。返回代码:" +msgid "Unknown capability or status." +msgstr "" + #, php-format msgid "Unknown field for %s: %s" msgstr "" @@ -11219,9 +11237,15 @@ msgstr "已成功更新" msgid "What it does" msgstr "已成功更新" +msgid "What the agent did with one capability at one revision. Recorded on the host as agent.result. Same gate as poll." +msgstr "" + msgid "What the browser is shown. Replaced by an ACME renewal where one is configured." msgstr "" +msgid "What this host should look like, for the capabilities the poll listed, with the revision the poll reported. Same gate as poll." +msgstr "" + msgid "What this server itself trusts: FOG's root, plus any root imported below." msgstr "" diff --git a/packages/web/src/Agent/Enrollment.php b/packages/web/src/Agent/Enrollment.php index 0671e7a498..12ced14cba 100644 --- a/packages/web/src/Agent/Enrollment.php +++ b/packages/web/src/Agent/Enrollment.php @@ -520,7 +520,7 @@ public static function renew(Host $Host, $csrPEM) (string)$notAfter, substr($fingerprint, 0, 16) ), - 'authSource' => Audit::SOURCE_ANONYMOUS + 'authSource' => Principal::AUTH_SOURCE ] ); return $leaf . $chain; diff --git a/packages/web/src/Agent/Principal.php b/packages/web/src/Agent/Principal.php index 2c3b495008..541c7db64c 100644 --- a/packages/web/src/Agent/Principal.php +++ b/packages/web/src/Agent/Principal.php @@ -45,6 +45,12 @@ */ class Principal { + /** + * The audit authSource for a write the client certificate authorized. + * Not anonymous: the caller proved a key the server bound to a host. + */ + const AUTH_SOURCE = 'agent'; + /** * sha256 of a public key's SPKI, as enrollment stores it on the host. * diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php new file mode 100644 index 0000000000..8f62c3a78d --- /dev/null +++ b/packages/web/src/Agent/State.php @@ -0,0 +1,160 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; +use FOG\Router\Route; + +/** + * The convergence half of the protocol (design 0001 section 2): the server + * holds a desired state per host, the agent fetches it when the revision + * moves, reconciles, and reports. + * + * A capability is listed for a host when its legacy module is on for that + * host -- the global FOG_CLIENT_*_ENABLED setting and the host's resolved + * module set, exactly the two checks FOGClient makes for the old client -- + * so an admin's existing per-host and per-group module choices carry over + * unchanged. Desired state is built only for the capabilities listed, and + * the revision is a digest of that state, so "anything changed?" costs the + * poll one string compare. + * + * Results are audit rows for now (renderable, on the host): the place FOG + * already shows what happened to a host, and no schema until inventory + * needs one. + * + * @category State + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class State extends FOGBase +{ + /** + * Capability name => the legacy module short name that switches it. + */ + const CAPABILITIES = [ + 'hostname' => 'hostnamechanger' + ]; + + /** + * What the agent may report for one capability. + */ + const RESULT_STATUSES = ['applied', 'unchanged', 'pending_reboot', 'failed']; + + /** + * The capabilities this server offers this host. + * + * @param Host $Host the principal + * + * @return array capability names, in CAPABILITIES order + */ + public static function capabilities(Host $Host) + { + $global = self::getGlobalModuleStatus(); + $on = (array)Route::getIds( + 'module', + ['id' => $Host->resolvedModules()], + 'shortName' + ); + $out = []; + foreach (self::CAPABILITIES as $capability => $shortName) { + if (!empty($global[$shortName]) && in_array($shortName, $on, true)) { + $out[] = $capability; + } + } + return $out; + } + + /** + * The desired state, with its revision. + * + * @param Host $Host the principal + * + * @return array + */ + public static function desired(Host $Host) + { + $capabilities = self::capabilities($Host); + $state = ['capabilities' => $capabilities]; + if (in_array('hostname', $capabilities, true)) { + $state['hostname'] = [ + 'name' => (string)$Host->get('name'), + // The host's "Enforce Hostname | AD Join Reboots" flag: may + // the agent reboot to finish a rename. The agent's reboot + // coordinator owns the when; this is only the permission. + 'enforce' => (bool)$Host->get('enforce') + ]; + } + $state['revision'] = self::revision($state); + return $state; + } + + /** + * The revision of a desired state: a digest of everything but itself. + * + * @param array $state the desired state, revision ignored + * + * @return string 16 hex characters + */ + public static function revision(array $state) + { + unset($state['revision']); + ksort($state); + return substr(hash('sha256', (string)json_encode($state)), 0, 16); + } + + /** + * Records what the agent did with one capability. + * + * @param Host $Host the principal + * @param array $body revision, capability, status, detail + * + * @throws \RuntimeException 400 on a body that is not a result + * + * @return void + */ + public static function result(Host $Host, array $body) + { + $capability = (string)($body['capability'] ?? ''); + if (!isset(self::CAPABILITIES[$capability])) { + throw new \RuntimeException('unknown capability', 400); + } + $status = (string)($body['status'] ?? ''); + if (!in_array($status, self::RESULT_STATUSES, true)) { + throw new \RuntimeException('unknown status', 400); + } + $revision = substr(preg_replace('/[^a-f0-9]/', '', (string)($body['revision'] ?? '')), 0, 16); + $detail = substr(trim((string)($body['detail'] ?? '')), 0, Audit::MAX_DETAIL); + Audit::record( + [ + 'type' => 'agent.result', + 'subjectType' => 'host', + 'subjectID' => (int)$Host->get('id'), + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'text' => sprintf( + '%s %s at revision %s%s', + $capability, + $status, + $revision, + '' === $detail ? '' : ': ' . $detail + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } +} diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 49021f3776..7425d0c4e9 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -341,6 +341,8 @@ class Authorization extends FOGBase 'agentenroll' => null, 'agentpoll' => null, // fog-agent: gated by the client certificate in Route, not by a token 'agentrenew' => null, // fog-agent: same gate + 'agentstate' => null, // fog-agent: same gate + 'agentresult' => null, // fog-agent: same gate 'agentenrollments' => 'host.view', 'agentenrollmentdecide' => 'host.edit', // Tokens approve machines the admin has not seen, which creates diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 94e5c3c394..6fbe9c33ec 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -868,6 +868,28 @@ public function agentTokens() echo '
'; echo ''; } + /** + * The bare halves of the two ajax handlers below. FOGPageManager + * appends the Ajax suffix only after method_exists() passes for the + * bare name, so without these the posts fell through to index() and + * answered with the host list (found by the first browser run). A + * plain GET of either sub lands back on the tokens page. + * + * @return void + */ + public function createAgentToken() + { + self::redirect('?node=host&sub=agentTokens'); + } + /** + * See createAgentToken(). + * + * @return void + */ + public function deleteAgentTokens() + { + self::redirect('?node=host&sub=agentTokens'); + } /** * Mints a token from the modal. Named create* so the permission is * host.create: a token creates hosts. diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index f0a1e1e350..5e25f4790e 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2585,6 +2585,7 @@ private static function _fixedPaths() 'type' => 'array', 'items' => ['type' => 'string'] ], + 'state_revision' => ['type' => 'string'], 'poll_interval' => ['type' => 'integer'], 'server_time' => ['type' => 'string'] ] @@ -2603,6 +2604,64 @@ private static function _fixedPaths() ] ) ], + '/agent/v1/state' => [ + 'get' => self::_op( + '', + 'agentstate', + _('FOG Agent desired state'), + _('What this host should look like, for the capabilities ' + . 'the poll listed, with the revision the poll ' + . 'reported. Same gate as poll.'), + [ + '200' => [ + 'description' => _('The desired state.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'revision' => ['type' => 'string'], + 'capabilities' => ['type' => 'array', 'items' => ['type' => 'string']], + 'hostname' => [ + 'type' => 'object', + 'properties' => [ + 'name' => ['type' => 'string'], + 'enforce' => ['type' => 'boolean'] + ] + ] + ] + ]]] + ], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')] + ] + ) + ], + '/agent/v1/result' => [ + 'post' => self::_op( + '', + 'agentresult', + _('FOG Agent capability result'), + _('What the agent did with one capability at one ' + . 'revision. Recorded on the host as agent.result. ' + . 'Same gate as poll.'), + [ + '200' => ['description' => _('Recorded.')], + '400' => ['description' => _('Unknown capability or status.')], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')] + ], + [], + [ + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'required' => ['revision', 'capability', 'status'], + 'properties' => [ + 'revision' => ['type' => 'string'], + 'capability' => ['type' => 'string', 'enum' => ['hostname']], + 'status' => ['type' => 'string', 'enum' => ['applied', 'unchanged', 'pending_reboot', 'failed']], + 'detail' => ['type' => 'string'] + ] + ]]] + ] + ) + ], '/agent/v1/renew' => [ 'post' => self::_op( '', diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index c02e385b05..d83ef5c731 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -1552,6 +1552,8 @@ protected static function defineRoutes(\FastRoute\RouteCollector $r) self::_registerRoute($r, 'POST', '/agent/v1/enroll', [__CLASS__, 'agentEnroll'], 'agentenroll'); self::_registerRoute($r, 'POST', '/agent/v1/poll', [__CLASS__, 'agentPoll'], 'agentpoll'); self::_registerRoute($r, 'POST', '/agent/v1/renew', [__CLASS__, 'agentRenew'], 'agentrenew'); + self::_registerRoute($r, 'GET', '/agent/v1/state', [__CLASS__, 'agentState'], 'agentstate'); + self::_registerRoute($r, 'POST', '/agent/v1/result', [__CLASS__, 'agentResult'], 'agentresult'); self::_registerRoute($r, 'GET', '/agent/enrollments', [__CLASS__, 'agentEnrollments'], 'agentenrollments'); self::_registerRoute($r, 'POST', '/agent/enrollment/[i:id]/[*:action]', [__CLASS__, 'agentEnrollmentDecide'], 'agentenrollmentdecide'); self::_registerRoute($r, 'GET', '/agent/tokens', [__CLASS__, 'agentTokens'], 'agenttokens'); @@ -2887,6 +2889,7 @@ public static function agentPoll() '', $fields ); + $desired = \FOG\Agent\State::desired($Host); HTTPResponseCodes::breakHead( HTTPResponseCodes::HTTP_OK, json_encode( @@ -2897,15 +2900,56 @@ public static function agentPoll() 'id' => (int)$Host->get('id'), 'name' => (string)$Host->get('name'), ], - // Grows as capabilities land server-side; an empty - // list is a valid answer and the agent idles on it. - 'capabilities' => [], + // What this server offers this host, and the revision + // of its desired state: the agent fetches the state + // only when this moves. An empty list is a valid + // answer and the agent idles on it. + 'capabilities' => $desired['capabilities'], + 'state_revision' => $desired['revision'], 'poll_interval' => 300, 'server_time' => self::niceDate()->format('c'), ] ) ); } + /** + * fog-agent's desired state: what this host should look like. + * + * Fetched when the poll's state_revision moved. Only the capabilities + * the poll listed appear, so a server that does not offer something + * simply never describes it. + * + * @return void + */ + public static function agentState() + { + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_OK, + json_encode(\FOG\Agent\State::desired(self::$agentHost)) + ); + } + /** + * fog-agent's report of what it did with one capability. + * + * @return void + */ + public static function agentResult() + { + $body = self::_jsonBody(); + try { + \FOG\Agent\State::result(self::$agentHost, (array)$body); + } catch (\RuntimeException $e) { + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_BAD_REQUEST, + json_encode(['status' => 'error', 'error' => $e->getMessage()]) + ); + return; + } + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_OK, + json_encode(['status' => 'ok']) + ); + } /** * fog-agent's certificate renewal, over the certificate being renewed. * From 667600b067fdce022f9fd87a2e73ed9f4ff0da93 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 15:22:17 +0000 Subject: [PATCH 007/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 77d0c2dcbe..7ead25c542 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,6 +10095,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 3c62115db7..e22d608b87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,6 +10104,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 9c91c2b963..cc3f86d120 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,6 +10265,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7503aa7adb..64b59f5596 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,6 +10096,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 908d094785..7010a6a1be 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index a80cc0c333..5685710e13 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,6 +9809,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 41149ef67e..13356298dd 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,6 +9758,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 43156920ec..e12c192735 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,6 +8648,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 68e5dd5b23..67482f8307 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ce590f9901..694b50ba84 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 7b8f4d284e892ceeffdd64f972b8286d149397d7 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 10:34:22 -0500 Subject: [PATCH 008/117] Agent: give agent-created hosts the default modules; clear the mint form A host created by an agent enrollment had no module rows, so State::capabilities() resolved to [] and the agent never received the hostname capability even though FOG_CLIENT_HOSTNAMECHANGER_ENABLED was on. Resolver::resolveModules has no default tier: a host only has the modules explicitly attached to it or granted through a group. Match Boot\Registration and HostManagement::addPost by attaching the isDefault modules at creation. Also clear the token name field when the mint modal opens, so the second token does not silently inherit the first one's name. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- packages/web/management/js/fog/host/fog.host.agentTokens.js | 3 +++ .../languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/management/languages/messages.pot | 1 - .../languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Agent/Enrollment.php | 6 ++++++ 12 files changed, 9 insertions(+), 10 deletions(-) diff --git a/packages/web/management/js/fog/host/fog.host.agentTokens.js b/packages/web/management/js/fog/host/fog.host.agentTokens.js index b821f18c9c..4bb18b1421 100644 --- a/packages/web/management/js/fog/host/fog.host.agentTokens.js +++ b/packages/web/management/js/fog/host/fog.host.agentTokens.js @@ -71,6 +71,9 @@ }); mintBtn.on('click', function() { + // Each token is a fresh credential: start the form clean rather than + // carrying the previous token's name into the next mint. + $('#tokenName').val(''); mintModal.modal('show'); }); confirmMint.on('click', function() { diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 7ead25c542..77d0c2dcbe 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,7 +10095,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index e22d608b87..3c62115db7 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,7 +10104,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cc3f86d120..9c91c2b963 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,7 +10265,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 64b59f5596..7503aa7adb 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,7 +10096,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 7010a6a1be..908d094785 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5685710e13..a80cc0c333 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,7 +9809,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 13356298dd..41149ef67e 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,7 +9758,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e12c192735..43156920ec 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,7 +8648,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 67482f8307..68e5dd5b23 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 694b50ba84..ce590f9901 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/Enrollment.php b/packages/web/src/Agent/Enrollment.php index 12ced14cba..5c3b37a542 100644 --- a/packages/web/src/Agent/Enrollment.php +++ b/packages/web/src/Agent/Enrollment.php @@ -437,6 +437,12 @@ private static function _createPendingHost(AgentEnrollment $Row, array $identity ->set('description', _('Pending Registration created by FOG_AGENT')) ->set('imageID', null) ->set('pending', '1') + // The default modules, as Boot\Registration and the host add + // form attach them: Resolver::resolveModules() has no default + // tier, so a host created without these has every capability + // off until an admin visits its Modules tab. Found when the + // first agent-created host polled and got no capabilities. + ->set('modules', Route::getIds('module', ['isDefault' => 1])) ->addPriMAC(array_shift($macs)); $Host->save(); $hostID = (int)$Host->get('id'); From db8ee075d70ed52d6ca32c98d6aa4ad555c0c0bb Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 15:35:08 +0000 Subject: [PATCH 009/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 77d0c2dcbe..7ead25c542 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,6 +10095,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 3c62115db7..e22d608b87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,6 +10104,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 9c91c2b963..cc3f86d120 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,6 +10265,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7503aa7adb..64b59f5596 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,6 +10096,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 908d094785..7010a6a1be 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index a80cc0c333..5685710e13 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,6 +9809,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 41149ef67e..13356298dd 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,6 +9758,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 43156920ec..e12c192735 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,6 +8648,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 68e5dd5b23..67482f8307 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ce590f9901..694b50ba84 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 17ee8613e6e67461d217d8ef6c08a8327d0c7788 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 11:27:10 -0500 Subject: [PATCH 010/117] Agent: taskreboot capability and the reboot policy blocks Desired state gains a `task` block (capability taskreboot, module taskreboot): the task waiting for the host in a state that needs it to boot into FOS, the same answer Client\Jobs gives the old client, with FOG_TASK_FORCE_REBOOT as its force flag. Present only while one waits, so queueing or canceling a task moves the revision. A `reboot` block carries FOG_GRACE_TIMEOUT with any non-empty capability list. The agent's reboot coordinator reports its decisions as results with capability `reboot`, so results now accept that alongside the capabilities proper. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Agent/State.php | 37 ++++++++++++++++++- packages/web/src/Router/OpenAPI.php | 15 ++++++++ 12 files changed, 50 insertions(+), 12 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 7ead25c542..77d0c2dcbe 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,7 +10095,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index e22d608b87..3c62115db7 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,7 +10104,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cc3f86d120..9c91c2b963 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,7 +10265,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 64b59f5596..7503aa7adb 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,7 +10096,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 7010a6a1be..908d094785 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5685710e13..a80cc0c333 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,7 +9809,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 13356298dd..41149ef67e 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,7 +9758,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e12c192735..43156920ec 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,7 +8648,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 67482f8307..68e5dd5b23 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 694b50ba84..ce590f9901 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 8f62c3a78d..30583fad3d 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -47,9 +47,16 @@ class State extends FOGBase * Capability name => the legacy module short name that switches it. */ const CAPABILITIES = [ - 'hostname' => 'hostnamechanger' + 'hostname' => 'hostnamechanger', + 'taskreboot' => 'taskreboot' ]; + /** + * Results the agent reports that are not a capability of their own: + * the reboot coordinator's decisions (design 0001 section 6). + */ + const RESULT_SOURCES = ['reboot']; + /** * What the agent may report for one capability. */ @@ -99,6 +106,30 @@ public static function desired(Host $Host) 'enforce' => (bool)$Host->get('enforce') ]; } + if (in_array('taskreboot', $capabilities, true)) { + // What Client\Jobs answers the old client: a task in a state + // that needs the machine to boot into FOS. Present only while + // one waits, so queueing or canceling a task moves the + // revision and the agent fetches the change on its next poll. + $Task = $Host->get('task'); + $state['task'] = null; + if ($Task->isValid() && $Task->isInitNeededTasking()) { + $state['task'] = [ + 'id' => (int)$Task->get('id'), + 'type' => (string)$Task->getTaskTypeText(), + // FOG_TASK_FORCE_REBOOT: reboot for the task even + // with users logged in. + 'force' => (bool)self::getSetting('FOG_TASK_FORCE_REBOOT') + ]; + } + } + if (count($capabilities) > 0) { + // The policy every reboot obeys, whatever asked for it: + // FOG_GRACE_TIMEOUT is the warning logged-in users get. + $state['reboot'] = [ + 'grace' => (int)self::getSetting('FOG_GRACE_TIMEOUT') + ]; + } $state['revision'] = self::revision($state); return $state; } @@ -130,7 +161,9 @@ public static function revision(array $state) public static function result(Host $Host, array $body) { $capability = (string)($body['capability'] ?? ''); - if (!isset(self::CAPABILITIES[$capability])) { + if (!isset(self::CAPABILITIES[$capability]) + && !in_array($capability, self::RESULT_SOURCES, true) + ) { throw new \RuntimeException('unknown capability', 400); } $status = (string)($body['status'] ?? ''); diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index 5e25f4790e..52406f75bb 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2626,6 +2626,21 @@ private static function _fixedPaths() 'name' => ['type' => 'string'], 'enforce' => ['type' => 'boolean'] ] + ], + 'task' => [ + 'type' => 'object', + 'nullable' => true, + 'properties' => [ + 'id' => ['type' => 'integer'], + 'type' => ['type' => 'string'], + 'force' => ['type' => 'boolean'] + ] + ], + 'reboot' => [ + 'type' => 'object', + 'properties' => [ + 'grace' => ['type' => 'integer'] + ] ] ] ]]] From 290ebb73f3f11aba4ccee1162bdf2d3de0da9c44 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 16:28:02 +0000 Subject: [PATCH 011/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 77d0c2dcbe..7ead25c542 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,6 +10095,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 3c62115db7..e22d608b87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,6 +10104,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 9c91c2b963..cc3f86d120 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,6 +10265,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7503aa7adb..64b59f5596 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,6 +10096,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 908d094785..7010a6a1be 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index a80cc0c333..5685710e13 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,6 +9809,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 41149ef67e..13356298dd 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,6 +9758,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 43156920ec..e12c192735 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,6 +8648,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 68e5dd5b23..67482f8307 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ce590f9901..694b50ba84 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 79806258eac8632a89e62abe158767a2d66867be Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 11:56:42 -0500 Subject: [PATCH 012/117] Agent: a certificate-bound agent request is a machine request On a server with sites configured, every Route::getIds on a scoped node inside an /agent/v1/ request answered empty: the site boundary asks which objects THIS USER may see, an agent request has no user, and Authorization only lifts the boundary for an entry point that declares FOG_MACHINE_REQUEST, as every service/*.php does. The agent's desired state therefore carried task: null with a task queued, and group-granted modules would have dropped out the same way. Declare it for the prefix, after _agentPrincipal() has bound a host and after the 401 for an unbound one, so it stays a positive statement about the entry point and a route that lost its 401 still would not get it. Guard (k) in route-read-path-guards anchors the block and fails if the declaration is removed or moved above the 401. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Router/Route.php | 13 +++++++++ tests/route-read-path-guards.test.php | 29 +++++++++++++++++++ 12 files changed, 42 insertions(+), 10 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 7ead25c542..77d0c2dcbe 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,7 +10095,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index e22d608b87..3c62115db7 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,7 +10104,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cc3f86d120..9c91c2b963 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,7 +10265,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 64b59f5596..7503aa7adb 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,7 +10096,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 7010a6a1be..908d094785 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5685710e13..a80cc0c333 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,7 +9809,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 13356298dd..41149ef67e 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,7 +9758,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e12c192735..43156920ec 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,7 +8648,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 67482f8307..68e5dd5b23 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 694b50ba84..ce590f9901 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,7 +10091,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index d83ef5c731..3449da0cf7 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -943,6 +943,19 @@ public function __construct() json_encode(['error' => 'client certificate required']) ); } + // A bound agent is a machine principal. The site boundary + // answers "which objects may THIS USER see", and this request + // has no user by design, so without the declaration every + // scoped read under the prefix -- the host's own task, its + // group grants -- answers empty on a server with sites + // configured (Authorization::_hasNoPrincipal). Declared here, + // AFTER the certificate bound a host, for the reason + // service/*.php declare it at the top of the file: it is a + // positive statement about the entry point, never an + // inference from a missing user, so a route that lost its 401 + // still would not get it. Every handler under the prefix + // reads through self::$agentHost and nothing else. + define('FOG_MACHINE_REQUEST', true); // Authenticated by certificate: the token and session tests // below are for humans and API tokens and would only 401 it. $isunauth = true; diff --git a/tests/route-read-path-guards.test.php b/tests/route-read-path-guards.test.php index 6f7803418a..e610bbe8de 100644 --- a/tests/route-read-path-guards.test.php +++ b/tests/route-read-path-guards.test.php @@ -1162,6 +1162,35 @@ function statementsFor($db, $class) FogTestHarness::setStatic($cls, 'FOGUser', $savedUser); } +// (k) The fog-agent prefix declares FOG_MACHINE_REQUEST, and only after the +// certificate has bound a host. +// +// (j) is what an agent request would otherwise get: no user, so every +// scoped read under /agent/v1/ -- the host's own task, its group grants -- +// answers empty on a server with sites configured. The declaration is +// the cure, and its position is the safety: before the 401 it would be an +// inference from a missing user, exactly what _hasNoPrincipal's comment +// rules out. The whole dispatch block is anchored so a rewrite of either +// half is a visible failure here rather than a silent policy change. +$routeSrc = (string)file_get_contents(__DIR__ . '/../packages/web/src/Router/Route.php'); +$agentBlockStart = strpos($routeSrc, "'agent/v1/')"); +$agentBlockEnd = false === $agentBlockStart ? false : strpos($routeSrc, '$isunauth = true;', $agentBlockStart); +$agentBlock = ( + false === $agentBlockStart || false === $agentBlockEnd ? + '' : + substr($routeSrc, $agentBlockStart, $agentBlockEnd - $agentBlockStart) +); +$agent401 = strpos($agentBlock, 'HTTP_UNAUTHORIZED'); +$agentDefine = strpos($agentBlock, "define('FOG_MACHINE_REQUEST', true);"); +$t->check( + 'the agent prefix declares FOG_MACHINE_REQUEST', + false !== $agentDefine +); +$t->check( + 'the agent prefix declares it only after the 401 on an unbound host', + false !== $agent401 && false !== $agentDefine && $agent401 < $agentDefine +); + /* * The null-vs-[] distinction, asserted directly on the filter as well. The * end-to-end cases above would both pass if listem() stopped calling the From 1da3999edd284224aac1efa2727c1789ae908cea Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 16:57:41 +0000 Subject: [PATCH 013/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 77d0c2dcbe..7ead25c542 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10095,6 +10095,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 3c62115db7..e22d608b87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10104,6 +10104,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 9c91c2b963..cc3f86d120 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10265,6 +10265,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7503aa7adb..64b59f5596 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10096,6 +10096,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 908d094785..7010a6a1be 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index a80cc0c333..5685710e13 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9809,6 +9809,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 41149ef67e..13356298dd 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9758,6 +9758,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 43156920ec..e12c192735 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8648,6 +8648,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 68e5dd5b23..67482f8307 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ce590f9901..694b50ba84 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10091,6 +10091,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 9618f9d127bb7de6fc89de8c18396ac1bf1263ba Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 13:04:57 -0500 Subject: [PATCH 014/117] Agent: snapins as payload-only software, shared with the legacy client Capability `snapin` (module snapinclient) puts the host's snapin queue in the desired state exactly as the server tasked it -- snapinTasks in sequence order, from the resolver's host-first, then groups, deduplicated list -- with each task's file, size, sha512, arguments, interpreter, timeout, reboot or shutdown flag and the job's abort-on-fail. Two routes serve it: GET /agent/v1/snapin/{id}/file streams the payload from the storage node over the web tier's own FTP session and marks the task in progress; POST /agent/v1/snapin/{id}/result closes it with the exit code and output tail, cancels the rest of a job that aborts on failure, ends the job after its last task, and audits agent.result on the host. Both check the task belongs to the host's own job, the legacy Aisle 009 guard, with one message for "missing" and "not yours". Agent\Snapins::stream() and close() ARE the legacy SnapinClient's _downloadfile and _closeout bodies; the legacy methods keep their input parsing and call the shared code, so both clients mark tasks and end jobs identically and cannot drift. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- bin/psr4-scan.php | 1 + .../de_DE.UTF-8/LC_MESSAGES/messages.po | 51 ++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 51 ++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 53 ++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 51 ++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 51 ++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 47 +- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 50 ++- .../web/management/languages/messages.pot | 34 +- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 51 ++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 51 ++- packages/web/src/Agent/Snapins.php | 409 ++++++++++++++++++ packages/web/src/Agent/State.php | 9 +- packages/web/src/Auth/Authorization.php | 2 + packages/web/src/Client/SnapinClient.php | 203 +-------- packages/web/src/Router/OpenAPI.php | 68 +++ packages/web/src/Router/Route.php | 64 +++ 17 files changed, 932 insertions(+), 314 deletions(-) create mode 100644 packages/web/src/Agent/Snapins.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 217f744346..4cf2551520 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -209,6 +209,7 @@ 'Principal' => 'Agent', 'Token' => 'Agent', 'State' => 'Agent', + 'Snapins' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 7ead25c542..dcc00e1364 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -1659,10 +1659,6 @@ msgstr "kann keine Verbindung aufbauen" msgid "Cannot connect to database" msgstr "Kann keine Verbindung zur Datenbank herstellen" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Verbindung zum FTP-Server nicht möglich" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2130,10 +2126,6 @@ msgstr "Tmp-Datei konnte nicht gelesen werden." msgid "Could not read local file" msgstr "Temporäre Datei konnte nicht gelesen werden." -#, fuzzy -msgid "Could not read snapin file" -msgstr "Snapin-Datei konnte nicht gelesen werden." - #, fuzzy msgid "Could not read the database structure" msgstr "Erstellen eines Snapin-Jobs fehlgeschlagen" @@ -3176,6 +3168,13 @@ msgstr "wurde abgebrochen" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Client" msgstr "FOG-Client-Wiki" @@ -4968,9 +4967,6 @@ msgstr "Ungültiges Snapin-Tasking-Objekt" msgid "Invalid Storage Group" msgstr "Ungültige Speichergruppe" -msgid "Invalid Storage Node" -msgstr "Ungültiger Speicherknoten" - #, fuzzy msgid "Invalid Tasking" msgstr "Ungültiger Vorgang" @@ -6488,6 +6484,10 @@ msgstr "Snapin geschützt" msgid "No snapins associated with this group as master" msgstr "Keine Snapins mit dieser Gruppe als Master verbunden" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Hinzufügen eines Speicherknotens fehlgeschlagen!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "da innerhalb diese Speichergruppe einer aktiviert ist" @@ -6611,6 +6611,10 @@ msgstr "Nicht registrierte Hosts" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "Keinen aktiven Task gefunden für Host" + msgid "Not a number" msgstr "Keine Zahl" @@ -9856,9 +9860,15 @@ msgstr "Fehler beim Erstellen eines Tasks" msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9982,6 +9992,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Home-Verzeichnis speichern" @@ -10092,10 +10105,13 @@ msgstr "Der einer/mehrere Speichergruppen zugeordnete Speicherknoten ist ungült msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Dieser Host ist bereits vorhanden." + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12647,6 +12663,10 @@ msgstr "" #~ msgid "Can not redeclare route" #~ msgstr "Route kann nicht nochmal neu definiert werden" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Verbindung zum FTP-Server nicht möglich" + #~ msgid "Check that database is running" #~ msgstr "Überprüfen Sie, ob die Datenbank ausgeführt wird" @@ -12654,6 +12674,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Client Einstellungen" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "Snapin-Datei konnte nicht gelesen werden." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Neuen Schlüssel erstellen" @@ -12988,6 +13012,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "Installierte Plugins" +#~ msgid "Invalid Storage Node" +#~ msgstr "Ungültiger Speicherknoten" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "Ungültiger Typ" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index e22d608b87..55f04ebf04 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -1663,10 +1663,6 @@ msgstr "Cannot connect to node." msgid "Cannot connect to database" msgstr "Cannot connect to database" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Cannot connect to database" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2132,10 +2128,6 @@ msgstr "Could not read tmp file." msgid "Could not read local file" msgstr "Could not read temp file" -#, fuzzy -msgid "Could not read snapin file" -msgstr "Could not read tmp file." - #, fuzzy msgid "Could not read the database structure" msgstr "Failed to create Snapin Job" @@ -3178,6 +3170,13 @@ msgstr "has been successfully updated" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "No file was uploaded" + #, fuzzy msgid "FOG Client" msgstr "FOG Client Wiki" @@ -4970,9 +4969,6 @@ msgstr "Invalid Snapin Tasking" msgid "Invalid Storage Group" msgstr "Invalid Storage Group" -msgid "Invalid Storage Node" -msgstr "Invalid Storage Node" - #, fuzzy msgid "Invalid Tasking" msgstr "Invalid task" @@ -6500,6 +6496,10 @@ msgstr "Snapin updated" msgid "No snapins associated with this group as master" msgstr "There are no snapins associated with this host" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Add snapin failed!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "Could not find a Storage Node Is there one enabled within this Storage Group" @@ -6623,6 +6623,10 @@ msgstr "Not Registered Hosts" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "No Active Task found for Host" + msgid "Not a number" msgstr "Not a number" @@ -9865,9 +9869,15 @@ msgstr "Failed to create task" msgid "The enrollment is no longer pending." msgstr " no longer exists" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9991,6 +10001,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Directory" @@ -10101,10 +10114,13 @@ msgstr "The storage groups associated storage node is not valid" msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Printer already exists" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12644,6 +12660,10 @@ msgstr "" #~ msgid "CA private key" #~ msgstr "Private key failed" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Cannot connect to database" + #~ msgid "Check that database is running" #~ msgstr "Check that database is running" @@ -12651,6 +12671,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Settings" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "Could not read tmp file." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Create New %s" @@ -12979,6 +13003,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "Installed Plugins" +#~ msgid "Invalid Storage Node" +#~ msgstr "Invalid Storage Node" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "Invalid type" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cc3f86d120..dc9a04c7ce 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -1680,10 +1680,6 @@ msgstr "" msgid "Cannot connect to database" msgstr "" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Error: No se pudo descargar kernel" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2152,10 +2148,6 @@ msgstr "No se pudo leer el archivo tmp." msgid "Could not read local file" msgstr "No se pudo leer el archivo temporal" -#, fuzzy -msgid "Could not read snapin file" -msgstr "No se pudo leer el archivo tmp." - #, fuzzy msgid "Could not read the database structure" msgstr "No se pudo crear Complemento de empleo" @@ -3214,6 +3206,13 @@ msgstr "se ha actualizado correctamente" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Ningún archivo fue subido" + #, fuzzy msgid "FOG Client" msgstr "FOG Wiki Cliente" @@ -5051,10 +5050,6 @@ msgstr "Tipo de tarea no válida" msgid "Invalid Storage Group" msgstr "Grupo de almacenamiento" -#, fuzzy -msgid "Invalid Storage Node" -msgstr "nodo de almacenamiento" - #, fuzzy msgid "Invalid Tasking" msgstr "tarea no válido" @@ -6609,6 +6604,10 @@ msgstr "Nombre snapin" msgid "No snapins associated with this group as master" msgstr "No hay snapins asociados con este anfitrión" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Añadir fallidos SNAPin!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "Grupo de almacenamiento primaria" @@ -6735,6 +6734,10 @@ msgstr "Registrado" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "No se encontró Clase FOGPage para este nodo" + msgid "Not a number" msgstr "No un número" @@ -10025,9 +10028,15 @@ msgstr "No se pudo crear la tarea" msgid "The enrollment is no longer pending." msgstr "" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -10152,6 +10161,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Directorio" @@ -10262,10 +10274,13 @@ msgstr "" msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Impresora ya existe" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12800,6 +12815,10 @@ msgstr "" #~ msgid "CA private key" #~ msgstr "clave privada fracasó" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Error: No se pudo descargar kernel" + #, fuzzy #~ msgid "Check that database is running" #~ msgstr "Compruebe que la base de datos se está ejecutando" @@ -12808,6 +12827,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Configuración Tecla" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "No se pudo leer el archivo tmp." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Crear nuevo grupo" @@ -13146,6 +13169,10 @@ msgstr "" #~ msgid "Install" #~ msgstr "Instalador inteligente (recomendado)" +#, fuzzy +#~ msgid "Invalid Storage Node" +#~ msgstr "nodo de almacenamiento" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "tipo no válido" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 64b59f5596..065909ca8c 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -1659,10 +1659,6 @@ msgstr "kann keine Verbindung aufbauen" msgid "Cannot connect to database" msgstr "Kann keine Verbindung zur Datenbank herstellen" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Verbindung zum FTP-Server nicht möglich" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2130,10 +2126,6 @@ msgstr "Tmp-Datei konnte nicht gelesen werden." msgid "Could not read local file" msgstr "Temporäre Datei konnte nicht gelesen werden." -#, fuzzy -msgid "Could not read snapin file" -msgstr "Snapin-Datei konnte nicht gelesen werden." - #, fuzzy msgid "Could not read the database structure" msgstr "Erstellen eines Snapin-Jobs fehlgeschlagen" @@ -3176,6 +3168,13 @@ msgstr "wurde abgebrochen" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Keine Datei wurde hochgeladen" + #, fuzzy msgid "FOG Client" msgstr "FOG-Client-Wiki" @@ -4969,9 +4968,6 @@ msgstr "Ungültiges Snapin-Tasking-Objekt" msgid "Invalid Storage Group" msgstr "Ungültige Speichergruppe" -msgid "Invalid Storage Node" -msgstr "Ungültiger Speicherknoten" - #, fuzzy msgid "Invalid Tasking" msgstr "Ungültiger Vorgang" @@ -6489,6 +6485,10 @@ msgstr "Snapin geschützt" msgid "No snapins associated with this group as master" msgstr "Keine Snapins mit dieser Gruppe als Master verbunden" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Hinzufügen eines Speicherknotens fehlgeschlagen!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "da innerhalb diese Speichergruppe einer aktiviert ist" @@ -6612,6 +6612,10 @@ msgstr "Nicht registrierte Hosts" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "Keinen aktiven Task gefunden für Host" + msgid "Not a number" msgstr "Keine Zahl" @@ -9857,9 +9861,15 @@ msgstr "Fehler beim Erstellen eines Tasks" msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9983,6 +9993,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Home-Verzeichnis speichern" @@ -10093,10 +10106,13 @@ msgstr "Der einer/mehrere Speichergruppen zugeordnete Speicherknoten ist ungült msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Dieser Host ist bereits vorhanden." + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12648,6 +12664,10 @@ msgstr "" #~ msgid "Can not redeclare route" #~ msgstr "Route kann nicht nochmal neu definiert werden" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Verbindung zum FTP-Server nicht möglich" + #~ msgid "Check that database is running" #~ msgstr "Überprüfen Sie, ob die Datenbank ausgeführt wird" @@ -12655,6 +12675,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Client Einstellungen" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "Snapin-Datei konnte nicht gelesen werden." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Neuen Schlüssel erstellen" @@ -12989,6 +13013,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "Installierte Plugins" +#~ msgid "Invalid Storage Node" +#~ msgstr "Ungültiger Speicherknoten" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "Ungültiger Typ" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 7010a6a1be..a89256eb64 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -1664,10 +1664,6 @@ msgstr "Impossible de se connecter au noeud." msgid "Cannot connect to database" msgstr "Impossible de se connecter à la base de données" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Impossible de se connecter à la base de données" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2134,10 +2130,6 @@ msgstr "Impossible de lire le fichier tmp." msgid "Could not read local file" msgstr "Impossible de lire le fichier temporaire" -#, fuzzy -msgid "Could not read snapin file" -msgstr "Impossible de lire le fichier tmp." - #, fuzzy msgid "Could not read the database structure" msgstr "Échec de la création d'emploi Snapin" @@ -3179,6 +3171,13 @@ msgstr "a été mis à jour avec succès" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Aucun fichier a été téléchargé" + #, fuzzy msgid "FOG Client" msgstr "FOG client Wiki" @@ -4971,9 +4970,6 @@ msgstr "Invalid Snapin Tasking" msgid "Invalid Storage Group" msgstr "Groupe de stockage non valide" -msgid "Invalid Storage Node" -msgstr "Invalid Storage Node" - #, fuzzy msgid "Invalid Tasking" msgstr "tâche non valide" @@ -6487,6 +6483,10 @@ msgstr "snapin mise à jour" msgid "No snapins associated with this group as master" msgstr "Il n'y a pas snapins associés à cet hôte" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Ajouter SnapIn a échoué!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "Impossible de trouver un nœud de stockage est-il un permis dans ce groupe de stockage" @@ -6610,6 +6610,10 @@ msgstr "Hosts Pas encore inscrit" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "Aucune tâche active trouvée pour Host" + msgid "Not a number" msgstr "Pas un certain nombre" @@ -9850,9 +9854,15 @@ msgstr "Impossible de créer la tâche" msgid "The enrollment is no longer pending." msgstr " n'existe plus" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9976,6 +9986,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Annuaire" @@ -10086,10 +10099,13 @@ msgstr "Le nœud de stockage des groupes de stockage associé est pas valide" msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Imprimante existe déjà" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12630,6 +12646,10 @@ msgstr "" #~ msgid "CA private key" #~ msgstr "La clé privée a échoué" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Impossible de se connecter à la base de données" + #~ msgid "Check that database is running" #~ msgstr "Vérifiez que la base de données est en cours d'exécution" @@ -12637,6 +12657,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Paramètres" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "Impossible de lire le fichier tmp." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Créer un nouveau %s" @@ -12965,6 +12989,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "Plugins installés" +#~ msgid "Invalid Storage Node" +#~ msgstr "Invalid Storage Node" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "Type non valide" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5685710e13..7eec626466 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -1622,9 +1622,6 @@ msgstr "Impossibile connettersi" msgid "Cannot connect to database" msgstr "Non è possibile connettersi al database" -msgid "Cannot connect to ftp server" -msgstr "Impossibile connettersi al server ftp" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2076,9 +2073,6 @@ msgstr "Impossibile leggere il file tmp." msgid "Could not read local file" msgstr "Impossibile leggere il file temporaneo" -msgid "Could not read snapin file" -msgstr "Impossibile leggere il file snapin" - #, fuzzy msgid "Could not read the database structure" msgstr "Impossibile creare Snapin lavoro" @@ -3105,6 +3099,13 @@ msgstr "è stato cancellato" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Nessun file è stato caricato" + #, fuzzy msgid "FOG Client" msgstr "FOG client Wiki" @@ -4838,9 +4839,6 @@ msgstr "Oggetto Snapin Tasking non valido" msgid "Invalid Storage Group" msgstr "Gruppo di archiviazione non valido" -msgid "Invalid Storage Node" -msgstr "Non valido Storage Node" - #, fuzzy msgid "Invalid Tasking" msgstr "Compito non valido!" @@ -6307,6 +6305,10 @@ msgstr "Snapin Protetto" msgid "No snapins associated with this group as master" msgstr "Nessun snapins associato a questo gruppo come master" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Aggiunta nodo di archiviazione fallita!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "c'è uno abilitato all'interno di questo gruppo di archiviazione" @@ -6425,6 +6427,10 @@ msgstr "Host non registrati" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "Nessun compito attivo trovato per Host" + msgid "Not a number" msgstr "Non è un numero" @@ -9571,9 +9577,15 @@ msgstr "Impossibile creare un'attività" msgid "The enrollment is no longer pending." msgstr "non è più in esecuzione" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9696,6 +9708,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "directory" @@ -9806,10 +9821,13 @@ msgstr "Il nodo di archiviazione gruppi di archiviazione associato non è valido msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Questo host esiste già" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12271,6 +12289,9 @@ msgstr "" #~ msgid "Can not redeclare route" #~ msgstr "Impossibile ridichiarare il percorso" +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Impossibile connettersi al server ftp" + #~ msgid "Check that database is running" #~ msgstr "Controllare che database è in esecuzione" @@ -12278,6 +12299,9 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Impostazioni Client" +#~ msgid "Could not read snapin file" +#~ msgstr "Impossibile leggere il file snapin" + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Crea nuovo %s" @@ -12607,6 +12631,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "plugin installati" +#~ msgid "Invalid Storage Node" +#~ msgstr "Non valido Storage Node" + #~ msgid "Invalid Type" #~ msgstr "Tipo non valido" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 13356298dd..2133a476ea 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -1604,9 +1604,6 @@ msgstr "接続できません:" msgid "Cannot connect to database" msgstr "データベースに接続できません" -msgid "Cannot connect to ftp server" -msgstr "FTP サーバーに接続できません" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2060,9 +2057,6 @@ msgstr "一時ファイルを読み取れませんでした。" msgid "Could not read local file" msgstr "スナップインファイルを読み取れませんでした" -msgid "Could not read snapin file" -msgstr "スナップインファイルを読み取れませんでした" - #, fuzzy msgid "Could not read the database structure" msgstr "スナップインジョブの作成に失敗しました" @@ -3090,6 +3084,13 @@ msgstr "強制終了されました" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "ファイルはアップロードされませんでした" + msgid "FOG Client" msgstr "FOG クライアント" @@ -4804,9 +4805,6 @@ msgstr "無効なスナップインタスクオブジェクト" msgid "Invalid Storage Group" msgstr "無効なストレージグループ" -msgid "Invalid Storage Node" -msgstr "無効なストレージノード" - msgid "Invalid Tasking" msgstr "無効なタスク" @@ -6273,6 +6271,10 @@ msgstr "関連付けられたノードがありません" msgid "No snapins associated with this group as master" msgstr "このグループにマスターとして関連付けられたスナップインはありません" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "ストレージノードの更新に失敗しました!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "このストレージグループ内に有効なものがあるか" @@ -6392,6 +6394,10 @@ msgstr "未登録ホスト" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "このホストの実行中タスクは見つかりません" + msgid "Not a number" msgstr "数値ではありません" @@ -9517,9 +9523,15 @@ msgstr "タスクの作成に失敗しました" msgid "The enrollment is no longer pending." msgstr "選択したプリンターを追加" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9644,6 +9656,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "これは既に別のイメージで使用されています" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "ディレクトリ" @@ -9755,10 +9770,13 @@ msgstr "ストレージグループに関連付けられたストレージノー msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "このホストは既に存在します" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12333,6 +12351,9 @@ msgstr "" #~ msgid "Cannot cancel tasks this way" #~ msgstr "この方法ではタスクをキャンセルできません" +#~ msgid "Cannot connect to ftp server" +#~ msgstr "FTP サーバーに接続できません" + #~ msgid "Cannot create tasking as image is not enabled" #~ msgstr "イメージが有効ではないためタスクを作成できません" @@ -12405,6 +12426,9 @@ msgstr "" #~ msgid "Conflicting path/file" #~ msgstr "競合するパス/ファイル" +#~ msgid "Could not read snapin file" +#~ msgstr "スナップインファイルを読み取れませんでした" + #~ msgid "Create New Access Control Role" #~ msgstr "新しいアクセス制御ロールを作成" @@ -13127,6 +13151,9 @@ msgstr "" #~ msgid "Invalid Plugin Passed" #~ msgstr "無効なプラグインが渡されました" +#~ msgid "Invalid Storage Node" +#~ msgstr "無効なストレージノード" + #~ msgid "Invalid Task Type" #~ msgstr "無効なタスクタイプ" @@ -13956,9 +13983,6 @@ msgstr "" #~ msgid "Storage Node General" #~ msgstr "ストレージノード全般" -#~ msgid "Storage Node update failed!" -#~ msgstr "ストレージノードの更新に失敗しました!" - #~ msgid "SubnetGroup General" #~ msgstr "サブネットグループ全般" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e12c192735..663c9cc986 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -1442,9 +1442,6 @@ msgstr "" msgid "Cannot connect to database" msgstr "" -msgid "Cannot connect to ftp server" -msgstr "" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -1846,9 +1843,6 @@ msgstr "" msgid "Could not read local file" msgstr "" -msgid "Could not read snapin file" -msgstr "" - msgid "Could not read the database structure" msgstr "" @@ -2743,6 +2737,12 @@ msgstr "" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +msgid "FOG Agent snapin result" +msgstr "" + msgid "FOG Client" msgstr "" @@ -4257,9 +4257,6 @@ msgstr "" msgid "Invalid Storage Group" msgstr "" -msgid "Invalid Storage Node" -msgstr "" - msgid "Invalid Tasking" msgstr "" @@ -5553,6 +5550,9 @@ msgstr "" msgid "No snapins associated with this group as master" msgstr "" +msgid "No storage node can serve the file." +msgstr "" + msgid "No storage nodes assigned to this storage group" msgstr "" @@ -5661,6 +5661,9 @@ msgstr "" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +msgid "Not a live task of this host's job." +msgstr "" + msgid "Not a number" msgstr "" @@ -8427,9 +8430,15 @@ msgstr "" msgid "The enrollment is no longer pending." msgstr "" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -8545,6 +8554,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + msgid "The plugin directory" msgstr "" @@ -8645,10 +8657,12 @@ msgstr "" msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +msgid "The task was already closed." +msgstr "" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 67482f8307..8ed0c03df1 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -1663,10 +1663,6 @@ msgstr "Não é possível ligar para o nó." msgid "Cannot connect to database" msgstr "Não é possível conectar ao banco de dados" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "Não é possível conectar ao banco de dados" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2133,10 +2129,6 @@ msgstr "Não foi possível ler o arquivo tmp." msgid "Could not read local file" msgstr "Não foi possível ler arquivo temporário" -#, fuzzy -msgid "Could not read snapin file" -msgstr "Não foi possível ler o arquivo tmp." - #, fuzzy msgid "Could not read the database structure" msgstr "Falha ao criar Snapin Job" @@ -3178,6 +3170,13 @@ msgstr "foi atualizado com sucesso" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "Nenhum arquivo foi transferido" + #, fuzzy msgid "FOG Client" msgstr "FOG Cliente Wiki" @@ -4970,9 +4969,6 @@ msgstr "Inválida Snapin Tasking" msgid "Invalid Storage Group" msgstr "Grupo de armazenamento inválido" -msgid "Invalid Storage Node" -msgstr "Inválida Storage Node" - #, fuzzy msgid "Invalid Tasking" msgstr "tarefa inválido" @@ -6487,6 +6483,10 @@ msgstr "Snapin atualizada" msgid "No snapins associated with this group as master" msgstr "Não há snapins associados a este alojamento" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "Adicionar snap-in falhou!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "Não foi possível encontrar um nó de armazenamento Existe um ativado dentro deste grupo de armazenamento" @@ -6610,6 +6610,10 @@ msgstr "Hosts não registrada" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "Nenhuma tarefa ativa encontrada para o Host" + msgid "Not a number" msgstr "Não é um número" @@ -9852,9 +9856,15 @@ msgstr "Falha ao criar tarefa" msgid "The enrollment is no longer pending." msgstr " não existe mais" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9978,6 +9988,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "Diretório" @@ -10088,10 +10101,13 @@ msgstr "O nó de armazenamento grupos de armazenamento associado não é válido msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "Impressora já existe" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12632,6 +12648,10 @@ msgstr "" #~ msgid "CA private key" #~ msgstr "chave privada falhou" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "Não é possível conectar ao banco de dados" + #~ msgid "Check that database is running" #~ msgstr "Verifique o banco de dados está em execução" @@ -12639,6 +12659,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "Configurações" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "Não foi possível ler o arquivo tmp." + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "Criar novo %s" @@ -12967,6 +12991,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "Plugins instalados" +#~ msgid "Invalid Storage Node" +#~ msgstr "Inválida Storage Node" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "tipo inválido" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 694b50ba84..013e184bfc 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -1663,10 +1663,6 @@ msgstr "无法连接到节点。" msgid "Cannot connect to database" msgstr "无法连接到数据库" -#, fuzzy -msgid "Cannot connect to ftp server" -msgstr "无法连接到数据库" - #, php-format msgid "Cannot delete %1$s because a %2$s still refers to it. Reassign or remove it first." msgstr "" @@ -2133,10 +2129,6 @@ msgstr "无法读取tmp文件。" msgid "Could not read local file" msgstr "无法读取临时文件" -#, fuzzy -msgid "Could not read snapin file" -msgstr "无法读取tmp文件。" - #, fuzzy msgid "Could not read the database structure" msgstr "无法创建管理单元工作" @@ -3178,6 +3170,13 @@ msgstr "已成功更新" msgid "FOG Agent poll" msgstr "" +msgid "FOG Agent snapin payload" +msgstr "" + +#, fuzzy +msgid "FOG Agent snapin result" +msgstr "没有文件被上传" + #, fuzzy msgid "FOG Client" msgstr "FOG客户维基" @@ -4970,9 +4969,6 @@ msgstr "无效的管理单元任务处理" msgid "Invalid Storage Group" msgstr "无效的存储组" -msgid "Invalid Storage Node" -msgstr "无效的存储节点" - #, fuzzy msgid "Invalid Tasking" msgstr "任务无效" @@ -6487,6 +6483,10 @@ msgstr "更新管理单元" msgid "No snapins associated with this group as master" msgstr "没有与此主机关联snapins" +#, fuzzy +msgid "No storage node can serve the file." +msgstr "添加管理单元失败!" + #, fuzzy msgid "No storage nodes assigned to this storage group" msgstr "找不到存储节点是否有什么这个存储组中启用" @@ -6610,6 +6610,10 @@ msgstr "未注册主机" msgid "Not a certificate request, or one for a key other than the one this certificate proved." msgstr "" +#, fuzzy +msgid "Not a live task of this host's job." +msgstr "发现主机没有活动任务" + msgid "Not a number" msgstr "不是一个数字" @@ -9852,9 +9856,15 @@ msgstr "无法创建任务" msgid "The enrollment is no longer pending." msgstr "不复存在" +msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" +msgid "The file for one task of the host's own snapin job; fetching it marks the task in progress. Same gate as poll." +msgstr "" + msgid "The file itself." msgstr "" @@ -9978,6 +9988,9 @@ msgstr "" msgid "The path requested is already in use by another image!" msgstr "" +msgid "The payload bytes." +msgstr "" + #, fuzzy msgid "The plugin directory" msgstr "目录" @@ -10088,10 +10101,13 @@ msgstr "存储组相关联的存储节点无效" msgid "The task finishes on the client with nobody at the keyboard" msgstr "" +#, fuzzy +msgid "The task was already closed." +msgstr "打印机已经存在" + msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12632,6 +12648,10 @@ msgstr "" #~ msgid "CA private key" #~ msgstr "私钥失败" +#, fuzzy +#~ msgid "Cannot connect to ftp server" +#~ msgstr "无法连接到数据库" + #~ msgid "Check that database is running" #~ msgstr "检查数据库运行" @@ -12639,6 +12659,10 @@ msgstr "" #~ msgid "Client Module Settings" #~ msgstr "设置" +#, fuzzy +#~ msgid "Could not read snapin file" +#~ msgstr "无法读取tmp文件。" + #, fuzzy #~ msgid "Create New Accesscontrol" #~ msgstr "新建%s" @@ -12967,6 +12991,9 @@ msgstr "" #~ msgid "Install" #~ msgstr "已安装的插件" +#~ msgid "Invalid Storage Node" +#~ msgstr "无效的存储节点" + #, fuzzy #~ msgid "Invalid Type" #~ msgstr "无效类型" diff --git a/packages/web/src/Agent/Snapins.php b/packages/web/src/Agent/Snapins.php new file mode 100644 index 0000000000..38841f46c7 --- /dev/null +++ b/packages/web/src/Agent/Snapins.php @@ -0,0 +1,409 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; +use FOG\Items\Snapin; +use FOG\Items\SnapinTask; +use FOG\Items\StorageGroup; +use FOG\Items\StorageNode; +use FOG\Router\Route; + +/** + * The snapin capability (design 0001 section 7: snapins as payload-only + * software, the detection rule comes later). + * + * The queue is the host's snapin job as the server already builds it -- + * snapinTasks in `sequence` order, which _createSnapinTasking wrote from + * Resolver::resolveSnapins: the host's own associations first, then its + * groups' grants, deduplicated. The agent honors that order and never + * re-sorts. Listing is read-only so the desired state stays idempotent; + * a task moves to in-progress when its payload is fetched and to + * complete when its result lands, exactly as the legacy client's + * SnapinClient does -- stream() and close() ARE that code, shared, so + * both clients mark tasks and end jobs identically. + * + * @category Snapins + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class Snapins extends FOGBase +{ + /** + * stReturnDetails is varchar(250); the agent sends the tail of the + * output and this is what survives. + */ + const MAX_DETAILS = 250; + + /** + * The tasks still to run for this host, in run order. Empty when the + * host has no live snapin job. + * + * @param Host $Host the principal + * + * @return array + */ + public static function queue(Host $Host) + { + $SnapinJob = $Host->get('snapinjob'); + if (!$SnapinJob->isValid()) { + return []; + } + $rows = Route::getList( + 'snapintask', + [ + 'jobID' => (int)$SnapinJob->get('id'), + 'stateID' => self::fastmerge( + self::getQueuedStates(), + (array)self::getProgressState() + ) + ], + 'AND', + 'sequence' + ); + $out = []; + foreach ($rows as $row) { + $SnapinTask = new SnapinTask((int)$row->id); + if (!$SnapinTask->isValid()) { + continue; + } + $Snapin = $SnapinTask->getSnapin(); + if (!$Snapin->isValid()) { + continue; + } + $action = ''; + if ($Snapin->get('shutdown')) { + $action = 'shutdown'; + } elseif ($Snapin->get('reboot')) { + $action = 'reboot'; + } + $out[] = [ + 'task' => (int)$SnapinTask->get('id'), + 'snapin' => (int)$Snapin->get('id'), + 'name' => (string)$Snapin->get('name'), + 'file' => (string)$Snapin->get('file'), + 'size' => (int)$Snapin->get('size'), + // The hash the SnapinHash scanner maintains; the agent + // refuses a payload that does not match it. + 'sha512' => strtolower((string)$Snapin->get('hash')), + 'args' => (string)$Snapin->get('args'), + 'run_with' => (string)$Snapin->get('runWith'), + 'run_with_args' => (string)$Snapin->get('runWithArgs'), + 'timeout' => (int)$Snapin->get('timeout'), + 'action' => $action, + 'abort_on_fail' => (bool)$SnapinJob->get('abortOnFail') + ]; + } + return $out; + } + + /** + * The task, checked to belong to this host's own job. + * + * @param Host $Host the principal + * @param int $taskID the caller-supplied snapin task id + * + * @throws \RuntimeException 404 when it is not this host's live task + * + * @return SnapinTask + */ + public static function ownTask(Host $Host, $taskID) + { + $SnapinJob = $Host->get('snapinjob'); + $SnapinTask = new SnapinTask((int)$taskID); + // Same message for "no such task" and "someone else's task", so + // the id space is not an oracle (the legacy client's Aisle 009 + // guard, kept here for the same reason). + if (!$SnapinJob->isValid() + || !$SnapinTask->isValid() + || (int)$SnapinTask->get('jobID') !== (int)$SnapinJob->get('id') + ) { + throw new \RuntimeException('no such snapin task', 404); + } + return $SnapinTask; + } + + /** + * Streams the task's payload to the caller and marks the task, job and + * host task in progress. Does not return. + * + * The bytes come over the web tier's own FTP session to the storage + * node, as they always have: the agent trusts one certificate, the + * server's, and never a node's. + * + * @param Host $Host the principal + * @param SnapinTask $SnapinTask a task ownTask() returned + * + * @throws \RuntimeException 503 when no node can serve the file + * + * @return void + */ + public static function stream(Host $Host, SnapinTask $SnapinTask) + { + $Snapin = $SnapinTask->getSnapin(); + if (!$Snapin->isValid()) { + throw new \RuntimeException('no such snapin', 404); + } + $StorageNode = self::_node($Host, $Snapin); + $path = sprintf('/%s', trim((string)$StorageNode->get('snapinpath'), '/')); + $file = (string)$Snapin->get('file'); + $filepath = sprintf('%s/%s', $path, $file); + $host = (string)$StorageNode->get('ip'); + $user = (string)$StorageNode->get('user'); + $pass = (string)$StorageNode->get('pass'); + self::$FOGFTP->username = $user; + self::$FOGFTP->password = $pass; + self::$FOGFTP->host = $host; + if (!self::$FOGFTP->connect()) { + throw new \RuntimeException('cannot connect to the storage node', 503); + } + $SnapinFile = sprintf('ftp://%s:%s@%s%s', $user, urlencode($pass), $host, $filepath); + $fh = fopen($SnapinFile, 'rb'); + if (false === $fh) { + throw new \RuntimeException('cannot read the snapin file', 503); + } + $date = self::niceDate()->format('Y-m-d H:i:s'); + $Task = $Host->get('task'); + if ($Task->isValid()) { + $Task + ->set('stateID', self::getProgressState()) + ->set('checkInTime', $date) + ->save(); + } + $Host->get('snapinjob')->set('stateID', self::getProgressState())->save(); + $SnapinTask + ->set('checkin', $date) + ->set('stateID', self::getProgressState()) + ->set('return', -1) + ->set('details', _('Pending...')) + ->save(); + while (ob_get_level()) { + ob_end_clean(); + } + header("X-Sendfile: $filepath"); + header('Content-Description: File Transfer'); + header('Content-Type: application/octet-stream'); + header("Content-Disposition: attachment; filename=$file"); + header('Expires: 0'); + header('Cache-Control: must-revalidate'); + header('Pragma: public'); + while (feof($fh) === false) { + if (($line = fread($fh, 4096)) === false) { + break; + } + echo $line; + flush(); + } + fclose($fh); + exit; + } + + /** + * Records the result of one task and, when it was the last, ends the + * job -- canceling the rest first when the job aborts on failure. + * + * @param Host $Host the principal + * @param SnapinTask $SnapinTask a task ownTask() returned + * @param int $exitcode the payload's exit code + * @param string $details the output tail, or the agent's reason + * + * @throws \RuntimeException 409 when the task is already closed + * + * @return void + */ + public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $details) + { + if (in_array( + (int)$SnapinTask->get('stateID'), + [self::getCompleteState(), self::getCancelledState()], + true + )) { + throw new \RuntimeException('snapin task already closed', 409); + } + $Snapin = $SnapinTask->getSnapin(); + if (!$Snapin->isValid()) { + throw new \RuntimeException('no such snapin', 404); + } + $exitcode = (int)$exitcode; + $details = substr(trim((string)$details), 0, self::MAX_DETAILS); + $date = self::niceDate()->format('Y-m-d H:i:s'); + $HostName = (string)$Host->get('name'); + $SnapinJob = $Host->get('snapinjob'); + $SnapinTask + ->set('stateID', self::getCompleteState()) + ->set('return', $exitcode) + ->set('details', $details) + ->set('complete', $date) + ->save(); + self::$EventManager->notify( + 'HOST_SNAPINTASK_COMPLETE', + [ + 'Snapin' => &$Snapin, + 'SnapinTask' => &$SnapinTask, + 'Host' => &$Host, + 'HostName' => &$HostName + ] + ); + $live = [ + 'jobID' => (int)$SnapinJob->get('id'), + 'stateID' => self::fastmerge( + self::getQueuedStates(), + (array)self::getProgressState() + ) + ]; + $abortedOnFailure = false; + if ($SnapinJob->get('abortOnFail') && 0 !== $exitcode) { + $abortedOnFailure = true; + self::getClass('SnapinTaskManager')->update( + $live, + '', + [ + 'stateID' => self::getCancelledState(), + 'return' => $exitcode, + 'details' => sprintf( + _('Aborted due to failure of "%s" with exit code %s'), + $Snapin->get('name'), + $exitcode + ), + 'complete' => $date + ] + ); + } + if (Route::getCount('snapintask', $live) < 1) { + $stateID = $abortedOnFailure ? self::getCancelledState() : self::getCompleteState(); + $Task = $Host->get('task'); + if ($Task->isValid()) { + $Task->set('stateID', $stateID)->save(); + } + $SnapinJob->set('stateID', $stateID)->save(); + self::$EventManager->notify( + 'HOST_SNAPIN_COMPLETE', + [ + 'HostName' => &$HostName, + 'Host' => &$Host + ] + ); + } + } + + /** + * The agent's report for one task: close it and leave the audit row + * the host page shows. + * + * @param Host $Host the principal + * @param int $taskID the snapin task + * @param array $body exit_code, details + * + * @return void + */ + public static function report(Host $Host, $taskID, array $body) + { + $SnapinTask = self::ownTask($Host, $taskID); + $name = (string)$SnapinTask->getSnapin()->get('name'); + $exitcode = (int)($body['exit_code'] ?? 1); + $details = (string)($body['details'] ?? ''); + self::close($Host, $SnapinTask, $exitcode, $details); + Audit::record( + [ + 'type' => 'agent.result', + 'subjectType' => 'host', + 'subjectID' => (int)$Host->get('id'), + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'text' => substr( + sprintf( + 'snapin "%s" (task %d) exit %d%s', + $name, + (int)$SnapinTask->get('id'), + $exitcode, + '' === trim($details) ? '' : ': ' . trim($details) + ), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } + + /** + * The node that serves this snapin to this host: a hook's choice, else + * the master of the snapin's storage group. + * + * @param Host $Host the principal + * @param Snapin $Snapin the snapin + * + * @throws \RuntimeException 503 when there is none + * + * @return StorageNode + */ + private static function _node(Host $Host, Snapin $Snapin) + { + $HostName = (string)$Host->get('name'); + $StorageGroup = self::_hooked( + 'SNAPIN_GROUP', + [ + 'Host' => &$Host, + 'Snapin' => &$Snapin, + 'StorageGroup' => null, + 'HostName' => &$HostName + ], + 'StorageGroup' + ); + $StorageNode = self::_hooked( + 'SNAPIN_NODE', + [ + 'Host' => &$Host, + 'Snapin' => &$Snapin, + 'StorageNode' => null + ], + 'StorageNode' + ); + if (!($StorageGroup instanceof StorageGroup && $StorageGroup->isValid())) { + $StorageGroup = $Snapin->getStorageGroup(); + if (!$StorageGroup->isValid()) { + throw new \RuntimeException('no storage group for this snapin', 503); + } + } + if (!($StorageNode instanceof StorageNode && $StorageNode->isValid())) { + $StorageNode = $StorageGroup->getMasterStorageNode(); + if (!($StorageNode instanceof StorageNode && $StorageNode->isValid())) { + throw new \RuntimeException('no storage node for this snapin', 503); + } + } + return $StorageNode; + } + + /** + * Fires a hook event whose listeners answer by filling one slot of the + * payload, and returns what they left there. + * + * @param string $event the event + * @param array $args the payload; $args[$key] is the answer slot + * @param string $key the slot + * + * @return mixed null when no listener answered + */ + private static function _hooked($event, array $args, $key) + { + $answer = $args[$key]; + $args[$key] = &$answer; + self::$HookManager->processEvent($event, $args); + return $answer; + } +} diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 30583fad3d..e18560e901 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -48,7 +48,8 @@ class State extends FOGBase */ const CAPABILITIES = [ 'hostname' => 'hostnamechanger', - 'taskreboot' => 'taskreboot' + 'taskreboot' => 'taskreboot', + 'snapin' => 'snapinclient' ]; /** @@ -123,6 +124,12 @@ public static function desired(Host $Host) ]; } } + if (in_array('snapin', $capabilities, true)) { + // The host's snapin queue in run order (Agent\Snapins). Tasks + // leave it as they complete, so the revision moves with the + // queue and an empty list is the resting state. + $state['snapins'] = Snapins::queue($Host); + } if (count($capabilities) > 0) { // The policy every reboot obeys, whatever asked for it: // FOG_GRACE_TIMEOUT is the warning logged-in users get. diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 7425d0c4e9..a07d95f2ae 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -343,6 +343,8 @@ class Authorization extends FOGBase 'agentrenew' => null, // fog-agent: same gate 'agentstate' => null, // fog-agent: same gate 'agentresult' => null, // fog-agent: same gate + 'agentsnapinfile' => null, // fog-agent: same gate + 'agentsnapinresult' => null, // fog-agent: same gate 'agentenrollments' => 'host.view', 'agentenrollmentdecide' => 'host.edit', // Tokens approve machines the admin has not seen, which creates diff --git a/packages/web/src/Client/SnapinClient.php b/packages/web/src/Client/SnapinClient.php index ade3c14a82..8fb5cc4a0f 100644 --- a/packages/web/src/Client/SnapinClient.php +++ b/packages/web/src/Client/SnapinClient.php @@ -13,6 +13,7 @@ namespace FOG\Client; +use FOG\Agent\Snapins; use FOG\Items\SnapinTask; use FOG\Items\StorageGroup; use FOG\Items\StorageNode; @@ -317,75 +318,10 @@ private function _closeout(object $Task, object $SnapinJob, string $date, string } else { $exitcode = (int)$exitcode; } - $SnapinTask - ->set('stateID', self::getCompleteState()) - ->set('return', $exitcode) - ->set('details', $exitdesc) - ->set('complete', $date) - ->save(); - self::$EventManager->notify( - 'HOST_SNAPINTASK_COMPLETE', - [ - 'Snapin' => &$Snapin, - 'SnapinTask' => &$SnapinTask, - 'Host' => &self::$Host, - 'HostName' => &$HostName - ] - ); - $abortedOnFailure = false; - if ($SnapinJob->get('abortOnFail') && $exitcode !== 0) { - $abortedOnFailure = true; - self::getClass('SnapinTaskManager') - ->update( - [ - 'jobID' => $SnapinJob->get('id'), - 'stateID' => self::fastmerge( - self::getQueuedStates(), - (array)self::getProgressState() - ) - ], - '', - [ - 'stateID' => self::getCancelledState(), - 'return' => $exitcode, - 'details' => sprintf( - _( - 'Aborted due to failure of "%s" ' - . 'with exit code %s' - ), - $Snapin->get('name'), - $exitcode - ), - 'complete' => $date - ] - ); - } - $STaskCount = Route::getCount( - 'snapintask', - [ - 'jobID' => $SnapinJob->get('id'), - 'stateID' => self::fastmerge( - self::getQueuedStates(), - (array)self::getProgressState() - ) - ] - ); - if ($STaskCount < 1) { - $stateID = $abortedOnFailure ? - self::getCancelledState() : - self::getCompleteState(); - if ($Task->isValid()) { - $Task->set('stateID', $stateID)->save(); - } - $SnapinJob->set('stateID', $stateID)->save(); - self::$EventManager->notify( - 'HOST_SNAPIN_COMPLETE', - [ - 'HostName' => &$HostName, - 'Host' => &self::$Host - ] - ); - } + // The state changes -- task complete, abort-on-fail cancellation, + // job and host task closed -- are the same for fog-agent and live + // in one place, so the two clients cannot drift. + Snapins::close(self::$Host, $SnapinTask, $exitcode, (string)$exitdesc); } /** @@ -441,131 +377,8 @@ private function _downloadfile(object $Task, object $SnapinJob, string $date, st ) ); } - $Snapin = $SnapinTask->getSnapin(); - if (!$Snapin->isValid()) { - throw new \Exception(_('Invalid Snapin')); - } - $StorageGroup = $StorageNode = null; - self::$HookManager->processEvent( - 'SNAPIN_GROUP', - [ - 'Host' => &self::$Host, - 'Snapin' => &$Snapin, - 'StorageGroup' => &$StorageGroup, - 'HostName' => &$HostName - ] - ); - self::$HookManager->processEvent( - 'SNAPIN_NODE', - [ - 'Host' => &self::$Host, - 'Snapin' => &$Snapin, - 'StorageNode' => &$StorageNode - ] - ); - if (!($StorageGroup instanceof StorageGroup - && $StorageGroup->isValid()) - ) { - $StorageGroup = $Snapin->getStorageGroup(); - if (!$StorageGroup->isValid()) { - throw new \Exception( - sprintf( - '%s: %s', - '#!er', - _('Invalid Storage Group') - ) - ); - } - } - if (!($StorageNode instanceof StorageNode - && $StorageNode->isValid()) - ) { - $StorageNode = $StorageGroup->getMasterStorageNode(); - if (!($StorageNode instanceof StorageNode - && $StorageNode->isValid()) - ) { - throw new \Exception( - sprintf( - '%s: %s', - '#!er', - _('Invalid Storage Node') - ) - ); - } - } - $path = sprintf( - '/%s', - trim($StorageNode->get('snapinpath'), '/') - ); - $file = $Snapin->get('file'); - $filepath = sprintf( - '%s/%s', - $path, - $file - ); - $host = $StorageNode->get('ip'); - $user = $StorageNode->get('user'); - $pass = $StorageNode->get('pass'); - self::$FOGFTP->username = $user; - self::$FOGFTP->password = $pass; - self::$FOGFTP->host = $host; - if (!self::$FOGFTP->connect()) { - throw new \Exception( - sprintf( - '%s: %s', - '#!er', - _('Cannot connect to ftp server') - ) - ); - } - $SnapinFile = sprintf( - 'ftp://%s:%s@%s%s', - $user, - urlencode($pass), - $host, - $filepath - ); - if ($Task->isValid()) { - $Task - ->set('stateID', self::getProgressState()) - ->set('checkInTime', $date) - ->save(); - } - $SnapinJob - ->set('stateID', self::getProgressState()) - ->save(); - $SnapinTask - ->set('stateID', self::getProgressState()) - ->set('return', -1) - ->set('details', _('Pending...')) - ->save(); - while (ob_get_level()) { - ob_end_clean(); - } - header("X-Sendfile: $filepath"); - header('Content-Description: File Transfer'); - header('Content-Type: application/octet-stream'); - header("Content-Disposition: attachment; filename=$file"); - header('Expires: 0'); - header('Cache-Control: must-revalidate'); - header('Pragma: public'); - if (($fh = fopen($SnapinFile, 'rb')) === false) { - throw new \Exception( - sprintf( - '%s: %s', - '#!er', - _('Could not read snapin file') - ) - ); - } - while (feof($fh) === false) { - if (($line = fread($fh, 4096)) === false) { - break; - } - echo $line; - flush(); - } - fclose($fh); - exit; + // Node choice, in-progress marking and the FTP-backed stream are + // shared with fog-agent (Agent\Snapins::stream); does not return. + Snapins::stream(self::$Host, $SnapinTask); } } diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index 52406f75bb..0ae28104ad 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2641,6 +2641,26 @@ private static function _fixedPaths() 'properties' => [ 'grace' => ['type' => 'integer'] ] + ], + 'snapins' => [ + 'type' => 'array', + 'items' => [ + 'type' => 'object', + 'properties' => [ + 'task' => ['type' => 'integer'], + 'snapin' => ['type' => 'integer'], + 'name' => ['type' => 'string'], + 'file' => ['type' => 'string'], + 'size' => ['type' => 'integer'], + 'sha512' => ['type' => 'string'], + 'args' => ['type' => 'string'], + 'run_with' => ['type' => 'string'], + 'run_with_args' => ['type' => 'string'], + 'timeout' => ['type' => 'integer'], + 'action' => ['type' => 'string', 'enum' => ['', 'reboot', 'shutdown']], + 'abort_on_fail' => ['type' => 'boolean'] + ] + ] ] ] ]]] @@ -2677,6 +2697,54 @@ private static function _fixedPaths() ] ) ], + '/agent/v1/snapin/{id}/file' => [ + 'get' => self::_op( + '', + 'agentsnapinfile', + _('FOG Agent snapin payload'), + _('The file for one task of the host\'s own snapin job; ' + . 'fetching it marks the task in progress. Same gate ' + . 'as poll.'), + [ + '200' => [ + 'description' => _('The payload bytes.'), + 'content' => ['application/octet-stream' => ['schema' => ['type' => 'string', 'format' => 'binary']]] + ], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')], + '404' => ['description' => _('Not a live task of this host\'s job.')], + '503' => ['description' => _('No storage node can serve the file.')] + ], + [self::_idParameter()] + ) + ], + '/agent/v1/snapin/{id}/result' => [ + 'post' => self::_op( + '', + 'agentsnapinresult', + _('FOG Agent snapin result'), + _('The exit code and output tail of one task. Closes the ' + . 'task as the legacy check-in does, cancels the rest ' + . 'of a job that aborts on failure, ends the job after ' + . 'its last task, and records it on the host as ' + . 'agent.result.'), + [ + '200' => ['description' => _('Recorded.')], + '401' => ['description' => _('No verified client certificate, or one bound to no live host.')], + '404' => ['description' => _('Not a live task of this host\'s job.')] + ] + self::_conflictResponse(_('The task was already closed.')), + [self::_idParameter()], + [ + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'required' => ['exit_code'], + 'properties' => [ + 'exit_code' => ['type' => 'integer'], + 'details' => ['type' => 'string', 'maxLength' => 250] + ] + ]]] + ] + ) + ], '/agent/v1/renew' => [ 'post' => self::_op( '', diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 3449da0cf7..2d35827469 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -1567,6 +1567,8 @@ protected static function defineRoutes(\FastRoute\RouteCollector $r) self::_registerRoute($r, 'POST', '/agent/v1/renew', [__CLASS__, 'agentRenew'], 'agentrenew'); self::_registerRoute($r, 'GET', '/agent/v1/state', [__CLASS__, 'agentState'], 'agentstate'); self::_registerRoute($r, 'POST', '/agent/v1/result', [__CLASS__, 'agentResult'], 'agentresult'); + self::_registerRoute($r, 'GET', '/agent/v1/snapin/[i:id]/file', [__CLASS__, 'agentSnapinFile'], 'agentsnapinfile'); + self::_registerRoute($r, 'POST', '/agent/v1/snapin/[i:id]/result', [__CLASS__, 'agentSnapinResult'], 'agentsnapinresult'); self::_registerRoute($r, 'GET', '/agent/enrollments', [__CLASS__, 'agentEnrollments'], 'agentenrollments'); self::_registerRoute($r, 'POST', '/agent/enrollment/[i:id]/[*:action]', [__CLASS__, 'agentEnrollmentDecide'], 'agentenrollmentdecide'); self::_registerRoute($r, 'GET', '/agent/tokens', [__CLASS__, 'agentTokens'], 'agenttokens'); @@ -2963,6 +2965,68 @@ public static function agentResult() json_encode(['status' => 'ok']) ); } + /** + * fog-agent's snapin payload: the bytes of one task's file. + * + * The task must belong to the host's own job; the fetch is what marks + * the task in progress. Streams and exits (Agent\Snapins::stream). + * + * @param int $id the snapin task + * + * @return void + */ + public static function agentSnapinFile($id) + { + try { + $SnapinTask = \FOG\Agent\Snapins::ownTask(self::$agentHost, (int)$id); + \FOG\Agent\Snapins::stream(self::$agentHost, $SnapinTask); + } catch (\RuntimeException $e) { + HTTPResponseCodes::breakHead( + self::_agentErrorCode($e), + json_encode(['status' => 'error', 'error' => $e->getMessage()]) + ); + } + } + /** + * fog-agent's result for one snapin task: exit_code and details. + * + * Closes the task the way the legacy client's check-in does, ends the + * job when it was the last, and audits it on the host. + * + * @param int $id the snapin task + * + * @return void + */ + public static function agentSnapinResult($id) + { + $body = self::_jsonBody(); + try { + \FOG\Agent\Snapins::report(self::$agentHost, (int)$id, (array)$body); + } catch (\RuntimeException $e) { + HTTPResponseCodes::breakHead( + self::_agentErrorCode($e), + json_encode(['status' => 'error', 'error' => $e->getMessage()]) + ); + return; + } + HTTPResponseCodes::breakHead( + HTTPResponseCodes::HTTP_OK, + json_encode(['status' => 'ok']) + ); + } + /** + * The HTTP status for an agent-side RuntimeException: its code when it + * is one of the statuses these routes answer with, else 400. + * + * @param \RuntimeException $e the exception + * + * @return int + */ + private static function _agentErrorCode(\RuntimeException $e) + { + $code = (int)$e->getCode(); + return in_array($code, [404, 409, 503], true) ? $code : HTTPResponseCodes::HTTP_BAD_REQUEST; + } /** * fog-agent's certificate renewal, over the certificate being renewed. * From 13cc333d480c548ef7252dadb98de20239a5815c Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 18:06:02 +0000 Subject: [PATCH 015/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index dcc00e1364..4ec80efaa3 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10112,6 +10112,7 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 55f04ebf04..2546a0565d 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10121,6 +10121,7 @@ msgstr "Printer already exists" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index dc9a04c7ce..0ec8cdb08b 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10281,6 +10281,7 @@ msgstr "Impresora ya existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 065909ca8c..2654ef5269 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10113,6 +10113,7 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a89256eb64..76c3817978 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10106,6 +10106,7 @@ msgstr "Imprimante existe déjà" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 7eec626466..629846528d 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9828,6 +9828,7 @@ msgstr "Questo host esiste già" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 2133a476ea..d2a6515cf1 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9777,6 +9777,7 @@ msgstr "このホストは既に存在します" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 663c9cc986..aa13d81a7a 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8663,6 +8663,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 8ed0c03df1..739862e14f 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10108,6 +10108,7 @@ msgstr "Impressora já existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 013e184bfc..0f154b4b25 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10108,6 +10108,7 @@ msgstr "打印机已经存在" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From e449c58a07242a8f8f195c834028c9837de429dc Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 13:30:23 -0500 Subject: [PATCH 016/117] Agent snapins: per-snapin return-code table, status column, TEXT details Snapins reported success only when the payload exited 0, which mislabels the codes installers actually return: 3010/1641 (installed, reboot to finish) and 1618 (another install in progress, retry) both read as failures and could trip abort-on-fail. This gives each snapin a `code=class` table (sReturnCodes, empty = the Intune defaults 0/1707=success, 3010/1641=reboot, 1618=retry) and derives an outcome from it on the server: - retry puts the task back to queued so the next check-in runs it again - reboot returns the outcome to the agent, whose coordinator handles it - abort-on-fail only fires on failed, not on reboot/retry snapinTasks gains stStatus (ran/hash_mismatch/timeout/cannot_run) next to the raw exit code, so a payload that never ran is no longer recorded as exit code 0, and stReturnDetails widens to TEXT so a 4 KB tail of the payload's output fits. Schema 417. UI: Return Codes textarea on snapin add/edit, Status column in the host and group snapin history, report page labels outcomes from stStatus. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- packages/web/commons/schema.php | 18 +++ .../management/js/fog/group/fog.group.edit.js | 3 +- .../management/js/fog/host/fog.host.edit.js | 3 +- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 37 ++++- .../web/management/languages/messages.pot | 30 +++- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 34 ++++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 34 ++++- packages/web/src/Agent/Snapins.php | 132 ++++++++++++++++-- packages/web/src/Audit/SnapinStats.php | 1 + packages/web/src/Base/System.php | 2 +- packages/web/src/Items/Snapin.php | 3 + packages/web/src/Items/SnapinTask.php | 3 +- packages/web/src/Pages/GroupManagement.php | 4 +- packages/web/src/Pages/HostManagement.php | 4 +- packages/web/src/Pages/SnapinManagement.php | 53 ++++++- packages/web/src/Reports/Snapin_Report.php | 28 +++- packages/web/src/Router/OpenAPI.php | 32 +++-- packages/web/src/Router/Route.php | 6 +- phpstan-baseline.neon | 2 +- tests/fixtures/route-column-contract.txt | 28 ++-- 26 files changed, 578 insertions(+), 83 deletions(-) diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 8ec517c0c9..9954b197ed 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -10861,3 +10861,21 @@ function () { . "deploy was the approval. 0 disables the shortcut and every " . "enrollment waits for a click or a token.','24','General Settings')", ]; +// 417 +$this->schema[] = [ + // fog-agent snapins (design 0001 section 7, protocol-v1 "Snapins"). + // A snapin's return-code table: one `code=class` per line, class one + // of success, reboot, retry, failed. Empty means the installer + // defaults (0 and 1707 success, 3010 and 1641 reboot, 1618 retry). + // The server reads a task's exit code against it for the agent and + // the legacy client alike, so an MSI that answers 3010 is a success + // that needs a reboot instead of a failed job. + "ALTER TABLE `snapins` ADD COLUMN IF NOT EXISTS `sReturnCodes` text NULL", + // What a run came to: success, reboot, retry, failed, or when the + // payload never ran, hash_mismatch, timeout, cannot_run. Beside the + // raw exit code, which stays the program's own. + "ALTER TABLE `snapinTasks` ADD COLUMN IF NOT EXISTS `stStatus` varchar(16) NOT NULL DEFAULT '' AFTER `stReturnCode`", + // Installers put the useful line well past 250 characters; the agent + // reports the last 4 KB of output. + "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL", +]; diff --git a/packages/web/management/js/fog/group/fog.group.edit.js b/packages/web/management/js/fog/group/fog.group.edit.js index 52fb6e7e98..88421e4350 100644 --- a/packages/web/management/js/fog/group/fog.group.edit.js +++ b/packages/web/management/js/fog/group/fog.group.edit.js @@ -691,7 +691,8 @@ {data: 'checkin'}, {data: 'complete'}, {data: 'diff'}, - {data: 'return'} + {data: 'return'}, + {data: 'status'} ], // Host first, because RowGroup only groups correctly when the // grouped column is the primary sort -- otherwise a host's rows diff --git a/packages/web/management/js/fog/host/fog.host.edit.js b/packages/web/management/js/fog/host/fog.host.edit.js index 3b846a1e9f..1405fba0f9 100644 --- a/packages/web/management/js/fog/host/fog.host.edit.js +++ b/packages/web/management/js/fog/host/fog.host.edit.js @@ -1319,7 +1319,8 @@ {data: 'checkin'}, {data: 'complete'}, {data: 'diff'}, - {data: 'return'} + {data: 'return'}, + {data: 'status'} ], columnDefs: [ { diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 4ec80efaa3..5eed0a5674 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -1645,6 +1645,10 @@ msgstr "Stornierter Task" msgid "Canceled due to new tasking." msgstr "Abgebrochen aufgrund neuer Aufgabe (Task)" +#, fuzzy +msgid "Cannot Run" +msgstr "Datensatz wurde nicht gefunden, Fehler: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Anmeldung am LDAP-Server nicht möglich" @@ -3967,6 +3971,9 @@ msgstr "Hardwareinformationen" msgid "Hash" msgstr "Hash" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6808,6 +6815,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Eine oder mehrere MACs sind mit einem Host verknüpft" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7952,9 +7962,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "Entfernen fehlgeschlagen" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "Return-Code" +#, fuzzy +msgid "Return Codes" +msgstr "Return-Code" + msgid "Return To Local Login" msgstr "" @@ -9860,9 +9877,6 @@ msgstr "Fehler beim Erstellen eines Tasks" msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10025,6 +10039,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "Die zugeordnete Primär-MAC lautet" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10453,6 +10470,10 @@ msgstr "Zeit bereits vorhanden" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Zeit" + msgid "Title" msgstr "" @@ -10765,6 +10786,10 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unknown power action" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +#, fuzzy +msgid "Unknown status." +msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" + #, fuzzy msgid "Unknown upload error occurred" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" @@ -11290,6 +11315,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "Wo bekomme ich Hilfe?" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 2546a0565d..5878d1020e 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -1649,6 +1649,10 @@ msgstr "Cancelled task" msgid "Canceled due to new tasking." msgstr "Cancelled due to new tasking." +#, fuzzy +msgid "Cannot Run" +msgstr "Record not found, Error: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Cannot connect to database" @@ -3969,6 +3973,9 @@ msgstr "Hardware Information" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6819,6 +6826,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Error, Is an image associated with this host" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7963,9 +7973,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "Removed" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "Return Code" +#, fuzzy +msgid "Return Codes" +msgstr "Return Code" + msgid "Return To Local Login" msgstr "" @@ -9869,9 +9886,6 @@ msgstr "Failed to create task" msgid "The enrollment is no longer pending." msgstr " no longer exists" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10034,6 +10048,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10462,6 +10479,10 @@ msgstr "Time Already Exists" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Time" + msgid "Title" msgstr "" @@ -10773,6 +10794,10 @@ msgstr "Unknown upload error occurred. Return code: " msgid "Unknown power action" msgstr "Unknown upload error occurred. Return code: " +#, fuzzy +msgid "Unknown status." +msgstr "Unknown upload error occurred. Return code: " + #, fuzzy msgid "Unknown upload error occurred" msgstr "Unknown upload error occurred. Return code: " @@ -11297,6 +11322,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 0ec8cdb08b..e44b7eb0ec 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -1667,6 +1667,10 @@ msgstr "tarea Cancelado" msgid "Canceled due to new tasking." msgstr "Cancelada debido a las nuevas tareas a la vez." +#, fuzzy +msgid "Cannot Run" +msgstr "Registro no encontrado, error: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Error: No se pudo descargar kernel" @@ -4020,6 +4024,9 @@ msgstr "Información general" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6928,6 +6935,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Error, es una imagen asociada con este anfitrión" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -8088,10 +8098,17 @@ msgstr "" msgid "Retention sweep failed" msgstr "Remoto" +msgid "Retry" +msgstr "" + #, fuzzy msgid "Return Code" msgstr "Código de retorno" +#, fuzzy +msgid "Return Codes" +msgstr "Código de retorno" + msgid "Return To Local Login" msgstr "" @@ -10028,9 +10045,6 @@ msgstr "No se pudo crear la tarea" msgid "The enrollment is no longer pending." msgstr "" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10195,6 +10209,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "Error, es una imagen asociada con este anfitrión" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10621,6 +10638,10 @@ msgstr "nombre de usuario ya existe" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Hora" + msgid "Title" msgstr "" @@ -10931,6 +10952,10 @@ msgstr "Se produjo un error de carga desconocida. Código de retorno: " msgid "Unknown power action" msgstr "Se produjo un error de carga desconocida. Código de retorno: " +#, fuzzy +msgid "Unknown status." +msgstr "Se produjo un error de carga desconocida. Código de retorno: " + #, fuzzy msgid "Unknown upload error occurred" msgstr "Se produjo un error de carga desconocida. Código de retorno: " @@ -11459,6 +11484,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2654ef5269..7a2f833b7d 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -1645,6 +1645,10 @@ msgstr "Stornierter Task" msgid "Canceled due to new tasking." msgstr "Abgebrochen aufgrund neuer Aufgabe (Task)" +#, fuzzy +msgid "Cannot Run" +msgstr "Datensatz wurde nicht gefunden, Fehler: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Anmeldung am LDAP-Server nicht möglich" @@ -3967,6 +3971,9 @@ msgstr "Hardwareinformationen" msgid "Hash" msgstr "Hash" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6809,6 +6816,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Eine oder mehrere MACs sind mit einem Host verknüpft" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7953,9 +7963,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "Entfernen fehlgeschlagen" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "Return-Code" +#, fuzzy +msgid "Return Codes" +msgstr "Return-Code" + msgid "Return To Local Login" msgstr "" @@ -9861,9 +9878,6 @@ msgstr "Fehler beim Erstellen eines Tasks" msgid "The enrollment is no longer pending." msgstr "läuft nicht mehr" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10026,6 +10040,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "Die zugeordnete Primär-MAC lautet" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10454,6 +10471,10 @@ msgstr "Zeit bereits vorhanden" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Zeit" + msgid "Title" msgstr "" @@ -10766,6 +10787,10 @@ msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" msgid "Unknown power action" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" +#, fuzzy +msgid "Unknown status." +msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" + #, fuzzy msgid "Unknown upload error occurred" msgstr "Ein unbekannter Upload-Fehler ist aufgetreten" @@ -11291,6 +11316,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "Wo bekomme ich Hilfe?" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 76c3817978..6df8ec8ace 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -1650,6 +1650,10 @@ msgstr "tâche Annulé" msgid "Canceled due to new tasking." msgstr "Annulé en raison de nouvelles tâches." +#, fuzzy +msgid "Cannot Run" +msgstr "Enregistrement non trouvé, Erreur: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Impossible de se connecter à la base de données" @@ -3970,6 +3974,9 @@ msgstr "Informations sur le matériel" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6806,6 +6813,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Erreur, est une image associée à cet hôte" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7950,9 +7960,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "supprimé" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "code de retour" +#, fuzzy +msgid "Return Codes" +msgstr "code de retour" + msgid "Return To Local Login" msgstr "" @@ -9854,9 +9871,6 @@ msgstr "Impossible de créer la tâche" msgid "The enrollment is no longer pending." msgstr " n'existe plus" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10019,6 +10033,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10447,6 +10464,10 @@ msgstr "Temps existe déjà" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Temps" + msgid "Title" msgstr "" @@ -10759,6 +10780,10 @@ msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " msgid "Unknown power action" msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " +#, fuzzy +msgid "Unknown status." +msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " + #, fuzzy msgid "Unknown upload error occurred" msgstr "Erreur inconnue de téléchargement a eu lieu. Code de retour: " @@ -11283,6 +11308,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 629846528d..ce1b066fb6 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -1610,6 +1610,10 @@ msgstr "compito Annullato" msgid "Canceled due to new tasking." msgstr "Annullato a causa della nuova tasking." +#, fuzzy +msgid "Cannot Run" +msgstr "Record non trovato" + msgid "Cannot bind to the LDAP server" msgstr "Impossibile legare al server LDAP" @@ -3876,6 +3880,9 @@ msgstr "Informazioni sull'hardware" msgid "Hash" msgstr "Hash" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6621,6 +6628,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Uno o più MAC sono associati a questo host" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7738,9 +7748,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "Rimozione fallita" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "Codice di ritorno" +#, fuzzy +msgid "Return Codes" +msgstr "Codice di ritorno" + msgid "Return To Local Login" msgstr "" @@ -9577,9 +9594,6 @@ msgstr "Impossibile creare un'attività" msgid "The enrollment is no longer pending." msgstr "non è più in esecuzione" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9741,6 +9755,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "Il MAC primario associato è" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10160,6 +10177,10 @@ msgstr "Ora esiste già" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Tempo" + msgid "Title" msgstr "" @@ -10465,6 +10486,10 @@ msgstr "Si è verificato errore di caricamento sconosciuto" msgid "Unknown power action" msgstr "Si è verificato errore di caricamento sconosciuto" +#, fuzzy +msgid "Unknown status." +msgstr "Si è verificato errore di caricamento sconosciuto" + msgid "Unknown upload error occurred" msgstr "Si è verificato errore di caricamento sconosciuto" @@ -10972,6 +10997,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "Dove ottenere aiuto" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index d2a6515cf1..f641eebb7d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -1592,6 +1592,10 @@ msgstr "キャンセルされたタスク" msgid "Canceled due to new tasking." msgstr "新しいタスクによりキャンセルされました。" +#, fuzzy +msgid "Cannot Run" +msgstr "アイコンファイルが見つかりません" + msgid "Cannot bind to the LDAP server" msgstr "LDAP サーバーにバインドできません" @@ -3851,6 +3855,10 @@ msgstr "レポートを作成しますか?" msgid "Hash" msgstr "ハッシュ" +#, fuzzy +msgid "Hash Mismatch" +msgstr "ファイルハッシュが一致しません" + msgid "Have not locked the host for access" msgstr "ホストはアクセス用にロックされていません" @@ -6589,6 +6597,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "1 つ以上の MAC アドレスがホストに関連付けられています" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7709,9 +7720,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "削除に失敗しました" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "戻りコード" +#, fuzzy +msgid "Return Codes" +msgstr "戻りコード" + msgid "Return To Local Login" msgstr "" @@ -9523,9 +9541,6 @@ msgstr "タスクの作成に失敗しました" msgid "The enrollment is no longer pending." msgstr "選択したプリンターを追加" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -9689,6 +9704,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "関連付けられたプライマリ MAC アドレス:" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10109,6 +10127,9 @@ msgstr "この時刻は既に存在します" msgid "Time since last imaged" msgstr "" +msgid "Timeout" +msgstr "タイムアウト" + msgid "Title" msgstr "" @@ -10411,6 +10432,10 @@ msgstr "不明なデータベースエラー" msgid "Unknown power action" msgstr "不明なデータベースエラー" +#, fuzzy +msgid "Unknown status." +msgstr "不明なデータベースエラー" + msgid "Unknown upload error occurred" msgstr "不明なアップロードエラーが発生しました" @@ -10921,6 +10946,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "ヘルプの入手先" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" @@ -14161,9 +14189,6 @@ msgstr "" #~ msgid "This will set the configuration level to all hosts in this group" #~ msgstr "このグループ内のすべてのホストでこの項目をクリアします。" -#~ msgid "Timeout" -#~ msgstr "タイムアウト" - #~ msgid "Title must be a string" #~ msgstr "タイトルは文字列で指定してください" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index aa13d81a7a..c961b2b1c9 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -1430,6 +1430,9 @@ msgstr "" msgid "Canceled due to new tasking." msgstr "" +msgid "Cannot Run" +msgstr "" + msgid "Cannot bind to the LDAP server" msgstr "" @@ -3420,6 +3423,9 @@ msgstr "" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -5832,6 +5838,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -6818,9 +6827,15 @@ msgstr "" msgid "Retention sweep failed" msgstr "" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "" +msgid "Return Codes" +msgstr "" + msgid "Return To Local Login" msgstr "" @@ -8430,9 +8445,6 @@ msgstr "" msgid "The enrollment is no longer pending." msgstr "" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -8584,6 +8596,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -8985,6 +9000,9 @@ msgstr "" msgid "Time since last imaged" msgstr "" +msgid "Timeout" +msgstr "" + msgid "Title" msgstr "" @@ -9249,6 +9267,9 @@ msgstr "" msgid "Unknown power action" msgstr "" +msgid "Unknown status." +msgstr "" + msgid "Unknown upload error occurred" msgstr "" @@ -9705,6 +9726,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 739862e14f..7e61926da0 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -1649,6 +1649,10 @@ msgstr "tarefa cancelada" msgid "Canceled due to new tasking." msgstr "Cancelado devido à nova tarefa." +#, fuzzy +msgid "Cannot Run" +msgstr "Registro não encontrado, erro: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "Não é possível conectar ao banco de dados" @@ -3969,6 +3973,9 @@ msgstr "Informações de hardware" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6806,6 +6813,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Erro, é uma imagem associada com este anfitrião" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7950,9 +7960,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "Removido" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "Código de retorno" +#, fuzzy +msgid "Return Codes" +msgstr "Código de retorno" + msgid "Return To Local Login" msgstr "" @@ -9856,9 +9873,6 @@ msgstr "Falha ao criar tarefa" msgid "The enrollment is no longer pending." msgstr " não existe mais" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10021,6 +10035,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10449,6 +10466,10 @@ msgstr "Tempo já existe" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "Tempo" + msgid "Title" msgstr "" @@ -10761,6 +10782,10 @@ msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " msgid "Unknown power action" msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " +#, fuzzy +msgid "Unknown status." +msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " + #, fuzzy msgid "Unknown upload error occurred" msgstr "Ocorreu um erro de upload desconhecido. Código de retorno: " @@ -11285,6 +11310,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 0f154b4b25..e0cb5e6f36 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -1649,6 +1649,10 @@ msgstr "取消任务" msgid "Canceled due to new tasking." msgstr "由于新的任务取消。" +#, fuzzy +msgid "Cannot Run" +msgstr "未发现记录,错误: %s" + #, fuzzy msgid "Cannot bind to the LDAP server" msgstr "无法连接到数据库" @@ -3969,6 +3973,9 @@ msgstr "硬件信息" msgid "Hash" msgstr "" +msgid "Hash Mismatch" +msgstr "" + msgid "Have not locked the host for access" msgstr "" @@ -6806,6 +6813,9 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "错误,与此主机关联的图像" +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgstr "" + msgid "One preference." msgstr "" @@ -7950,9 +7960,16 @@ msgstr "" msgid "Retention sweep failed" msgstr "去除" +msgid "Retry" +msgstr "" + msgid "Return Code" msgstr "返回代码" +#, fuzzy +msgid "Return Codes" +msgstr "返回代码" + msgid "Return To Local Login" msgstr "" @@ -9856,9 +9873,6 @@ msgstr "无法创建任务" msgid "The enrollment is no longer pending." msgstr "不复存在" -msgid "The exit code and output tail of one task. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." -msgstr "" - msgid "The fields a queued task accepts. Wake-on-lan is this body with wol set, not a route of its own." msgstr "" @@ -10021,6 +10035,9 @@ msgstr "" msgid "The primary mac associated is" msgstr "" +msgid "The program's own exit code; meaningful only for status ran." +msgstr "" + #, php-format msgid "The provider published no https %s" msgstr "" @@ -10449,6 +10466,10 @@ msgstr "时间已存在" msgid "Time since last imaged" msgstr "" +#, fuzzy +msgid "Timeout" +msgstr "时间" + msgid "Title" msgstr "" @@ -10761,6 +10782,10 @@ msgstr "发生未知上传错误。返回代码:" msgid "Unknown power action" msgstr "发生未知上传错误。返回代码:" +#, fuzzy +msgid "Unknown status." +msgstr "发生未知上传错误。返回代码:" + #, fuzzy msgid "Unknown upload error occurred" msgstr "发生未知上传错误。返回代码:" @@ -11285,6 +11310,9 @@ msgstr "" msgid "Where to get help and guides" msgstr "" +msgid "Whether the payload ran, its raw exit code when it did, and the output tail. The server reads the code against the snapin's return-code table and answers the outcome: success, reboot, retry (the task is queued again) or failed. Closes the task as the legacy check-in does, cancels the rest of a job that aborts on failure, ends the job after its last task, and records it on the host as agent.result." +msgstr "" + msgid "Which side of a deploy each architecture may be picked on. An architecture set to Hosts only is not offered on an image, and one set to Images only is not offered on a host. This never affects what a host reports at boot or what a capture records -- only what a person may choose." msgstr "" diff --git a/packages/web/src/Agent/Snapins.php b/packages/web/src/Agent/Snapins.php index 38841f46c7..0ce913dfb7 100644 --- a/packages/web/src/Agent/Snapins.php +++ b/packages/web/src/Agent/Snapins.php @@ -45,10 +45,41 @@ class Snapins extends FOGBase { /** - * stReturnDetails is varchar(250); the agent sends the tail of the - * output and this is what survives. + * stReturnDetails is TEXT; the agent sends the last 4 KB of output and + * this is the server's own bound on what it keeps. */ - const MAX_DETAILS = 250; + const MAX_DETAILS = 4096; + + /** + * What a reported run says about itself: the payload ran and the exit + * code is its own, or it never ran and this names why. + */ + const STATUS_RAN = 'ran'; + const STATUSES = ['ran', 'hash_mismatch', 'timeout', 'cannot_run']; + + /** + * What the server decides from the exit code and the snapin's + * return-code table, the way Intune and SCCM read installer codes. + */ + const OUTCOME_SUCCESS = 'success'; + const OUTCOME_REBOOT = 'reboot'; + const OUTCOME_RETRY = 'retry'; + const OUTCOME_FAILED = 'failed'; + const OUTCOMES = ['success', 'reboot', 'retry', 'failed']; + + /** + * The return-code table a snapin gets when its own is empty: Intune's + * defaults for installer exit codes. 3010 and 1641 are the two MSI + * "installed, reboot to finish" answers, 1618 is "another install is + * running". Anything not listed is failed. + */ + const DEFAULT_RETURN_CODES = [ + 0 => 'success', + 1707 => 'success', + 3010 => 'reboot', + 1641 => 'reboot', + 1618 => 'retry' + ]; /** * The tasks still to run for this host, in run order. Empty when the @@ -213,20 +244,75 @@ public static function stream(Host $Host, SnapinTask $SnapinTask) exit; } + /** + * A snapin's return-code table: its own, one `code=class` per line, + * or the defaults when it has none. Lines that are not a code and a + * known class are ignored rather than failing the run. + * + * @param Snapin $Snapin the snapin + * + * @return array code => outcome + */ + public static function returnCodes(Snapin $Snapin) + { + $table = []; + foreach (preg_split('/[\r\n,;]+/', (string)$Snapin->get('returnCodes')) as $line) { + if (!preg_match('/^\s*(-?\d+)\s*=\s*([a-z_]+)\s*$/i', (string)$line, $m)) { + continue; + } + $class = strtolower($m[2]); + if (in_array($class, self::OUTCOMES, true)) { + $table[(int)$m[1]] = $class; + } + } + return count($table) > 0 ? $table : self::DEFAULT_RETURN_CODES; + } + + /** + * What one run came to. A payload that never ran failed, whatever + * the number beside it; one that ran is read against the table, and + * a code the table does not name is failed unless it is 0. + * + * @param Snapin $Snapin the snapin + * @param string $status one of STATUSES + * @param int $exitcode the program's exit code + * + * @return string one of OUTCOMES + */ + public static function outcome(Snapin $Snapin, $status, $exitcode) + { + if (self::STATUS_RAN !== $status) { + return self::OUTCOME_FAILED; + } + $table = self::returnCodes($Snapin); + $exitcode = (int)$exitcode; + if (isset($table[$exitcode])) { + return $table[$exitcode]; + } + return 0 === $exitcode ? self::OUTCOME_SUCCESS : self::OUTCOME_FAILED; + } + /** * Records the result of one task and, when it was the last, ends the * job -- canceling the rest first when the job aborts on failure. * + * The outcome comes from the snapin's return-code table. A retry puts + * the task back in the queue with its details kept; everything else + * completes it. The task row keeps the raw code, the status (the + * outcome, or why the payload never ran) and the output. + * * @param Host $Host the principal * @param SnapinTask $SnapinTask a task ownTask() returned * @param int $exitcode the payload's exit code * @param string $details the output tail, or the agent's reason + * @param string $status one of STATUSES; the legacy client + * only ever reports a run * * @throws \RuntimeException 409 when the task is already closed * - * @return void + * @return string the outcome, one of OUTCOMES */ - public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $details) + public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $details, $status = self::STATUS_RAN) { if (in_array( (int)$SnapinTask->get('stateID'), @@ -241,13 +327,22 @@ public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $det } $exitcode = (int)$exitcode; $details = substr(trim((string)$details), 0, self::MAX_DETAILS); + $outcome = self::outcome($Snapin, $status, $exitcode); $date = self::niceDate()->format('Y-m-d H:i:s'); $HostName = (string)$Host->get('name'); $SnapinJob = $Host->get('snapinjob'); $SnapinTask - ->set('stateID', self::getCompleteState()) ->set('return', $exitcode) ->set('details', $details) + ->set('status', self::STATUS_RAN === $status ? $outcome : $status); + if (self::OUTCOME_RETRY === $outcome) { + // Back to the queue, not complete: the next check-in runs it + // again. The job stays open around it. + $SnapinTask->set('stateID', self::getQueuedState())->save(); + return $outcome; + } + $SnapinTask + ->set('stateID', self::getCompleteState()) ->set('complete', $date) ->save(); self::$EventManager->notify( @@ -267,7 +362,7 @@ public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $det ) ]; $abortedOnFailure = false; - if ($SnapinJob->get('abortOnFail') && 0 !== $exitcode) { + if ($SnapinJob->get('abortOnFail') && self::OUTCOME_FAILED === $outcome) { $abortedOnFailure = true; self::getClass('SnapinTaskManager')->update( $live, @@ -299,6 +394,7 @@ public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $det ] ); } + return $outcome; } /** @@ -307,17 +403,26 @@ public static function close(Host $Host, SnapinTask $SnapinTask, $exitcode, $det * * @param Host $Host the principal * @param int $taskID the snapin task - * @param array $body exit_code, details + * @param array $body status, exit_code, details * - * @return void + * @throws \RuntimeException 400 on a status that is not one + * + * @return string the outcome, for the agent to act on */ public static function report(Host $Host, $taskID, array $body) { $SnapinTask = self::ownTask($Host, $taskID); $name = (string)$SnapinTask->getSnapin()->get('name'); - $exitcode = (int)($body['exit_code'] ?? 1); + $status = (string)($body['status'] ?? self::STATUS_RAN); + if (!in_array($status, self::STATUSES, true)) { + throw new \RuntimeException('unknown status', 400); + } + $exitcode = (int)($body['exit_code'] ?? 0); $details = (string)($body['details'] ?? ''); - self::close($Host, $SnapinTask, $exitcode, $details); + $outcome = self::close($Host, $SnapinTask, $exitcode, $details, $status); + $summary = self::STATUS_RAN === $status + ? sprintf('exit %d, %s', $exitcode, $outcome) + : $status; Audit::record( [ 'type' => 'agent.result', @@ -327,10 +432,10 @@ public static function report(Host $Host, $taskID, array $body) 'renderable' => 1, 'text' => substr( sprintf( - 'snapin "%s" (task %d) exit %d%s', + 'snapin "%s" (task %d) %s%s', $name, (int)$SnapinTask->get('id'), - $exitcode, + $summary, '' === trim($details) ? '' : ': ' . trim($details) ), 0, @@ -339,6 +444,7 @@ public static function report(Host $Host, $taskID, array $body) 'authSource' => Principal::AUTH_SOURCE ] ); + return $outcome; } /** diff --git a/packages/web/src/Audit/SnapinStats.php b/packages/web/src/Audit/SnapinStats.php index 7b01d1ee47..7dd8a2a6f6 100644 --- a/packages/web/src/Audit/SnapinStats.php +++ b/packages/web/src/Audit/SnapinStats.php @@ -143,6 +143,7 @@ private static function _runsSql() `snapinJobs`.`sjHostID` AS `hostID`, `st`.`stCompleteDate` AS `completed`, `st`.`stReturnCode` AS `code`, + `st`.`stStatus` AS `status`, `st`.`stReturnDetails` AS `details`, `st`.`stState` AS `stateID` FROM `snapinTasks` AS `st` diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 677e3b3317..e72a205c51 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 416); + define('FOG_SCHEMA', 417); define('FOG_BCACHE_VER', 359); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Items/Snapin.php b/packages/web/src/Items/Snapin.php index 6c26320d62..305e180091 100644 --- a/packages/web/src/Items/Snapin.php +++ b/packages/web/src/Items/Snapin.php @@ -57,6 +57,7 @@ class Snapin extends FOGController 'toReplicate' => 'sReplicate', 'hide' => 'sHideLog', 'timeout' => 'sTimeout', + 'returnCodes' => 'sReturnCodes', 'packtype' => 'sPackType', 'hash' => 'sHash', 'size' => 'sSize', @@ -451,6 +452,7 @@ public static function uploadAndCreate(array $post, array $files) $action = trim((string)($post['action'] ?? '')); $args = trim((string)($post['args'] ?? '')); $timeout = trim((string)($post['timeout'] ?? '')); + $returnCodes = trim((string)($post['returnCodes'] ?? '')); if (self::getClass('SnapinManager')->exists($snapin)) { throw new \InvalidArgumentException( @@ -550,6 +552,7 @@ public static function uploadAndCreate(array $post, array $files) ->set('toReplicate', $toReplicate) ->set('hide', $hide) ->set('timeout', $timeout) + ->set('returnCodes', $returnCodes) ->addGroup($storagegroup); if (!$Snapin->save()) { throw new SnapinSaveException(_('Add snapin failed!')); diff --git a/packages/web/src/Items/SnapinTask.php b/packages/web/src/Items/SnapinTask.php index 7280dd736d..ef1ef276a7 100644 --- a/packages/web/src/Items/SnapinTask.php +++ b/packages/web/src/Items/SnapinTask.php @@ -46,7 +46,8 @@ class SnapinTask extends FOGController 'snapinID' => 'stSnapinID', 'sequence' => 'stSequence', 'return' => 'stReturnCode', - 'details' => 'stReturnDetails' + 'details' => 'stReturnDetails', + 'status' => 'stStatus' ]; /** * The grid list query, with the host joined in through the job. diff --git a/packages/web/src/Pages/GroupManagement.php b/packages/web/src/Pages/GroupManagement.php index 230a3e1fe4..a0e500dd41 100644 --- a/packages/web/src/Pages/GroupManagement.php +++ b/packages/web/src/Pages/GroupManagement.php @@ -1662,7 +1662,8 @@ public function groupSnapinHistory() _('Start Time'), _('Complete'), _('Duration'), - _('Return Code') + _('Return Code'), + _('Status') ], [ [], @@ -1670,6 +1671,7 @@ public function groupSnapinHistory() [], [], [], + [], [] ], _('Group Snapin History'), diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 6fbe9c33ec..fbcacb7b40 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -4238,13 +4238,15 @@ public function hostSnapinHistory() _('Start Time'), _('Complete'), _('Duration'), - _('Return Code') + _('Return Code'), + _('Status') ], [ [], [], [], [], + [], [] ], _('Host Snapin History'), diff --git a/packages/web/src/Pages/SnapinManagement.php b/packages/web/src/Pages/SnapinManagement.php index 6dd6e66fea..54ffe7c1fb 100644 --- a/packages/web/src/Pages/SnapinManagement.php +++ b/packages/web/src/Pages/SnapinManagement.php @@ -273,6 +273,7 @@ protected function _addFields() $rwa = filter_input(INPUT_POST, 'rwa'); $args = filter_input(INPUT_POST, 'args'); $timeout = filter_input(INPUT_POST, 'timeout'); + $returnCodes = filter_input(INPUT_POST, 'returnCodes'); if ($storagegroup > 0) { $sgID = $storagegroup; } else { @@ -523,6 +524,28 @@ protected function _addFields() 'timeout', $timeout ), + self::makeLabel( + $labelClass, + 'returnCodes', + _('Return Codes') + ) => self::makeTextarea( + 'form-control snapinreturncodes-input', + 'returnCodes', + "0=success\n1707=success\n3010=reboot\n1641=reboot\n1618=retry", + 'returnCodes', + $returnCodes, + false, + false, + 'rows="5"' + ) + . '

' + . _( + 'One per line, code=class. Classes: success, reboot ' + . '(installed, reboot to finish), retry (try again next ' + . 'check-in), failed. Empty uses the defaults shown; any ' + . 'code not listed is failed.' + ) + . '

', self::makeLabel( $labelClass, 'noaction', @@ -766,6 +789,10 @@ public function snapinGeneral() filter_input(INPUT_POST, 'timeout') ?: $this->obj->get('timeout') ); + $returnCodes = ( + filter_input(INPUT_POST, 'returnCodes') ?: + $this->obj->get('returnCodes') + ); self::$selected = $snapinfileexists; $StorageGroup = $this->obj->getStorageGroup(); @@ -1029,6 +1056,28 @@ public function snapinGeneral() 'timeout', $timeout ), + self::makeLabel( + $labelClass, + 'returnCodes', + _('Return Codes') + ) => self::makeTextarea( + 'form-control snapinreturncodes-input', + 'returnCodes', + "0=success\n1707=success\n3010=reboot\n1641=reboot\n1618=retry", + 'returnCodes', + $returnCodes, + false, + false, + 'rows="5"' + ) + . '

' + . _( + 'One per line, code=class. Classes: success, reboot ' + . '(installed, reboot to finish), retry (try again next ' + . 'check-in), failed. Empty uses the defaults shown; any ' + . 'code not listed is failed.' + ) + . '

', self::makeLabel( $labelClass, 'noaction', @@ -1156,6 +1205,7 @@ public function snapinGeneralPost() $toReplicate = (int)isset($_POST['toReplicate']); $hide = (int)isset($_POST['isHidden']); $timeout = trim((string)filter_input(INPUT_POST, 'timeout')); + $returnCodes = trim((string)filter_input(INPUT_POST, 'returnCodes')); $action = trim((string)filter_input(INPUT_POST, 'action')); $args = trim((string)filter_input(INPUT_POST, 'args')); @@ -1314,7 +1364,8 @@ public function snapinGeneralPost() ->set('isEnabled', $isEnabled) ->set('toReplicate', $toReplicate) ->set('hide', $hide) - ->set('timeout', $timeout); + ->set('timeout', $timeout) + ->set('returnCodes', $returnCodes); } /** * Display snapin storage groups. diff --git a/packages/web/src/Reports/Snapin_Report.php b/packages/web/src/Reports/Snapin_Report.php index 1479419da3..b18649d258 100644 --- a/packages/web/src/Reports/Snapin_Report.php +++ b/packages/web/src/Reports/Snapin_Report.php @@ -207,6 +207,21 @@ protected function reportRows() [$start, $end] = ReportWindow::fromRequest(self::DEFAULT_WINDOW); $rows = SnapinStats::runs($start, $end); + // The task's own status column knows about reboot/retry and about + // a payload that never ran at all (hash_mismatch/timeout/ + // cannot_run), none of which the exit code alone can tell you. It + // is the source of truth when a run recorded one; the code-based + // guess below is only for rows from before this column existed. + $outcomeLabels = [ + 'success' => _('Succeeded'), + 'reboot' => _('Reboot'), + 'retry' => _('Retry'), + 'failed' => _('Failed'), + 'hash_mismatch' => _('Hash Mismatch'), + 'timeout' => _('Timeout'), + 'cannot_run' => _('Cannot Run') + ]; + $states = []; $data = []; foreach ($rows as $row) { @@ -216,15 +231,18 @@ protected function reportRows() ->get('name'); } $code = (int)($row['code'] ?? 0); + $status = (string)($row['status'] ?? ''); $data[] = [ 'snapin' => (string)($row['snapin'] ?? ''), 'hostName' => (string)($row['hostName'] ?? ''), 'completed' => (string)($row['completed'] ?? ''), - // Said in words as well as in the code, because 0 meaning - // success is a convention rather than something the column - // says. The code stays in its own column for anyone who - // needs the actual value. - 'outcome' => 0 === $code ? _('Succeeded') : _('Failed'), + 'outcome' => '' !== $status + ? ($outcomeLabels[$status] ?? $status) + // Said in words as well as in the code, because 0 + // meaning success is a convention rather than + // something the column says. The code stays in its + // own column for anyone who needs the actual value. + : (0 === $code ? _('Succeeded') : _('Failed')), 'code' => (string)$code, 'details' => (string)($row['details'] ?? ''), // Shown beside the outcome rather than instead of it: the diff --git a/packages/web/src/Router/OpenAPI.php b/packages/web/src/Router/OpenAPI.php index 0ae28104ad..07b952699e 100644 --- a/packages/web/src/Router/OpenAPI.php +++ b/packages/web/src/Router/OpenAPI.php @@ -2722,13 +2722,26 @@ private static function _fixedPaths() '', 'agentsnapinresult', _('FOG Agent snapin result'), - _('The exit code and output tail of one task. Closes the ' - . 'task as the legacy check-in does, cancels the rest ' - . 'of a job that aborts on failure, ends the job after ' - . 'its last task, and records it on the host as ' - . 'agent.result.'), + _('Whether the payload ran, its raw exit code when it did, ' + . 'and the output tail. The server reads the code ' + . 'against the snapin\'s return-code table and answers ' + . 'the outcome: success, reboot, retry (the task is ' + . 'queued again) or failed. Closes the task as the ' + . 'legacy check-in does, cancels the rest of a job that ' + . 'aborts on failure, ends the job after its last task, ' + . 'and records it on the host as agent.result.'), [ - '200' => ['description' => _('Recorded.')], + '200' => [ + 'description' => _('Recorded.'), + 'content' => ['application/json' => ['schema' => [ + 'type' => 'object', + 'properties' => [ + 'status' => ['type' => 'string', 'enum' => ['ok']], + 'outcome' => ['type' => 'string', 'enum' => ['success', 'reboot', 'retry', 'failed']] + ] + ]]] + ], + '400' => ['description' => _('Unknown status.')], '401' => ['description' => _('No verified client certificate, or one bound to no live host.')], '404' => ['description' => _('Not a live task of this host\'s job.')] ] + self::_conflictResponse(_('The task was already closed.')), @@ -2736,10 +2749,11 @@ private static function _fixedPaths() [ 'content' => ['application/json' => ['schema' => [ 'type' => 'object', - 'required' => ['exit_code'], + 'required' => ['status', 'exit_code'], 'properties' => [ - 'exit_code' => ['type' => 'integer'], - 'details' => ['type' => 'string', 'maxLength' => 250] + 'status' => ['type' => 'string', 'enum' => ['ran', 'hash_mismatch', 'timeout', 'cannot_run']], + 'exit_code' => ['type' => 'integer', 'description' => _('The program\'s own exit code; meaningful only for status ran.')], + 'details' => ['type' => 'string', 'maxLength' => 4096] ] ]]] ] diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 2d35827469..736ea4cdf3 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -3001,7 +3001,7 @@ public static function agentSnapinResult($id) { $body = self::_jsonBody(); try { - \FOG\Agent\Snapins::report(self::$agentHost, (int)$id, (array)$body); + $outcome = \FOG\Agent\Snapins::report(self::$agentHost, (int)$id, (array)$body); } catch (\RuntimeException $e) { HTTPResponseCodes::breakHead( self::_agentErrorCode($e), @@ -3009,9 +3009,11 @@ public static function agentSnapinResult($id) ); return; } + // The outcome is the server's reading of the exit code against + // the snapin's return-code table; the agent acts on it. HTTPResponseCodes::breakHead( HTTPResponseCodes::HTTP_OK, - json_encode(['status' => 'ok']) + json_encode(['status' => 'ok', 'outcome' => $outcome]) ); } /** diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b6720665bd..5eeac403a4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -129,7 +129,7 @@ parameters: - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 386 + count: 387 path: packages/web/commons/schema.php - diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index bdf4c5a742..53ad156e2b 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -437,10 +437,11 @@ snapin 14 sEnabled isEnabled - - snapin 15 sReplicate toReplicate - - snapin 16 sHideLog hide - - snapin 17 sTimeout timeout - - -snapin 18 sPackType packtype - - -snapin 19 sHash hash - - -snapin 20 sSize size - - -snapin 21 sAnon3 anon3 - - +snapin 18 sReturnCodes returnCodes - - +snapin 19 sPackType packtype - - +snapin 20 sHash hash - - +snapin 21 sSize size - - +snapin 22 sAnon3 anon3 - - snapinassociation 0 saID id - - snapinassociation 1 saID DT_RowId f - snapinassociation 2 saHostID hostID - - @@ -473,15 +474,16 @@ snapintask 7 stSnapinID snapinLink f:node,relclass snapin snapintask 8 stSequence sequence - - snapintask 9 stReturnCode return - - snapintask 10 stReturnDetails details - - -snapintask 11 stJobID hostID f:snapinTaskHost snapinjob -snapintask 12 stJobID hostname f:snapinTaskHost - -snapintask 13 stJobID hostLink f:snapinTaskHost - -snapintask 14 stState taskstateicon f taskstate -snapintask 15 stState taskstatename f taskstate -snapintask 16 stSnapinID snapinID f snapin -snapintask 17 stSnapinID snapinname f snapin -snapintask 18 stSnapinID snapinLink f snapin -snapintask 19 stCheckinDate diff f - +snapintask 11 stStatus status - - +snapintask 12 stJobID hostID f:snapinTaskHost snapinjob +snapintask 13 stJobID hostname f:snapinTaskHost - +snapintask 14 stJobID hostLink f:snapinTaskHost - +snapintask 15 stState taskstateicon f taskstate +snapintask 16 stState taskstatename f taskstate +snapintask 17 stSnapinID snapinID f snapin +snapintask 18 stSnapinID snapinname f snapin +snapintask 19 stSnapinID snapinLink f snapin +snapintask 20 stCheckinDate diff f - storagegroup 0 ngID id - - storagegroup 1 ngID DT_RowId f - storagegroup 2 ngName name - - From 65cf3ed32e5cef60b24b8c736808d450f60ec566 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 13:42:20 -0500 Subject: [PATCH 017/117] Regenerate the schema manifest at 417 Generated from a live install after the 417 update: sReturnCodes, stStatus and the TEXT stReturnDetails, plus the foreign-key backing indexes the reconciler's constraint pass created on that install, which the generator keeps by design. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- packages/web/commons/schema-expected.php | 54 +++++++++---------- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - 11 files changed, 26 insertions(+), 38 deletions(-) diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index f1c25d052a..d84b6a79c1 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -70,15 +70,11 @@ 'retired' => [ [ 'table' => 'imagingLog', - 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging' - . ' run now, so the table was retired rather than ported', + 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging run now, so the table was retired rather than ported', ], [ 'table' => 'virus', - 'reason' => 'GH-328 -- the ClamAV scan is removed. 1.6 never' - . ' carried service/av.php across from 1.5, so nothing on' - . ' this branch has ever written the table and no model,' - . ' manager, report or page reads it', + 'reason' => 'GH-328 -- the ClamAV scan is removed. 1.6 never carried service/av.php across from 1.5, so nothing on this branch has ever written the table and no model, manager, report or page reads it', ], ], 'tables' => [ @@ -154,7 +150,7 @@ ], ], 'auditLog' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `auditLog` ( `alID` int(11) NOT NULL AUTO_INCREMENT, `alCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `alCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `alIP` varchar(45) NOT NULL DEFAULT \'\', `alAuthSource` varchar(64) NOT NULL DEFAULT \'\', `alType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectID` int(11) NOT NULL DEFAULT 0, `alSubjectLabel` varchar(255) NOT NULL DEFAULT \'\', `alPermission` varchar(128) NOT NULL DEFAULT \'\', `alOutcome` enum(\'unknown\',\'allowed\',\'denied\',\'failed\',\'partial\') NOT NULL DEFAULT \'unknown\', `alCorrelationID` varchar(32) NOT NULL DEFAULT \'\', `alAffectedCount` int(11) NOT NULL DEFAULT 0, `alRenderable` tinyint(1) unsigned NOT NULL DEFAULT 1, `alText` longtext NOT NULL DEFAULT \'\', `alActedAs` varchar(255) NOT NULL DEFAULT \'\', `alSpanID` varchar(32) NOT NULL DEFAULT \'\', PRIMARY KEY (`alID`), KEY `alCreatedTime` (`alCreatedTime`), KEY `alCreatedBy` (`alCreatedBy`), KEY `alCorrelationID` (`alCorrelationID`), KEY `alOutcome` (`alOutcome`), KEY `alSubject` (`alSubjectType`,`alSubjectID`), KEY `alSpanID` (`alSpanID`), KEY `alActedAs` (`alActedAs`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `auditLog` ( `alID` int(11) NOT NULL AUTO_INCREMENT, `alCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `alCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `alIP` varchar(45) NOT NULL DEFAULT \'\', `alAuthSource` varchar(64) NOT NULL DEFAULT \'\', `alType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectType` varchar(64) NOT NULL DEFAULT \'\', `alSubjectID` int(11) NOT NULL DEFAULT 0, `alSubjectLabel` varchar(255) NOT NULL DEFAULT \'\', `alPermission` varchar(128) NOT NULL DEFAULT \'\', `alOutcome` enum(\'unknown\',\'allowed\',\'denied\',\'failed\',\'partial\') NOT NULL DEFAULT \'unknown\', `alCorrelationID` varchar(32) NOT NULL DEFAULT \'\', `alAffectedCount` int(11) NOT NULL DEFAULT 0, `alRenderable` tinyint(1) unsigned NOT NULL DEFAULT 1, `alText` longtext NOT NULL DEFAULT \'\', `alActedAs` varchar(255) NOT NULL DEFAULT \'\', `alSpanID` varchar(32) NOT NULL DEFAULT \'\', PRIMARY KEY (`alID`), KEY `alCreatedTime` (`alCreatedTime`), KEY `alCreatedBy` (`alCreatedBy`), KEY `alCorrelationID` (`alCorrelationID`), KEY `alOutcome` (`alOutcome`), KEY `alSubject` (`alSubjectType`,`alSubjectID`), KEY `alActedAs` (`alActedAs`), KEY `alSpanID` (`alSpanID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'alID' => 'int(11) NOT NULL', 'alCreatedTime' => 'datetime NOT NULL DEFAULT current_timestamp()', @@ -215,7 +211,7 @@ ], ], 'fileDeleteQueue' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `fileDeleteQueue` ( `fdqID` int(11) NOT NULL AUTO_INCREMENT, `fdqPathName` varchar(255) NOT NULL, `fdqStorageGroupID` int(11) NOT NULL, `fdqCreateDate` datetime DEFAULT current_timestamp(), `fdqCompletedDate` datetime DEFAULT NULL, `fdqCreateBy` varchar(40) DEFAULT NULL, `fdqState` int(11) NOT NULL DEFAULT 0, `fdqPathType` varchar(255) NOT NULL, PRIMARY KEY (`fdqID`), KEY `idx_fdqCreateDate` (`fdqCreateDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `fileDeleteQueue` ( `fdqID` int(11) NOT NULL AUTO_INCREMENT, `fdqPathName` varchar(255) NOT NULL, `fdqStorageGroupID` int(11) NOT NULL, `fdqCreateDate` datetime DEFAULT current_timestamp(), `fdqCompletedDate` datetime DEFAULT NULL, `fdqCreateBy` varchar(40) DEFAULT NULL, `fdqState` int(11) NOT NULL DEFAULT 0, `fdqPathType` varchar(255) NOT NULL, PRIMARY KEY (`fdqID`), KEY `idx_fdqCreateDate` (`fdqCreateDate`), KEY `fk_fileDeleteQueue_fdqStorageGroupID` (`fdqStorageGroupID`), KEY `fk_fileDeleteQueue_fdqState` (`fdqState`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'fdqID' => 'int(11) NOT NULL', 'fdqPathName' => 'varchar(255) NOT NULL', @@ -354,7 +350,7 @@ ], ], 'hosts' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT \'\', `hostAgentNotAfter` datetime DEFAULT NULL, `hostAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `hostAgentCheckin` datetime DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`), KEY `hostAgentFingerprint` (`hostAgentFingerprint`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT \'\', `hostAgentNotAfter` datetime DEFAULT NULL, `hostAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `hostAgentCheckin` datetime DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`), KEY `fk_hosts_hostImage` (`hostImage`), KEY `fk_hosts_hostArchID` (`hostArchID`), KEY `hostAgentFingerprint` (`hostAgentFingerprint`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'hostID' => 'int(11) NOT NULL', 'hostName' => 'varchar(16) NOT NULL', @@ -386,10 +382,6 @@ 'hostExitEfi' => 'longtext DEFAULT NULL', 'hostEnforce' => 'tinyint(1) NOT NULL DEFAULT 1', 'hostInfoKey' => 'varchar(255) DEFAULT NULL', - 'hostAgentFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', - 'hostAgentNotAfter' => 'datetime DEFAULT NULL', - 'hostAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', - 'hostAgentCheckin' => 'datetime DEFAULT NULL', 'hostInfoLock' => 'tinyint(1) DEFAULT 0', 'hostSecTokenPrev' => 'longtext NOT NULL DEFAULT \'\'', 'hostLastPing' => 'datetime DEFAULT NULL', @@ -401,6 +393,10 @@ 'hostSbEnrolled' => 'datetime DEFAULT NULL', 'hostSbEnrollCert' => 'varchar(95) DEFAULT NULL', 'hostSbEnrollVia' => 'varchar(16) DEFAULT NULL', + 'hostAgentFingerprint' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'hostAgentNotAfter' => 'datetime DEFAULT NULL', + 'hostAgentVersion' => 'varchar(50) NOT NULL DEFAULT \'\'', + 'hostAgentCheckin' => 'datetime DEFAULT NULL', ], ], 'hostScreenSettings' => [ @@ -417,7 +413,7 @@ ], ], 'imageGroupAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `imageGroupAssoc` ( `igaID` mediumint(9) NOT NULL AUTO_INCREMENT, `igaImageID` int(11) NOT NULL, `igaStorageGroupID` int(11) NOT NULL, `igaPrimary` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`igaID`), UNIQUE KEY `igaImageID` (`igaImageID`,`igaStorageGroupID`), UNIQUE KEY `igaImageID_2` (`igaImageID`,`igaStorageGroupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `imageGroupAssoc` ( `igaID` mediumint(9) NOT NULL AUTO_INCREMENT, `igaImageID` int(11) NOT NULL, `igaStorageGroupID` int(11) NOT NULL, `igaPrimary` tinyint(1) NOT NULL DEFAULT 0, PRIMARY KEY (`igaID`), UNIQUE KEY `igaImageID` (`igaImageID`,`igaStorageGroupID`), UNIQUE KEY `igaImageID_2` (`igaImageID`,`igaStorageGroupID`), KEY `fk_imageGroupAssoc_igaStorageGroupID` (`igaStorageGroupID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'igaID' => 'mediumint(9) NOT NULL', 'igaImageID' => 'int(11) NOT NULL', @@ -434,7 +430,7 @@ ], ], 'images' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `images` ( `imageID` int(11) NOT NULL AUTO_INCREMENT, `imageName` varchar(40) NOT NULL, `imageDesc` longtext NOT NULL DEFAULT \'\', `imagePath` longtext NOT NULL, `imageProtect` mediumint(9) NOT NULL DEFAULT 0, `imageMagnetUri` longtext NOT NULL DEFAULT \'\', `imageDateTime` timestamp NOT NULL DEFAULT current_timestamp(), `imageCreateBy` varchar(50) NOT NULL DEFAULT \'\', `imageBuilding` int(11) NOT NULL DEFAULT 0, `imageSize` varchar(255) NOT NULL DEFAULT \'\', `imageTypeID` mediumint(9) NOT NULL, `imagePartitionTypeID` mediumint(9) NOT NULL, `imageOSID` mediumint(9) DEFAULT NULL, `imageFormat` char(1) DEFAULT NULL, `imageLastDeploy` datetime DEFAULT NULL, `imageCompress` int(11) DEFAULT NULL, `imageEnabled` tinyint(1) NOT NULL DEFAULT 1, `imageReplicate` tinyint(1) NOT NULL DEFAULT 1, `imageServerSize` bigint(20) unsigned NOT NULL DEFAULT 0, `imageSectorSize` int(11) DEFAULT NULL, `imageArchID` mediumint(9) DEFAULT NULL, PRIMARY KEY (`imageID`), UNIQUE KEY `imageName` (`imageName`), KEY `new_index` (`imageName`), KEY `new_index1` (`imageBuilding`), KEY `new_index2` (`imageTypeID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `images` ( `imageID` int(11) NOT NULL AUTO_INCREMENT, `imageName` varchar(40) NOT NULL, `imageDesc` longtext NOT NULL DEFAULT \'\', `imagePath` longtext NOT NULL, `imageProtect` mediumint(9) NOT NULL DEFAULT 0, `imageMagnetUri` longtext NOT NULL DEFAULT \'\', `imageDateTime` timestamp NOT NULL DEFAULT current_timestamp(), `imageCreateBy` varchar(50) NOT NULL DEFAULT \'\', `imageBuilding` int(11) NOT NULL DEFAULT 0, `imageSize` varchar(255) NOT NULL DEFAULT \'\', `imageTypeID` mediumint(9) NOT NULL, `imagePartitionTypeID` mediumint(9) NOT NULL, `imageOSID` mediumint(9) DEFAULT NULL, `imageFormat` char(1) DEFAULT NULL, `imageLastDeploy` datetime DEFAULT NULL, `imageCompress` int(11) DEFAULT NULL, `imageEnabled` tinyint(1) NOT NULL DEFAULT 1, `imageReplicate` tinyint(1) NOT NULL DEFAULT 1, `imageServerSize` bigint(20) unsigned NOT NULL DEFAULT 0, `imageSectorSize` int(11) DEFAULT NULL, `imageArchID` mediumint(9) DEFAULT NULL, PRIMARY KEY (`imageID`), UNIQUE KEY `imageName` (`imageName`), KEY `new_index` (`imageName`), KEY `new_index1` (`imageBuilding`), KEY `new_index2` (`imageTypeID`), KEY `fk_images_imageOSID` (`imageOSID`), KEY `fk_images_imagePartitionTypeID` (`imagePartitionTypeID`), KEY `fk_images_imageArchID` (`imageArchID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'imageID' => 'int(11) NOT NULL', 'imageName' => 'varchar(40) NOT NULL', @@ -548,7 +544,7 @@ ], ], 'multicastSessions' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `multicastSessions` ( `msID` int(11) NOT NULL AUTO_INCREMENT, `msName` varchar(250) NOT NULL DEFAULT \'\', `msBasePort` int(11) NOT NULL DEFAULT 0, `msLogPath` longtext NOT NULL DEFAULT \'\', `msImage` longtext NOT NULL DEFAULT \'\', `msClients` int(11) NOT NULL DEFAULT 0, `msSessClients` int(11) NOT NULL DEFAULT 0, `msInterface` varchar(250) NOT NULL DEFAULT \'\', `msStartDateTime` datetime DEFAULT NULL, `msPercent` int(11) NOT NULL DEFAULT 0, `msState` int(11) DEFAULT NULL, `msCompleteDateTime` datetime DEFAULT NULL, `msIsDD` int(11) NOT NULL DEFAULT 0, `msNFSGroupID` int(11) NOT NULL, `msShutdown` tinyint(1) NOT NULL DEFAULT 0, `msMaxwait` int(11) NOT NULL DEFAULT 0, `msAnon5` varchar(250) NOT NULL DEFAULT \'\', `msSenderPID` int(11) NOT NULL DEFAULT 0, `msSenderNode` int(11) DEFAULT NULL, `msSenderStart` datetime DEFAULT NULL, PRIMARY KEY (`msID`), KEY `new_index` (`msNFSGroupID`), KEY `idx_msStartDateTime` (`msStartDateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `multicastSessions` ( `msID` int(11) NOT NULL AUTO_INCREMENT, `msName` varchar(250) NOT NULL DEFAULT \'\', `msBasePort` int(11) NOT NULL DEFAULT 0, `msLogPath` longtext NOT NULL DEFAULT \'\', `msImage` longtext NOT NULL DEFAULT \'\', `msClients` int(11) NOT NULL DEFAULT 0, `msSessClients` int(11) NOT NULL DEFAULT 0, `msInterface` varchar(250) NOT NULL DEFAULT \'\', `msStartDateTime` datetime DEFAULT NULL, `msPercent` int(11) NOT NULL DEFAULT 0, `msState` int(11) DEFAULT NULL, `msCompleteDateTime` datetime DEFAULT NULL, `msIsDD` int(11) NOT NULL DEFAULT 0, `msNFSGroupID` int(11) NOT NULL, `msShutdown` tinyint(1) NOT NULL DEFAULT 0, `msMaxwait` int(11) NOT NULL DEFAULT 0, `msAnon5` varchar(250) NOT NULL DEFAULT \'\', `msSenderPID` int(11) NOT NULL DEFAULT 0, `msSenderNode` int(11) DEFAULT NULL, `msSenderStart` datetime DEFAULT NULL, PRIMARY KEY (`msID`), KEY `new_index` (`msNFSGroupID`), KEY `idx_msStartDateTime` (`msStartDateTime`), KEY `fk_multicastSessions_msSenderNode` (`msSenderNode`), KEY `fk_multicastSessions_msState` (`msState`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'msID' => 'int(11) NOT NULL', 'msName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -592,7 +588,7 @@ ], ], 'nfsGroupMembers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `nfsGroupMembers` ( `ngmID` int(11) NOT NULL AUTO_INCREMENT, `ngmMemberName` varchar(250) NOT NULL DEFAULT \'\', `ngmMemberDescription` longtext NOT NULL DEFAULT \'\', `ngmIsMasterNode` char(1) NOT NULL DEFAULT \'\', `ngmGroupID` int(11) NOT NULL, `ngmRootPath` longtext NOT NULL, `ngmSSLPath` longtext NOT NULL DEFAULT \'\', `ngmFTPPath` longtext NOT NULL, `ngmMaxBitrate` varchar(25) DEFAULT NULL, `ngmHelloInterval` varchar(8) DEFAULT NULL, `ngmGraphColor` varchar(6) DEFAULT NULL, `ngmSnapinPath` longtext NOT NULL DEFAULT \'\', `ngmIsEnabled` char(1) NOT NULL DEFAULT \'\', `ngmHostname` varchar(250) NOT NULL, `ngmMaxClients` int(11) NOT NULL DEFAULT 0, `ngmBandwidthLimit` int(20) NOT NULL DEFAULT 0, `ngmUser` varchar(250) NOT NULL, `ngmPass` varchar(250) NOT NULL, `ngmKey` varchar(250) NOT NULL DEFAULT \'\', `ngmInterface` varchar(25) NOT NULL DEFAULT \'\', `ngmGraphEnabled` tinyint(1) NOT NULL DEFAULT 1, `ngmWebroot` longtext NOT NULL DEFAULT \'\', PRIMARY KEY (`ngmID`), UNIQUE KEY `ngmMemberName` (`ngmMemberName`), UNIQUE KEY `ngmMemberName_2` (`ngmMemberName`), KEY `new_index` (`ngmMemberName`), KEY `new_index2` (`ngmIsMasterNode`), KEY `new_index3` (`ngmGroupID`), KEY `new_index4` (`ngmIsEnabled`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `nfsGroupMembers` ( `ngmID` int(11) NOT NULL AUTO_INCREMENT, `ngmMemberName` varchar(250) NOT NULL DEFAULT \'\', `ngmMemberDescription` longtext NOT NULL DEFAULT \'\', `ngmIsMasterNode` char(1) NOT NULL DEFAULT \'\', `ngmGroupID` int(11) NOT NULL, `ngmRootPath` longtext NOT NULL, `ngmSSLPath` longtext NOT NULL DEFAULT \'\', `ngmFTPPath` longtext NOT NULL, `ngmMaxBitrate` varchar(25) DEFAULT NULL, `ngmHelloInterval` varchar(8) DEFAULT NULL, `ngmGraphColor` varchar(6) DEFAULT NULL, `ngmSnapinPath` longtext NOT NULL DEFAULT \'\', `ngmIsEnabled` char(1) NOT NULL DEFAULT \'\', `ngmHostname` varchar(250) NOT NULL, `ngmMaxClients` int(11) NOT NULL DEFAULT 0, `ngmBandwidthLimit` int(20) NOT NULL DEFAULT 0, `ngmUser` varchar(250) NOT NULL, `ngmPass` varchar(250) NOT NULL, `ngmKey` varchar(250) NOT NULL DEFAULT \'\', `ngmInterface` varchar(25) NOT NULL DEFAULT \'enp58s0u2u4\', `ngmGraphEnabled` tinyint(1) NOT NULL DEFAULT 1, `ngmWebroot` longtext NOT NULL DEFAULT \'\', PRIMARY KEY (`ngmID`), UNIQUE KEY `ngmMemberName` (`ngmMemberName`), UNIQUE KEY `ngmMemberName_2` (`ngmMemberName`), KEY `new_index` (`ngmMemberName`), KEY `new_index2` (`ngmIsMasterNode`), KEY `new_index3` (`ngmGroupID`), KEY `new_index4` (`ngmIsEnabled`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ngmID' => 'int(11) NOT NULL', 'ngmMemberName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -613,7 +609,7 @@ 'ngmUser' => 'varchar(250) NOT NULL', 'ngmPass' => 'varchar(250) NOT NULL', 'ngmKey' => 'varchar(250) NOT NULL DEFAULT \'\'', - 'ngmInterface' => 'varchar(25) NOT NULL DEFAULT \'\'', + 'ngmInterface' => 'varchar(25) NOT NULL DEFAULT \'enp58s0u2u4\'', 'ngmGraphEnabled' => 'tinyint(1) NOT NULL DEFAULT 1', 'ngmWebroot' => 'longtext NOT NULL DEFAULT \'\'', ], @@ -745,7 +741,7 @@ ], ], 'roleUserAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserAssoc` ( `ruaID` int(11) NOT NULL AUTO_INCREMENT, `ruaName` varchar(60) NOT NULL DEFAULT \'\', `ruaRoleID` int(11) NOT NULL, `ruaUserID` int(11) NOT NULL, PRIMARY KEY (`ruaID`), UNIQUE KEY `ruaRoleUser` (`ruaRoleID`,`ruaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserAssoc` ( `ruaID` int(11) NOT NULL AUTO_INCREMENT, `ruaName` varchar(60) NOT NULL DEFAULT \'\', `ruaRoleID` int(11) NOT NULL, `ruaUserID` int(11) NOT NULL, PRIMARY KEY (`ruaID`), UNIQUE KEY `ruaRoleUser` (`ruaRoleID`,`ruaUserID`), KEY `fk_roleUserAssoc_ruaUserID` (`ruaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ruaID' => 'int(11) NOT NULL', 'ruaName' => 'varchar(60) NOT NULL DEFAULT \'\'', @@ -754,7 +750,7 @@ ], ], 'roleUserGroupAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserGroupAssoc` ( `rugID` int(11) NOT NULL AUTO_INCREMENT, `rugName` varchar(60) NOT NULL DEFAULT \'\', `rugGroupID` int(11) NOT NULL, `rugRoleID` int(11) NOT NULL, PRIMARY KEY (`rugID`), UNIQUE KEY `rugGroupRole` (`rugGroupID`,`rugRoleID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `roleUserGroupAssoc` ( `rugID` int(11) NOT NULL AUTO_INCREMENT, `rugName` varchar(60) NOT NULL DEFAULT \'\', `rugGroupID` int(11) NOT NULL, `rugRoleID` int(11) NOT NULL, PRIMARY KEY (`rugID`), UNIQUE KEY `rugGroupRole` (`rugGroupID`,`rugRoleID`), KEY `fk_roleUserGroupAssoc_rugRoleID` (`rugRoleID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'rugID' => 'int(11) NOT NULL', 'rugName' => 'varchar(60) NOT NULL DEFAULT \'\'', @@ -800,7 +796,7 @@ ], ], 'scheduledTasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `scheduledTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stName` varchar(240) NOT NULL DEFAULT \'\', `stDesc` longtext NOT NULL DEFAULT \'\', `stType` varchar(24) NOT NULL, `stTaskTypeID` mediumint(9) NOT NULL, `stMinute` varchar(240) NOT NULL DEFAULT \'\', `stHour` varchar(240) NOT NULL DEFAULT \'\', `stDOM` varchar(240) NOT NULL DEFAULT \'\', `stMonth` varchar(240) NOT NULL DEFAULT \'\', `stDOW` varchar(240) NOT NULL DEFAULT \'\', `stIsGroup` varchar(2) NOT NULL DEFAULT \'0\', `stGroupHostID` int(11) NOT NULL, `stImageID` int(11) DEFAULT NULL, `stShutDown` varchar(2) NOT NULL DEFAULT \'\', `stOther1` varchar(240) NOT NULL DEFAULT \'\', `stOther2` varchar(240) NOT NULL DEFAULT \'\', `stOther3` varchar(240) NOT NULL DEFAULT \'\', `stOther4` varchar(240) NOT NULL DEFAULT \'\', `stOther5` varchar(240) NOT NULL DEFAULT \'\', `stDateTime` bigint(20) unsigned NOT NULL DEFAULT 0, `stActive` varchar(2) NOT NULL DEFAULT \'1\', PRIMARY KEY (`stID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `scheduledTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stName` varchar(240) NOT NULL DEFAULT \'\', `stDesc` longtext NOT NULL DEFAULT \'\', `stType` varchar(24) NOT NULL, `stTaskTypeID` mediumint(9) NOT NULL, `stMinute` varchar(240) NOT NULL DEFAULT \'\', `stHour` varchar(240) NOT NULL DEFAULT \'\', `stDOM` varchar(240) NOT NULL DEFAULT \'\', `stMonth` varchar(240) NOT NULL DEFAULT \'\', `stDOW` varchar(240) NOT NULL DEFAULT \'\', `stIsGroup` varchar(2) NOT NULL DEFAULT \'0\', `stGroupHostID` int(11) NOT NULL, `stImageID` int(11) DEFAULT NULL, `stShutDown` varchar(2) NOT NULL DEFAULT \'\', `stOther1` varchar(240) NOT NULL DEFAULT \'\', `stOther2` varchar(240) NOT NULL DEFAULT \'\', `stOther3` varchar(240) NOT NULL DEFAULT \'\', `stOther4` varchar(240) NOT NULL DEFAULT \'\', `stOther5` varchar(240) NOT NULL DEFAULT \'\', `stDateTime` bigint(20) unsigned NOT NULL DEFAULT 0, `stActive` varchar(2) NOT NULL DEFAULT \'1\', PRIMARY KEY (`stID`), KEY `fk_scheduledTasks_stTaskTypeID` (`stTaskTypeID`), KEY `fk_scheduledTasks_stImageID` (`stImageID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'stID' => 'int(11) NOT NULL', 'stName' => 'varchar(240) NOT NULL DEFAULT \'\'', @@ -914,7 +910,7 @@ ], ], 'snapinJobs' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapinJobs` ( `sjID` int(11) NOT NULL AUTO_INCREMENT, `sjHostID` int(11) NOT NULL, `sjStateID` int(11) NOT NULL, `sjAbortOnFail` tinyint(1) NOT NULL DEFAULT 0, `sjCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`sjID`), KEY `new_index` (`sjHostID`), KEY `idx_sjCreateTime` (`sjCreateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapinJobs` ( `sjID` int(11) NOT NULL AUTO_INCREMENT, `sjHostID` int(11) NOT NULL, `sjStateID` int(11) NOT NULL, `sjAbortOnFail` tinyint(1) NOT NULL DEFAULT 0, `sjCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), PRIMARY KEY (`sjID`), KEY `new_index` (`sjHostID`), KEY `idx_sjCreateTime` (`sjCreateTime`), KEY `fk_snapinJobs_sjStateID` (`sjStateID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'sjID' => 'int(11) NOT NULL', 'sjHostID' => 'int(11) NOT NULL', @@ -924,7 +920,7 @@ ], ], 'snapins' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapins` ( `sID` int(11) NOT NULL AUTO_INCREMENT, `sName` varchar(200) NOT NULL, `sDesc` longtext NOT NULL DEFAULT \'\', `sFilePath` longtext NOT NULL, `sArgs` longtext NOT NULL DEFAULT \'\', `sCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `sCreator` varchar(200) NOT NULL DEFAULT \'\', `sReboot` varchar(1) NOT NULL DEFAULT \'\', `sRunWith` varchar(245) NOT NULL DEFAULT \'\', `sRunWithArgs` varchar(200) NOT NULL DEFAULT \'\', `sAnon3` varchar(45) NOT NULL DEFAULT \'\', `snapinProtect` mediumint(9) NOT NULL DEFAULT 0, `sEnabled` tinyint(1) NOT NULL DEFAULT 1, `sReplicate` tinyint(1) NOT NULL DEFAULT 1, `sShutdown` tinyint(1) NOT NULL DEFAULT 0, `sHideLog` tinyint(1) NOT NULL DEFAULT 0, `sTimeout` int(11) NOT NULL DEFAULT 0, `sPackType` tinyint(1) NOT NULL DEFAULT 0, `sHash` varchar(255) NOT NULL DEFAULT \'\', `sSize` bigint(20) NOT NULL DEFAULT 0, PRIMARY KEY (`sID`), UNIQUE KEY `sName` (`sName`), KEY `new_index` (`sName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapins` ( `sID` int(11) NOT NULL AUTO_INCREMENT, `sName` varchar(200) NOT NULL, `sDesc` longtext NOT NULL DEFAULT \'\', `sFilePath` longtext NOT NULL, `sArgs` longtext NOT NULL DEFAULT \'\', `sCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `sCreator` varchar(200) NOT NULL DEFAULT \'\', `sReboot` varchar(1) NOT NULL DEFAULT \'\', `sRunWith` varchar(245) NOT NULL DEFAULT \'\', `sRunWithArgs` varchar(200) NOT NULL DEFAULT \'\', `sAnon3` varchar(45) NOT NULL DEFAULT \'\', `snapinProtect` mediumint(9) NOT NULL DEFAULT 0, `sEnabled` tinyint(1) NOT NULL DEFAULT 1, `sReplicate` tinyint(1) NOT NULL DEFAULT 1, `sShutdown` tinyint(1) NOT NULL DEFAULT 0, `sHideLog` tinyint(1) NOT NULL DEFAULT 0, `sTimeout` int(11) NOT NULL DEFAULT 0, `sPackType` tinyint(1) NOT NULL DEFAULT 0, `sHash` varchar(255) NOT NULL DEFAULT \'\', `sSize` bigint(20) NOT NULL DEFAULT 0, `sReturnCodes` text DEFAULT NULL, PRIMARY KEY (`sID`), UNIQUE KEY `sName` (`sName`), KEY `new_index` (`sName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'sID' => 'int(11) NOT NULL', 'sName' => 'varchar(200) NOT NULL', @@ -946,10 +942,11 @@ 'sPackType' => 'tinyint(1) NOT NULL DEFAULT 0', 'sHash' => 'varchar(255) NOT NULL DEFAULT \'\'', 'sSize' => 'bigint(20) NOT NULL DEFAULT 0', + 'sReturnCodes' => 'text DEFAULT NULL', ], ], 'snapinTasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stReturnDetails` varchar(250) NOT NULL DEFAULT \'\', PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stStatus` varchar(16) NOT NULL DEFAULT \'\', `stReturnDetails` text NOT NULL, PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'stID' => 'int(11) NOT NULL', 'stJobID' => 'int(11) NOT NULL', @@ -959,7 +956,8 @@ 'stSnapinID' => 'int(11) NOT NULL', 'stSequence' => 'int(11) NOT NULL DEFAULT 0', 'stReturnCode' => 'int(11) NOT NULL DEFAULT 0', - 'stReturnDetails' => 'varchar(250) NOT NULL DEFAULT \'\'', + 'stStatus' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'stReturnDetails' => 'text NOT NULL', ], ], 'storageEpoch' => [ @@ -998,7 +996,7 @@ ], ], 'tasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `tasks` ( `taskID` int(11) NOT NULL AUTO_INCREMENT, `taskName` varchar(250) NOT NULL DEFAULT \'\', `taskCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), `taskCheckIn` datetime DEFAULT NULL, `taskHostID` int(11) NOT NULL, `taskImageID` int(11) DEFAULT NULL, `taskStateID` int(11) NOT NULL, `taskIsDebug` mediumint(9) NOT NULL DEFAULT 0, `taskCreateBy` varchar(200) NOT NULL DEFAULT \'\', `taskForce` varchar(1) NOT NULL DEFAULT \'\', `taskScheduledStartTime` datetime DEFAULT NULL, `taskTypeID` mediumint(9) NOT NULL, `taskPCT` int(10) unsigned zerofill NOT NULL DEFAULT 0000000000, `taskBPM` varchar(250) NOT NULL DEFAULT \'\', `taskTimeElapsed` varchar(250) NOT NULL DEFAULT \'\', `taskTimeRemaining` varchar(250) NOT NULL DEFAULT \'\', `taskDataCopied` varchar(250) NOT NULL DEFAULT \'\', `taskPercentText` varchar(250) NOT NULL DEFAULT \'\', `taskDataTotal` varchar(250) NOT NULL DEFAULT \'\', `taskNFSGroupID` int(11) DEFAULT NULL, `taskNFSMemberID` int(11) DEFAULT NULL, `taskNFSFailures` char(1) NOT NULL DEFAULT \'\', `taskLastMemberID` int(11) DEFAULT NULL, `taskWOL` tinyint(1) NOT NULL DEFAULT 0, `taskPassreset` varchar(250) NOT NULL DEFAULT \'\', `taskShutdown` char(1) NOT NULL DEFAULT \'\', `taskBypassBitlocker` tinyint(1) NOT NULL DEFAULT 0, `taskStateChangedTime` datetime DEFAULT NULL, PRIMARY KEY (`taskID`), KEY `new_index` (`taskHostID`), KEY `new_index1` (`taskCheckIn`), KEY `new_index2` (`taskStateID`), KEY `new_index3` (`taskForce`), KEY `new_index4` (`taskTypeID`), KEY `new_index5` (`taskNFSGroupID`), KEY `new_index6` (`taskNFSMemberID`), KEY `new_index7` (`taskNFSFailures`), KEY `new_index8` (`taskLastMemberID`), KEY `idx_taskCreateTime` (`taskCreateTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `tasks` ( `taskID` int(11) NOT NULL AUTO_INCREMENT, `taskName` varchar(250) NOT NULL DEFAULT \'\', `taskCreateTime` timestamp NOT NULL DEFAULT current_timestamp(), `taskCheckIn` datetime DEFAULT NULL, `taskHostID` int(11) NOT NULL, `taskImageID` int(11) DEFAULT NULL, `taskStateID` int(11) NOT NULL, `taskIsDebug` mediumint(9) NOT NULL DEFAULT 0, `taskCreateBy` varchar(200) NOT NULL DEFAULT \'\', `taskForce` varchar(1) NOT NULL DEFAULT \'\', `taskScheduledStartTime` datetime DEFAULT NULL, `taskTypeID` mediumint(9) NOT NULL, `taskPCT` int(10) unsigned zerofill NOT NULL DEFAULT 0000000000, `taskBPM` varchar(250) NOT NULL DEFAULT \'\', `taskTimeElapsed` varchar(250) NOT NULL DEFAULT \'\', `taskTimeRemaining` varchar(250) NOT NULL DEFAULT \'\', `taskDataCopied` varchar(250) NOT NULL DEFAULT \'\', `taskPercentText` varchar(250) NOT NULL DEFAULT \'\', `taskDataTotal` varchar(250) NOT NULL DEFAULT \'\', `taskNFSGroupID` int(11) DEFAULT NULL, `taskNFSMemberID` int(11) DEFAULT NULL, `taskNFSFailures` char(1) NOT NULL DEFAULT \'\', `taskLastMemberID` int(11) DEFAULT NULL, `taskWOL` tinyint(1) NOT NULL DEFAULT 0, `taskPassreset` varchar(250) NOT NULL DEFAULT \'\', `taskShutdown` char(1) NOT NULL DEFAULT \'\', `taskBypassBitlocker` tinyint(1) NOT NULL DEFAULT 0, `taskStateChangedTime` datetime DEFAULT NULL, PRIMARY KEY (`taskID`), KEY `new_index` (`taskHostID`), KEY `new_index1` (`taskCheckIn`), KEY `new_index2` (`taskStateID`), KEY `new_index3` (`taskForce`), KEY `new_index4` (`taskTypeID`), KEY `new_index5` (`taskNFSGroupID`), KEY `new_index6` (`taskNFSMemberID`), KEY `new_index7` (`taskNFSFailures`), KEY `new_index8` (`taskLastMemberID`), KEY `idx_taskCreateTime` (`taskCreateTime`), KEY `fk_tasks_taskImageID` (`taskImageID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'taskID' => 'int(11) NOT NULL', 'taskName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -1056,7 +1054,7 @@ ], ], 'userAuths' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `userAuths` ( `uaID` int(11) NOT NULL AUTO_INCREMENT, `uaUserID` int(11) NOT NULL, `uaExpireDate` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `uaIsExpired` int(11) NOT NULL DEFAULT 0, `uaSelectorHash` varchar(255) NOT NULL DEFAULT \'\', `uaPasswordHash` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`uaID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `userAuths` ( `uaID` int(11) NOT NULL AUTO_INCREMENT, `uaUserID` int(11) NOT NULL, `uaExpireDate` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(), `uaIsExpired` int(11) NOT NULL DEFAULT 0, `uaSelectorHash` varchar(255) NOT NULL DEFAULT \'\', `uaPasswordHash` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`uaID`), KEY `fk_userAuths_uaUserID` (`uaUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'uaID' => 'int(11) NOT NULL', 'uaUserID' => 'int(11) NOT NULL', @@ -1074,7 +1072,7 @@ ], ], 'userGroupMembers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `userGroupMembers` ( `ugmID` int(11) NOT NULL AUTO_INCREMENT, `ugmName` varchar(60) NOT NULL DEFAULT \'\', `ugmGroupID` int(11) NOT NULL, `ugmUserID` int(11) NOT NULL, PRIMARY KEY (`ugmID`), UNIQUE KEY `ugmGroupUser` (`ugmGroupID`,`ugmUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `userGroupMembers` ( `ugmID` int(11) NOT NULL AUTO_INCREMENT, `ugmName` varchar(60) NOT NULL DEFAULT \'\', `ugmGroupID` int(11) NOT NULL, `ugmUserID` int(11) NOT NULL, PRIMARY KEY (`ugmID`), UNIQUE KEY `ugmGroupUser` (`ugmGroupID`,`ugmUserID`), KEY `fk_userGroupMembers_ugmUserID` (`ugmUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ugmID' => 'int(11) NOT NULL', 'ugmName' => 'varchar(60) NOT NULL DEFAULT \'\'', diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 5eed0a5674..356f6046c5 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10129,7 +10129,6 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 5878d1020e..24ac8b60f2 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10138,7 +10138,6 @@ msgstr "Printer already exists" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index e44b7eb0ec..8d20e8614e 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10298,7 +10298,6 @@ msgstr "Impresora ya existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7a2f833b7d..4a39694809 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10130,7 +10130,6 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 6df8ec8ace..4bb588765a 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10123,7 +10123,6 @@ msgstr "Imprimante existe déjà" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index ce1b066fb6..d996cc97a4 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9845,7 +9845,6 @@ msgstr "Questo host esiste già" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index f641eebb7d..a5f2510403 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9795,7 +9795,6 @@ msgstr "このホストは既に存在します" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index c961b2b1c9..85c4000271 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8678,7 +8678,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 7e61926da0..bdc6f22a62 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10125,7 +10125,6 @@ msgstr "Impressora já existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index e0cb5e6f36..2b24817b1e 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10125,7 +10125,6 @@ msgstr "打印机已经存在" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 0c5228bac01805eeae749830e6921a99acedc917 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Thu, 3 Sep 2026 18:43:12 +0000 Subject: [PATCH 018/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 356f6046c5..5eed0a5674 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10129,6 +10129,7 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 24ac8b60f2..5878d1020e 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10138,6 +10138,7 @@ msgstr "Printer already exists" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 8d20e8614e..e44b7eb0ec 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10298,6 +10298,7 @@ msgstr "Impresora ya existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 4a39694809..7a2f833b7d 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10130,6 +10130,7 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 4bb588765a..6df8ec8ace 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10123,6 +10123,7 @@ msgstr "Imprimante existe déjà" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d996cc97a4..ce1b066fb6 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -9845,6 +9845,7 @@ msgstr "Questo host esiste già" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index a5f2510403..f641eebb7d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -9795,6 +9795,7 @@ msgstr "このホストは既に存在します" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 85c4000271..c961b2b1c9 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8678,6 +8678,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index bdc6f22a62..7e61926da0 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10125,6 +10125,7 @@ msgstr "Impressora já existe" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 2b24817b1e..e0cb5e6f36 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10125,6 +10125,7 @@ msgstr "打印机已经存在" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 5bf9f243807cd4064fa741423d102b1bedd848c1 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 13:46:14 -0500 Subject: [PATCH 019/117] Snapin form: say the default return codes are Windows codes Linux and macOS truncate an exit status to 8 bits, so 3010 and 1618 cannot be returned there; the help text now says to list the code the program can actually return. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- .../languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/en_US.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 2 +- packages/web/management/languages/messages.pot | 2 +- .../languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 2 +- .../languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 2 +- packages/web/src/Pages/SnapinManagement.php | 8 ++++++-- 11 files changed, 16 insertions(+), 12 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 5eed0a5674..4a4e5fe76e 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -6815,7 +6815,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Eine oder mehrere MACs sind mit einem Host verknüpft" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 5878d1020e..2f44d8ba16 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -6826,7 +6826,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Error, Is an image associated with this host" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index e44b7eb0ec..bc2d66e4b9 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -6935,7 +6935,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Error, es una imagen asociada con este anfitrión" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 7a2f833b7d..2486e2d773 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -6816,7 +6816,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Eine oder mehrere MACs sind mit einem Host verknüpft" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 6df8ec8ace..b5b421273f 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -6813,7 +6813,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Erreur, est une image associée à cet hôte" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index ce1b066fb6..5b89f5bcaf 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -6628,7 +6628,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Uno o più MAC sono associati a questo host" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index f641eebb7d..ff031bcab5 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -6597,7 +6597,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "1 つ以上の MAC アドレスがホストに関連付けられています" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index c961b2b1c9..77340fa599 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -5838,7 +5838,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 7e61926da0..b500a156c5 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -6813,7 +6813,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "Erro, é uma imagem associada com este anfitrião" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index e0cb5e6f36..ec7516ab26 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -6813,7 +6813,7 @@ msgstr "" msgid "One or more macs are associated with a host" msgstr "错误,与此主机关联的图像" -msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed." +msgid "One per line, code=class. Classes: success, reboot (installed, reboot to finish), retry (try again next check-in), failed. Empty uses the defaults shown; any code not listed is failed. The defaults are Windows codes: Linux and macOS keep only the low 8 bits of an exit status, so list the code the program can return." msgstr "" msgid "One preference." diff --git a/packages/web/src/Pages/SnapinManagement.php b/packages/web/src/Pages/SnapinManagement.php index 54ffe7c1fb..7bf7275449 100644 --- a/packages/web/src/Pages/SnapinManagement.php +++ b/packages/web/src/Pages/SnapinManagement.php @@ -543,7 +543,9 @@ protected function _addFields() 'One per line, code=class. Classes: success, reboot ' . '(installed, reboot to finish), retry (try again next ' . 'check-in), failed. Empty uses the defaults shown; any ' - . 'code not listed is failed.' + . 'code not listed is failed. The defaults are Windows ' + . 'codes: Linux and macOS keep only the low 8 bits of an ' + . 'exit status, so list the code the program can return.' ) . '

', self::makeLabel( @@ -1075,7 +1077,9 @@ public function snapinGeneral() 'One per line, code=class. Classes: success, reboot ' . '(installed, reboot to finish), retry (try again next ' . 'check-in), failed. Empty uses the defaults shown; any ' - . 'code not listed is failed.' + . 'code not listed is failed. The defaults are Windows ' + . 'codes: Linux and macOS keep only the low 8 bits of an ' + . 'exit status, so list the code the program can return.' ) . '

', self::makeLabel( From 45eff016b3c16c275c0e9a7a728de2f8dec45caf Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Thu, 3 Sep 2026 14:45:04 -0500 Subject: [PATCH 020/117] Software management: desired-state packages for the agent, Chocolatey first Design 0003 (fog-agent docs/design/0003-software.md). A software entry is a package id plus a version policy (any, latest, pinned) and a state (present, absent), assigned to hosts directly and granted to groups, resolved per host in the snapin order (direct, then groups in group order, deduplicated). The agent's `software` capability converges the set and reports per entry; the server reads the exit code against the entry's return-code table (snapin defaults plus Chocolatey's 350 as reboot), refreshes one status row per host and entry, and answers the outcome. Nothing here is a task; snapins are untouched. Schema 418: software, softwareAssoc, groupSoftwareAssoc, softwareStatus, module 13 `software`, FOG_SOFTWARE_DRIFT_INTERVAL (six hours), and the stReturnDetails default 417 left off. Manifest entries added by hand in the generator's shape until a migrated database can regenerate it. UI: Software node (list, add, edit with General, Hosts and Status tabs), Software tab on host and group edit with run order, Software Status tab on the host, Software Report. Route /agent/v1/software/{id}/result. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01111oNkpZ7ZAXWZDVGmM4Yh --- bin/psr4-scan.php | 1 + ...l-integrity-is-declared-in-the-database.md | 2 +- docs/development/foreign-keys.md | 2 +- packages/web/commons/schema-constraints.php | 9 + packages/web/commons/schema-expected.php | 58 +- packages/web/commons/schema.php | 86 ++ packages/web/commons/text.php | 1 + .../management/js/fog/group/fog.group.edit.js | 114 +++ .../management/js/fog/host/fog.host.edit.js | 159 ++++ .../js/fog/report/fog.report.file.js | 34 + .../js/fog/software/fog.software.add.js | 17 + .../js/fog/software/fog.software.edit.js | 75 ++ .../js/fog/software/fog.software.list.js | 116 +++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 210 ++++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 210 ++++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 209 ++++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 210 ++++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 209 ++++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 209 ++++- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 218 ++++- .../web/management/languages/messages.pot | 175 +++- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 210 ++++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 210 ++++- packages/web/src/Agent/Snapins.php | 22 +- packages/web/src/Agent/SoftwareSet.php | 215 +++++ packages/web/src/Agent/State.php | 9 +- packages/web/src/Assign/Resolver.php | 62 ++ packages/web/src/Auth/Authorization.php | 8 + packages/web/src/Base/FOGBase.php | 1 + packages/web/src/Base/FOGPage.php | 8 + packages/web/src/Base/FOGPagePost.php | 24 +- packages/web/src/Base/System.php | 2 +- packages/web/src/Items/Group.php | 131 +++ .../src/Items/GroupSoftwareAssociation.php | 55 ++ packages/web/src/Items/Host.php | 143 +++- packages/web/src/Items/Software.php | 157 ++++ .../web/src/Items/SoftwareAssociation.php | 73 ++ packages/web/src/Items/SoftwareStatus.php | 59 ++ .../GroupSoftwareAssociationManager.php | 29 + .../Managers/SoftwareAssociationManager.php | 29 + packages/web/src/Managers/SoftwareManager.php | 35 + .../src/Managers/SoftwareStatusManager.php | 29 + packages/web/src/Pages/GroupManagement.php | 140 +++ packages/web/src/Pages/HostManagement.php | 279 ++++++ packages/web/src/Pages/ReportManagement.php | 1 + packages/web/src/Pages/SoftwareManagement.php | 797 ++++++++++++++++++ packages/web/src/Reports/Software_Report.php | 229 +++++ packages/web/src/Router/OpenAPI.php | 43 + packages/web/src/Router/Route.php | 45 + phpstan-baseline.neon | 2 +- tests/fixtures/route-cascade-contract.txt | 6 + tests/fixtures/route-column-contract.txt | 41 + tests/foreign-key-map.test.php | 9 + tests/group-grants-column.test.php | 6 +- tests/menu-labels-are-whole-phrases.test.php | 1 + 55 files changed, 5361 insertions(+), 73 deletions(-) create mode 100644 packages/web/management/js/fog/software/fog.software.add.js create mode 100644 packages/web/management/js/fog/software/fog.software.edit.js create mode 100644 packages/web/management/js/fog/software/fog.software.list.js create mode 100644 packages/web/src/Agent/SoftwareSet.php create mode 100644 packages/web/src/Items/GroupSoftwareAssociation.php create mode 100644 packages/web/src/Items/Software.php create mode 100644 packages/web/src/Items/SoftwareAssociation.php create mode 100644 packages/web/src/Items/SoftwareStatus.php create mode 100644 packages/web/src/Managers/GroupSoftwareAssociationManager.php create mode 100644 packages/web/src/Managers/SoftwareAssociationManager.php create mode 100644 packages/web/src/Managers/SoftwareManager.php create mode 100644 packages/web/src/Managers/SoftwareStatusManager.php create mode 100644 packages/web/src/Pages/SoftwareManagement.php create mode 100644 packages/web/src/Reports/Software_Report.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 4cf2551520..050376975c 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -210,6 +210,7 @@ 'Token' => 'Agent', 'State' => 'Agent', 'Snapins' => 'Agent', + 'SoftwareSet' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', 'TaskError' => 'TaskHandling', diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index 1a5e293b7d..cb2e7fdd09 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**109 of the map's 124 relationships are declared.** The other 15 are not +**115 of the map's 130 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index c092333294..158bd25a2e 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 124 relationships live in them. +the survey and 25 of the map's 130 relationships live in them. ### Direction is the whole rule diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index ad4bbf6127..f3552c4e8b 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -299,6 +299,15 @@ ['child' => 'groupSnapinAssoc', 'column' => 'gsaSnapinID', 'parent' => 'snapins', 'pcolumn' => 'sID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], ['child' => 'groupPrinterAssoc', 'column' => 'gpaGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], ['child' => 'groupPrinterAssoc', 'column' => 'gpaPrinterID', 'parent' => 'printers', 'pcolumn' => 'pID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 9], + // Group 13 -- fog-agent software (design 0003, schema 418). An + // assignment or a status row is meaningless without both its host (or + // group) and its software entry. + ['child' => 'softwareAssoc', 'column' => 'swaHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareAssoc', 'column' => 'swaSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'groupSoftwareAssoc', 'column' => 'gswaGroupID', 'parent' => 'groups', 'pcolumn' => 'groupID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'groupSoftwareAssoc', 'column' => 'gswaSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareStatus', 'column' => 'sstHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], + ['child' => 'softwareStatus', 'column' => 'sstSoftwareID', 'parent' => 'software', 'pcolumn' => 'swID', 'class' => 'junction', 'action' => 'CASCADE', 'enabled' => true, 'group' => 13], // ADR 0038 decision 3, revised. Group 10, created empty by step 407 so // there is nothing to sweep before the flip. // diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index d84b6a79c1..3414ee03cd 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -307,6 +307,15 @@ 'gsaSequence' => 'int(11) NOT NULL DEFAULT 0', ], ], + 'groupSoftwareAssoc' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `groupSoftwareAssoc` ( `gswaID` int(11) NOT NULL AUTO_INCREMENT, `gswaGroupID` int(11) NOT NULL, `gswaSoftwareID` int(11) NOT NULL, `gswaSequence` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`gswaID`), UNIQUE KEY `gswaGroupSoftware` (`gswaGroupID`,`gswaSoftwareID`), KEY `gswaSoftwareID` (`gswaSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'gswaID' => 'int(11) NOT NULL', + 'gswaGroupID' => 'int(11) NOT NULL', + 'gswaSoftwareID' => 'int(11) NOT NULL', + 'gswaSequence' => 'int(11) NOT NULL DEFAULT 0', + ], + ], 'history' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `history` ( `hID` int(11) NOT NULL AUTO_INCREMENT, `hText` text NOT NULL, `hUser` varchar(200) NOT NULL DEFAULT \'\', `hTime` timestamp NOT NULL DEFAULT current_timestamp(), `hIP` varchar(50) NOT NULL DEFAULT \'\', `hType` varchar(16) NOT NULL DEFAULT \'\', `hSubjectType` varchar(64) NOT NULL DEFAULT \'\', `hSubjectID` int(11) DEFAULT NULL, `hSubjectLabel` varchar(200) NOT NULL DEFAULT \'\', PRIMARY KEY (`hID`), KEY `hTime` (`hTime`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -588,7 +597,7 @@ ], ], 'nfsGroupMembers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `nfsGroupMembers` ( `ngmID` int(11) NOT NULL AUTO_INCREMENT, `ngmMemberName` varchar(250) NOT NULL DEFAULT \'\', `ngmMemberDescription` longtext NOT NULL DEFAULT \'\', `ngmIsMasterNode` char(1) NOT NULL DEFAULT \'\', `ngmGroupID` int(11) NOT NULL, `ngmRootPath` longtext NOT NULL, `ngmSSLPath` longtext NOT NULL DEFAULT \'\', `ngmFTPPath` longtext NOT NULL, `ngmMaxBitrate` varchar(25) DEFAULT NULL, `ngmHelloInterval` varchar(8) DEFAULT NULL, `ngmGraphColor` varchar(6) DEFAULT NULL, `ngmSnapinPath` longtext NOT NULL DEFAULT \'\', `ngmIsEnabled` char(1) NOT NULL DEFAULT \'\', `ngmHostname` varchar(250) NOT NULL, `ngmMaxClients` int(11) NOT NULL DEFAULT 0, `ngmBandwidthLimit` int(20) NOT NULL DEFAULT 0, `ngmUser` varchar(250) NOT NULL, `ngmPass` varchar(250) NOT NULL, `ngmKey` varchar(250) NOT NULL DEFAULT \'\', `ngmInterface` varchar(25) NOT NULL DEFAULT \'enp58s0u2u4\', `ngmGraphEnabled` tinyint(1) NOT NULL DEFAULT 1, `ngmWebroot` longtext NOT NULL DEFAULT \'\', PRIMARY KEY (`ngmID`), UNIQUE KEY `ngmMemberName` (`ngmMemberName`), UNIQUE KEY `ngmMemberName_2` (`ngmMemberName`), KEY `new_index` (`ngmMemberName`), KEY `new_index2` (`ngmIsMasterNode`), KEY `new_index3` (`ngmGroupID`), KEY `new_index4` (`ngmIsEnabled`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `nfsGroupMembers` ( `ngmID` int(11) NOT NULL AUTO_INCREMENT, `ngmMemberName` varchar(250) NOT NULL DEFAULT \'\', `ngmMemberDescription` longtext NOT NULL DEFAULT \'\', `ngmIsMasterNode` char(1) NOT NULL DEFAULT \'\', `ngmGroupID` int(11) NOT NULL, `ngmRootPath` longtext NOT NULL, `ngmSSLPath` longtext NOT NULL DEFAULT \'\', `ngmFTPPath` longtext NOT NULL, `ngmMaxBitrate` varchar(25) DEFAULT NULL, `ngmHelloInterval` varchar(8) DEFAULT NULL, `ngmGraphColor` varchar(6) DEFAULT NULL, `ngmSnapinPath` longtext NOT NULL DEFAULT \'\', `ngmIsEnabled` char(1) NOT NULL DEFAULT \'\', `ngmHostname` varchar(250) NOT NULL, `ngmMaxClients` int(11) NOT NULL DEFAULT 0, `ngmBandwidthLimit` int(20) NOT NULL DEFAULT 0, `ngmUser` varchar(250) NOT NULL, `ngmPass` varchar(250) NOT NULL, `ngmKey` varchar(250) NOT NULL DEFAULT \'\', `ngmInterface` varchar(25) NOT NULL DEFAULT \'\', `ngmGraphEnabled` tinyint(1) NOT NULL DEFAULT 1, `ngmWebroot` longtext NOT NULL DEFAULT \'\', PRIMARY KEY (`ngmID`), UNIQUE KEY `ngmMemberName` (`ngmMemberName`), UNIQUE KEY `ngmMemberName_2` (`ngmMemberName`), KEY `new_index` (`ngmMemberName`), KEY `new_index2` (`ngmIsMasterNode`), KEY `new_index3` (`ngmGroupID`), KEY `new_index4` (`ngmIsEnabled`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'ngmID' => 'int(11) NOT NULL', 'ngmMemberName' => 'varchar(250) NOT NULL DEFAULT \'\'', @@ -609,7 +618,7 @@ 'ngmUser' => 'varchar(250) NOT NULL', 'ngmPass' => 'varchar(250) NOT NULL', 'ngmKey' => 'varchar(250) NOT NULL DEFAULT \'\'', - 'ngmInterface' => 'varchar(25) NOT NULL DEFAULT \'enp58s0u2u4\'', + 'ngmInterface' => 'varchar(25) NOT NULL DEFAULT \'\'', 'ngmGraphEnabled' => 'tinyint(1) NOT NULL DEFAULT 1', 'ngmWebroot' => 'longtext NOT NULL DEFAULT \'\'', ], @@ -946,7 +955,7 @@ ], ], 'snapinTasks' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stStatus` varchar(16) NOT NULL DEFAULT \'\', `stReturnDetails` text NOT NULL, PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `snapinTasks` ( `stID` int(11) NOT NULL AUTO_INCREMENT, `stJobID` int(11) NOT NULL, `stState` int(11) NOT NULL DEFAULT 0, `stCheckinDate` timestamp NOT NULL DEFAULT current_timestamp(), `stCompleteDate` datetime DEFAULT NULL, `stSnapinID` int(11) NOT NULL, `stSequence` int(11) NOT NULL DEFAULT 0, `stReturnCode` int(11) NOT NULL DEFAULT 0, `stStatus` varchar(16) NOT NULL DEFAULT \'\', `stReturnDetails` text NOT NULL DEFAULT \'\', PRIMARY KEY (`stID`), UNIQUE KEY `stJobID` (`stJobID`,`stSnapinID`), KEY `new_index` (`stJobID`), KEY `new_index1` (`stState`), KEY `new_index2` (`stSnapinID`), KEY `idx_stCheckinDate` (`stCheckinDate`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'stID' => 'int(11) NOT NULL', 'stJobID' => 'int(11) NOT NULL', @@ -957,7 +966,48 @@ 'stSequence' => 'int(11) NOT NULL DEFAULT 0', 'stReturnCode' => 'int(11) NOT NULL DEFAULT 0', 'stStatus' => 'varchar(16) NOT NULL DEFAULT \'\'', - 'stReturnDetails' => 'text NOT NULL', + 'stReturnDetails' => 'text NOT NULL DEFAULT \'\'', + ], + ], + 'software' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `software` ( `swID` int(11) NOT NULL AUTO_INCREMENT, `swName` varchar(200) NOT NULL, `swDesc` longtext NOT NULL DEFAULT \'\', `swBackend` varchar(16) NOT NULL DEFAULT \'choco\', `swPackage` varchar(255) NOT NULL, `swVersion` varchar(64) NOT NULL DEFAULT \'\', `swState` varchar(8) NOT NULL DEFAULT \'present\', `swSource` varchar(255) NOT NULL DEFAULT \'\', `swArgs` varchar(255) NOT NULL DEFAULT \'\', `swTimeout` int(11) NOT NULL DEFAULT 900, `swReturnCodes` text DEFAULT NULL, `swEnabled` tinyint(1) NOT NULL DEFAULT 1, `swCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `swCreator` varchar(50) NOT NULL DEFAULT \'\', PRIMARY KEY (`swID`), UNIQUE KEY `swName` (`swName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'swID' => 'int(11) NOT NULL', + 'swName' => 'varchar(200) NOT NULL', + 'swDesc' => 'longtext NOT NULL DEFAULT \'\'', + 'swBackend' => 'varchar(16) NOT NULL DEFAULT \'choco\'', + 'swPackage' => 'varchar(255) NOT NULL', + 'swVersion' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'swState' => 'varchar(8) NOT NULL DEFAULT \'present\'', + 'swSource' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'swArgs' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'swTimeout' => 'int(11) NOT NULL DEFAULT 900', + 'swReturnCodes' => 'text DEFAULT NULL', + 'swEnabled' => 'tinyint(1) NOT NULL DEFAULT 1', + 'swCreateDate' => 'timestamp NOT NULL DEFAULT current_timestamp()', + 'swCreator' => 'varchar(50) NOT NULL DEFAULT \'\'', + ], + ], + 'softwareAssoc' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `softwareAssoc` ( `swaID` int(11) NOT NULL AUTO_INCREMENT, `swaHostID` int(11) NOT NULL, `swaSoftwareID` int(11) NOT NULL, `swaSequence` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`swaID`), UNIQUE KEY `swaHostSoftware` (`swaHostID`,`swaSoftwareID`), KEY `swaSoftwareID` (`swaSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'swaID' => 'int(11) NOT NULL', + 'swaHostID' => 'int(11) NOT NULL', + 'swaSoftwareID' => 'int(11) NOT NULL', + 'swaSequence' => 'int(11) NOT NULL DEFAULT 0', + ], + ], + 'softwareStatus' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `softwareStatus` ( `sstID` int(11) NOT NULL AUTO_INCREMENT, `sstHostID` int(11) NOT NULL, `sstSoftwareID` int(11) NOT NULL, `sstInstalledVersion` varchar(64) NOT NULL DEFAULT \'\', `sstStatus` varchar(16) NOT NULL DEFAULT \'\', `sstReturnCode` int(11) NOT NULL DEFAULT 0, `sstDetails` text NOT NULL DEFAULT \'\', `sstChecked` datetime DEFAULT NULL, PRIMARY KEY (`sstID`), UNIQUE KEY `sstHostSoftware` (`sstHostID`,`sstSoftwareID`), KEY `sstSoftwareID` (`sstSoftwareID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'sstID' => 'int(11) NOT NULL', + 'sstHostID' => 'int(11) NOT NULL', + 'sstSoftwareID' => 'int(11) NOT NULL', + 'sstInstalledVersion' => 'varchar(64) NOT NULL DEFAULT \'\'', + 'sstStatus' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'sstReturnCode' => 'int(11) NOT NULL DEFAULT 0', + 'sstDetails' => 'text NOT NULL DEFAULT \'\'', + 'sstChecked' => 'datetime DEFAULT NULL', ], ], 'storageEpoch' => [ diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 9954b197ed..55298e4cae 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -10879,3 +10879,89 @@ function () { // reports the last 4 KB of output. "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL", ]; +// 418 +$this->schema[] = [ + // fog-agent software management (design 0003). Software is desired + // state, not a task: a package id plus a version policy, held on the + // host by a package manager (Chocolatey first) and reported back with + // the version the host actually has. Snapins stay as they are. + "CREATE TABLE IF NOT EXISTS `software` ( " + . "`swID` int(11) NOT NULL AUTO_INCREMENT, " + . "`swName` varchar(200) NOT NULL, " + . "`swDesc` longtext NOT NULL DEFAULT '', " + // Which package manager knows the package: choco now, others later + // behind the same interface. + . "`swBackend` varchar(16) NOT NULL DEFAULT 'choco', " + . "`swPackage` varchar(255) NOT NULL, " + // '' any version, 'latest' tracks the source, else an exact pin. + . "`swVersion` varchar(64) NOT NULL DEFAULT '', " + . "`swState` varchar(8) NOT NULL DEFAULT 'present', " + . "`swSource` varchar(255) NOT NULL DEFAULT '', " + . "`swArgs` varchar(255) NOT NULL DEFAULT '', " + . "`swTimeout` int(11) NOT NULL DEFAULT 900, " + // The same code=class table snapins carry (schema 417). + . "`swReturnCodes` text NULL, " + . "`swEnabled` tinyint(1) NOT NULL DEFAULT 1, " + . "`swCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), " + . "`swCreator` varchar(50) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`swID`), " + . "UNIQUE KEY `swName` (`swName`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Host-direct assignment, ordered like snapinAssoc. + "CREATE TABLE IF NOT EXISTS `softwareAssoc` ( " + . "`swaID` int(11) NOT NULL AUTO_INCREMENT, " + . "`swaHostID` int(11) NOT NULL, " + . "`swaSoftwareID` int(11) NOT NULL, " + . "`swaSequence` int(11) NOT NULL DEFAULT 0, " + . "PRIMARY KEY (`swaID`), " + . "UNIQUE KEY `swaHostSoftware` (`swaHostID`,`swaSoftwareID`), " + . "KEY `swaSoftwareID` (`swaSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Group grant, the ADR 0038 shape: a fact about the group, resolved + // per host after its direct assignments and deduplicated. + "CREATE TABLE IF NOT EXISTS `groupSoftwareAssoc` ( " + . "`gswaID` int(11) NOT NULL AUTO_INCREMENT, " + . "`gswaGroupID` int(11) NOT NULL, " + . "`gswaSoftwareID` int(11) NOT NULL, " + . "`gswaSequence` int(11) NOT NULL DEFAULT 0, " + . "PRIMARY KEY (`gswaID`), " + . "UNIQUE KEY `gswaGroupSoftware` (`gswaGroupID`,`gswaSoftwareID`), " + . "KEY `gswaSoftwareID` (`gswaSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // What each host last reported per entry: one row, refreshed in + // place, so the host's Software tab is a current picture rather than + // a history. + "CREATE TABLE IF NOT EXISTS `softwareStatus` ( " + . "`sstID` int(11) NOT NULL AUTO_INCREMENT, " + . "`sstHostID` int(11) NOT NULL, " + . "`sstSoftwareID` int(11) NOT NULL, " + . "`sstInstalledVersion` varchar(64) NOT NULL DEFAULT '', " + // converged, installed, upgraded, removed, failed, retry, reboot, + // timeout, cannot_run. + . "`sstStatus` varchar(16) NOT NULL DEFAULT '', " + . "`sstReturnCode` int(11) NOT NULL DEFAULT 0, " + . "`sstDetails` text NOT NULL DEFAULT '', " + . "`sstChecked` datetime DEFAULT NULL, " + . "PRIMARY KEY (`sstID`), " + . "UNIQUE KEY `sstHostSoftware` (`sstHostID`,`sstSoftwareID`), " + . "KEY `sstSoftwareID` (`sstSoftwareID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // The module row that lets an admin turn the capability off per host + // or group like every other one. Id 13 follows the seed in step 34. + // 417 widened stReturnDetails without a default, which a strict server + // refuses on an INSERT that omits it (the legacy client's does). + "ALTER TABLE `snapinTasks` MODIFY COLUMN `stReturnDetails` text NOT NULL DEFAULT ''", + "INSERT IGNORE INTO `modules` (`id`, `name`, `short_name`, `description`) " + . "VALUES (13,'Software','software','This setting will enable or disable " + . "the software management module on this specific host. If the module " + . "is globally disabled, this setting is ignored.')", + // How often a host re-checks its software set when nothing changed on + // the server. Six hours: often enough to catch a removed package the + // same working day, rare enough that choco is not a poll-loop cost. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_SOFTWARE_DRIFT_INTERVAL','Seconds between a host''s checks of " + . "its software set when the set has not changed. The check runs " + . "choco against every entry, so keep it hours, not minutes.'," + . "'21600','FOG Client')", +]; diff --git a/packages/web/commons/text.php b/packages/web/commons/text.php index 81ee17b60d..2375712306 100644 --- a/packages/web/commons/text.php +++ b/packages/web/commons/text.php @@ -33,6 +33,7 @@ $foglang['Storage'] = _('Storage'); $foglang['Snapin'] = _('Snapin'); $foglang['Snapins'] = _('Snapins'); +$foglang['Software'] = _('Software'); $foglang['Remove'] = _('Remove'); $foglang['Removed'] = _('Removed'); $foglang['Enabled'] = _('Enabled'); diff --git a/packages/web/management/js/fog/group/fog.group.edit.js b/packages/web/management/js/fog/group/fog.group.edit.js index 88421e4350..3a46d4e529 100644 --- a/packages/web/management/js/fog/group/fog.group.edit.js +++ b/packages/web/management/js/fog/group/fog.group.edit.js @@ -395,6 +395,119 @@ loadGroupSnapinOrder(); + // --------------------------------------------------------------- + // SOFTWARE TAB + // Association goes through Group::addSoftware(), which writes one grant + // row on the group, mirroring the snapin tab above. + var groupSoftwareTable = $.registerAssociationTab({ + slug: 'group-software', + item: 'software', + sub: 'getSoftwareList', + afterCommit: loadGroupSoftwareOrder + }); + $.registerCreateAndAssociate('group-software', groupSoftwareTable); + + // --------------------------------------------------------------- + // GROUP SOFTWARE ORDER (the software this group grants) + var groupSoftwareOrderList = $('#group-software-order-list'), + groupSoftwareOrderSaveBtn = $('#group-software-order-save'); + + function updateGroupSoftwareOrderPositions() { + groupSoftwareOrderList.children('li').each(function(i) { + $(this).find('.software-order-pos').text((i + 1) + '. '); + }); + } + + function renderGroupSoftwareOrder(items) { + groupSoftwareOrderList.empty(); + if (!items || items.length === 0) { + groupSoftwareOrderList.append( + $('
  • ', {'class': 'list-group-item text-muted'}) + .text('No software is granted by this group.') + ); + groupSoftwareOrderSaveBtn.prop('disabled', true); + return; + } + groupSoftwareOrderSaveBtn.prop('disabled', false); + $.each(items, function(i, item) { + var controls = $('', {'class': 'float-end'}) + .append( + $('
  • '; @@ -3979,7 +3986,7 @@ public function apitokenlistPost() $uid = (int)self::$FOGUser->get('id'); $rows = []; foreach ( - self::getClass('APITokenManager')->visibleTo($uid) as $token + (new APITokenManager())->visibleTo($uid) as $token ) { $rows[] = [ 'id' => $token['id'], @@ -4043,7 +4050,7 @@ public function apitokendeletePost() // null owner: spanning users is this pane's whole job. The // per-user tab passes its own id here instead. - $deleted = self::getClass('APITokenManager')->revokeMany( + $deleted = (new APITokenManager())->revokeMany( array_map('intval', (array)($_POST['remitems'] ?? [])), (int)self::$FOGUser->get('id') ); @@ -4086,7 +4093,7 @@ public function apitokenenablePost() } $enabled = (int)filter_input(INPUT_POST, 'enabled') === 1; - $changed = self::getClass('APITokenManager')->setEnabledMany( + $changed = (new APITokenManager())->setEnabledMany( array_map('intval', (array)($_POST['remitems'] ?? [])), $enabled, (int)self::$FOGUser->get('id') @@ -4154,8 +4161,8 @@ public function issueAPITokenForPost() // could mint a working credential for an account they are not // allowed to see, which is a privilege escalation dressed up as a // convenience feature. - $user = self::getClass('User', $forUserID); - $inScope = self::getClass('APITokenManager') + $user = new User($forUserID); + $inScope = (new APITokenManager()) ->userInScope($forUserID, (int)self::$FOGUser->get('id')); if (!$user->isValid() || !$inScope) { @@ -4338,7 +4345,7 @@ public function maclistPost() list( $first_id, $affected_rows - ) = self::getClass('OUIManager') + ) = (new OUIManager()) ->insertBatch( [ 'prefix', @@ -4378,10 +4385,7 @@ public function maclistPost() public function getOSID() { $imageid = (int)filter_input(INPUT_POST, 'image_id'); - $osname = self::getClass( - 'Image', - $imageid - )->getOS()->get('name'); + $osname = (new Image($imageid))->getOS()->get('name'); $this->jsonSend(HTTPResponseCodes::HTTP_SUCCESS, json_encode($osname ? $osname : _('No Image specified'))); } /** @@ -4847,7 +4851,7 @@ private static function _renderSettingInput( . $row['settingKey'] . '">'; foreach ((array)$tzIDs as $i => &$tz) { - $current_tz = self::getClass('DateTimeZone', $tz); + $current_tz = new \DateTimeZone($tz); $offset = $current_tz->getOffset($dt); $transition = $current_tz->getTransitions( $dt->getTimestamp(), @@ -5350,7 +5354,7 @@ public function settingsPost() unset($Setting); } if (count($items) > 0) { - $SettingMan = self::getClass('SettingManager'); + $SettingMan = new SettingManager(); /* * settingDesc and settingCategory are named even though this * saver never changes them, and the values are the ones just @@ -5595,7 +5599,7 @@ private function _renderSettings() . 'A hard refresh (Ctrl+F5, or Cmd+Shift+R) may be required.' ); - $table = self::getClass('SettingManager')->getTable(); + $table = (new SettingManager())->getTable(); $sql = 'SELECT `settingID`, `settingKey`, `settingDesc`, ' . '`settingValue`, `settingCategory` FROM `' . $table . '` ' . 'ORDER BY `settingCategory` ASC, `settingKey` ASC'; @@ -5850,7 +5854,7 @@ public function configPost() self::checkAuthAndCSRF(); header('Content-type: application/json'); self::$HookManager->processEvent('IMPORT_POST'); - $Schema = self::getClass('Schema'); + $Schema = new Schema(); $serverFault = false; try { if (isset($_POST['toExport'])) { @@ -5884,7 +5888,7 @@ public function configPost() } chmod($tmpfile, 0600); $data = ''; - self::getClass('Mysqldump')->start($tmpfile); + (new Mysqldump())->start($tmpfile); if (!file_exists($tmpfile) || !is_readable($tmpfile)) { throw new \Exception(_('Could not read file from tmp folder.')); } @@ -5934,7 +5938,7 @@ public function configPost() // Now import try { - $result = self::getClass('Schema')->importdb($dest); + $result = (new Schema())->importdb($dest); } finally { @unlink($dest); // cleanup regardless } @@ -5983,7 +5987,7 @@ public function getSettingsList() $meta = $this->_settingsMeta(); $needstobecheckbox = $meta['checkbox']; $needstobenumeric = $meta['numeric']; - $settingMan = self::getClass('SettingManager'); + $settingMan = new SettingManager(); $table = $settingMan->getTable(); $dbcolumns = $settingMan->getColumns(); $sqlStr = $settingMan->getQueryStr(); diff --git a/packages/web/src/Pages/GroupManagement.php b/packages/web/src/Pages/GroupManagement.php index 4ba34f17f4..ceef9e3550 100644 --- a/packages/web/src/Pages/GroupManagement.php +++ b/packages/web/src/Pages/GroupManagement.php @@ -17,8 +17,13 @@ use FOG\Auth\Authorization; use FOG\Base\FOGPage; +use FOG\Items\Group; +use FOG\Items\GroupPowerManagement; +use FOG\Items\ScheduledTask; use FOG\Items\Setting; use FOG\Items\TaskType; +use FOG\Managers\GroupManager; +use FOG\Managers\PowerManagementManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; use FOG\Util\FOGCron; @@ -252,14 +257,14 @@ function (&$serverFault) { 0, (int)filter_input(INPUT_POST, 'order') ); - $exists = self::getClass('GroupManager') + $exists = (new GroupManager()) ->exists($group); if ($exists) { throw new \Exception( _('A group already exists with this name!') ); } - $Group = self::getClass('Group') + $Group = (new Group()) ->set('name', $group) ->set('description', $description) ->set('order', $order) @@ -974,7 +979,7 @@ public function groupPowermanagementPost() ]; } if (count($items) > 0) { - self::getClass('PowerManagementManager') + (new PowerManagementManager()) ->insertBatch( [ 'hostID', @@ -992,7 +997,7 @@ public function groupPowermanagementPost() return; } // ONE ROW, ABOUT THE GROUP. Not one per member. - self::getClass('GroupPowerManagement') + (new GroupPowerManagement()) ->set('groupID', $groupID) ->set('min', FOGCron::_sanitizeCronField($min)) ->set('hour', FOGCron::_sanitizeCronField($hour)) @@ -2401,7 +2406,7 @@ public function deploy() $type = 1; } - $TaskType = self::getClass('TaskType', $type); + $TaskType = new TaskType($type); $this->title = $TaskType->get('name') . ' ' @@ -2542,7 +2547,7 @@ public function deployPost() } // Task Type setup - $TaskType = self::getClass('TaskType', $type); + $TaskType = new TaskType($type); if (!$TaskType->isValid()) { throw new \Exception(_('Task Type is invalid')); } @@ -2638,7 +2643,7 @@ public function deployPost() $snapinAbortOnFailure ); } else { - $ScheduledTask = self::getClass('ScheduledTask') + $ScheduledTask = (new ScheduledTask()) ->set('taskTypeID', $type) ->set('name', $taskName) ->set('hostID', $this->obj->get('id')) diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 5116da06c1..5a8f8e0d82 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -26,8 +26,17 @@ use FOG\Items\Host; use FOG\Items\HostAutoLogout; use FOG\Items\MACAddress; +use FOG\Items\PowerManagement; +use FOG\Items\ScheduledTask; use FOG\Items\Setting; use FOG\Items\TaskType; +use FOG\Managers\ArchitectureManager; +use FOG\Managers\HostAutoLogoutManager; +use FOG\Managers\HostManager; +use FOG\Managers\HostScreenSettingManager; +use FOG\Managers\ImageManager; +use FOG\Managers\MACAddressAssociationManager; +use FOG\Managers\PowerManagementManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; use FOG\Util\FOGCron; @@ -79,7 +88,7 @@ public function __construct($name = '') && !$this->obj->get('task')->isValid() ) ) { - self::getClass('HostManager')->update( + (new HostManager())->update( ['id' => $this->obj->get('id')], '', [ @@ -329,7 +338,7 @@ public function pendingAjax() ); } if (isset($_POST['approvepending'])) { - self::getClass('HostManager')->update( + (new HostManager())->update( [ 'id' => $pending, 'pending' => 1 @@ -509,7 +518,7 @@ public function pendingMacsAjax() } if (isset($_POST['approvepending'])) { $errt = _('Approve MAC Fail'); - self::getClass('MACAddressAssociationManager')->update( + (new MACAddressAssociationManager())->update( [ 'id' => $pending, 'pending' => 1 @@ -1033,7 +1042,7 @@ public function add() $enforce = isset($_POST['enforce']) ?: self::getSetting( 'FOG_ENFORCE_HOST_CHANGES' ); - $imageSelector = self::getClass('ImageManager') + $imageSelector = (new ImageManager()) ->buildSelectBox($image, '', 'id'); $labelClass = 'col-sm-3 col-form-label'; @@ -1181,7 +1190,7 @@ public function add() [ 'fields' => &$fields, 'buttons' => &$buttons, - 'Host' => self::getClass('Host') + 'Host' => new Host() ] ); $rendered = self::formFields($fields); @@ -1201,7 +1210,7 @@ public function add() 'HOST_ADD_AD_FIELDS', [ 'fields' => &$fieldads, - 'Host' => self::getClass('Host') + 'Host' => new Host() ] ); $renderedad = self::formFields($fieldads); @@ -1249,7 +1258,7 @@ function () { 'HOST_ADD_AD_FIELDS', [ 'fields' => &$fieldads, - 'Host' => self::getClass('Host') + 'Host' => new Host() ] ); $renderedad = self::formFields($fieldads); @@ -1283,7 +1292,7 @@ protected function _addFields() $enforce = isset($_POST['enforce']) ?: self::getSetting( 'FOG_ENFORCE_HOST_CHANGES' ); - $imageSelector = self::getClass('ImageManager') + $imageSelector = (new ImageManager()) ->buildSelectBox($image, '', 'id'); $labelClass = 'col-sm-3 col-form-label'; @@ -1482,7 +1491,7 @@ function (&$serverFault) { (string)filter_input(INPUT_POST, 'efiBootTypeExit') ); - $exists = self::getClass('HostManager') + $exists = (new HostManager()) ->exists($host); if ($exists) { throw new \Exception( @@ -1493,7 +1502,7 @@ function (&$serverFault) { if (!$MAC->isValid()) { throw new \Exception(_('MAC Format is invalid')); } - self::getClass('HostManager')->getHostByMacAddresses( + (new HostManager())->getHostByMacAddresses( $MAC->__toString() ); if (self::$Host->isValid()) { @@ -1552,7 +1561,7 @@ public function hostGeneral() filter_input(INPUT_POST, 'image') ?: ($this->obj->get('imageID') ?: '') ); - $imageSelector = self::getClass('ImageManager') + $imageSelector = (new ImageManager()) ->buildSelectBox($image); // The architectures an admin may pick on a HOST, which is what // `architectures.archIsAccess` is for -- the same flag taskTypes uses @@ -1570,7 +1579,7 @@ public function hostGeneral() if (count($archIds) < 1) { $archIds = [0]; } - $archSelector = self::getClass('ArchitectureManager') + $archSelector = (new ArchitectureManager()) ->buildSelectBox($archID, 'archID', 'name', $archIds); // Either use the passed in or get the objects info. $host = ( @@ -2428,7 +2437,7 @@ public function hostMacaddressPost() if (!$mact->isValid()) { throw new \Exception(_('MAC Address is invalid!')); } - $mace = self::getClass('MACAddressAssociationManager') + $mace = (new MACAddressAssociationManager()) ->exists($mac, '', 'mac'); if ($mace) { throw new \Exception( @@ -2442,14 +2451,14 @@ public function hostMacaddressPost() INPUT_POST, 'primary' ); - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( ['hostID' => $this->obj->get('id')], '', ['primary' => 0] ); if ($primary) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $primary, @@ -2473,7 +2482,7 @@ public function hostMacaddressPost() $imageIgnore = $items['imageIgnore']; $clientIgnore = $items['clientIgnore']; $pending = $items['pending']; - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( ['hostID' => $this->obj->get('id')], '', @@ -2484,7 +2493,7 @@ public function hostMacaddressPost() ] ); if (count($imageIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $imageIgnore, @@ -2495,7 +2504,7 @@ public function hostMacaddressPost() ); } if (count($clientIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $clientIgnore, @@ -2506,7 +2515,7 @@ public function hostMacaddressPost() ); } if (count($pending ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $pending, @@ -2575,7 +2584,7 @@ public function hostMacaddressPost() ); $imageIgnore = $items['imageIgnore']; if (count($imageIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $imageIgnore, @@ -2593,7 +2602,7 @@ public function hostMacaddressPost() ); $imageIgnore = $items['imageIgnore']; if (count($imageIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $imageIgnore, @@ -2611,7 +2620,7 @@ public function hostMacaddressPost() ); $clientIgnore = $items['clientIgnore']; if (count($clientIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $clientIgnore, @@ -2629,7 +2638,7 @@ public function hostMacaddressPost() ); $clientIgnore = $items['clientIgnore']; if (count($clientIgnore ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $clientIgnore, @@ -2647,7 +2656,7 @@ public function hostMacaddressPost() ); $pending = $items['pending']; if (count($pending ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $pending, @@ -2665,7 +2674,7 @@ public function hostMacaddressPost() ); $pending = $items['pending']; if (count($pending ?: []) > 0) { - self::getClass('MACAddressAssociationManager') + (new MACAddressAssociationManager()) ->update( [ 'id' => $pending, @@ -3553,7 +3562,7 @@ public function hostPowermanagementPost() $dom = FOGCron::_sanitizeCronField($dom); $month = FOGCron::_sanitizeCronField($month); $dow = FOGCron::_sanitizeCronField($dow); - self::getClass('PowerManagement') + (new PowerManagement()) ->set('hostID', $this->obj->get('id')) ->set('min', $min) ->set('hour', $hour) @@ -5015,7 +5024,7 @@ private function massEditApplyRows(array $resolved, array $hostIDs) foreach ($hostIDs as $hostID) { $rows[] = [$hostID, $minutes]; } - self::getClass('HostAutoLogoutManager') + (new HostAutoLogoutManager()) ->insertBatch(['hostID', 'time'], $rows); } $wrote = count($hostIDs); @@ -5032,7 +5041,7 @@ private function massEditApplyRows(array $resolved, array $hostIDs) foreach ($hostIDs as $hostID) { $rows[] = [$hostID, $x, $y, $r]; } - self::getClass('HostScreenSettingManager') + (new HostScreenSettingManager()) ->insertBatch( ['hostID', 'width', 'height', 'refresh'], $rows @@ -5165,7 +5174,7 @@ private function massEditPluginFields(array $hostIDs = []) */ private function massEditColumnMap(array $spec) { - $map = self::getClass('HostManager')->getColumns(); + $map = (new HostManager())->getColumns(); $columns = []; foreach ($spec as $key => $entry) { $field = $entry['field'] ?? ''; @@ -5239,7 +5248,7 @@ private function massEditValueControl($key, array $spec) $kind = $spec['kind'] ?? 'text'; switch ($kind) { case 'image': - return self::getClass('ImageManager') + return (new ImageManager()) ->buildSelectBox('', $name, 'name', '', false, 'id', $id); /** * The same picker the single-host form uses. Mass edit was left on @@ -5768,7 +5777,7 @@ public function massEditPost() // it reported "Updated 1 field(s) on 86 host(s)" for a write // the database had refused -- which is how a clear that // never landed reads as a clear that did. - if (!self::getClass('HostManager') + if (!(new HostManager()) ->update(['id' => $hosts], '', $updates) ) { throw new \Exception( @@ -6022,7 +6031,7 @@ function ($name) { // success while inserting nothing -- see the resolution above // for the case that made it, and tests/save-propagates- // failure.test.php for why this tree treats it as a rule. - $New = self::getClass('Group') + $New = (new Group()) ->set('name', $group) ->addHost($hosts); if (!$New->save()) { @@ -6655,7 +6664,7 @@ public function taskPowerMultiPost() if ('wol' === $action) { foreach (Route::getList('host', ['id' => $hosts]) as $Host) { - self::getClass('Host', $Host->id)->wakeOnLAN(); + (new Host($Host->id))->wakeOnLAN(); } } else { // insertBatch UPSERTS against `powerManagement`.`cron`, so @@ -6666,7 +6675,7 @@ public function taskPowerMultiPost() foreach ($hosts as $hostID) { $items[] = [$hostID, '', '', '', '', '', 1, $action]; } - self::getClass('PowerManagementManager') + (new PowerManagementManager()) ->insertBatch( [ 'hostID', @@ -6742,7 +6751,7 @@ private function _quickTaskItems() $items = ''; $types = [TaskType::DEPLOY, TaskType::CAPTURE, TaskType::MULTICAST]; foreach ($types as $typeId) { - $TaskType = self::getClass('TaskType', $typeId); + $TaskType = new TaskType($typeId); // A server whose taskTypes row was deleted simply loses that // button, the same way the accordion loses the entry. if (!$TaskType->isValid()) { @@ -6800,7 +6809,7 @@ public function deployMulti() throw new \Exception(_('No hosts are selected')); } - $TaskType = self::getClass('TaskType', $type); + $TaskType = new TaskType($type); if (!$TaskType->isValid()) { throw new \Exception( sprintf( @@ -6945,7 +6954,7 @@ public function deployMultiPost() // bounded. Authorization::requirePageObjectScopeMass('host', $hosts); - $TaskType = self::getClass('TaskType', $type); + $TaskType = new TaskType($type); if (!$TaskType->isValid()) { throw new \Exception( sprintf( @@ -7025,7 +7034,7 @@ public function deployMultiPost() // back and a groups row would outlive the tasking it exists for. // Group::loadHosts() short circuits on an unsaved group, so the // ids set here are the ids used. - $Selection = self::getClass('Group') + $Selection = (new Group()) ->set( 'name', sprintf( @@ -7362,7 +7371,7 @@ public function deployPost() $snapinAbortOnFailure ); } else { - $ScheduledTask = self::getClass('ScheduledTask') + $ScheduledTask = (new ScheduledTask()) ->set('taskTypeID', $TaskType->id) ->set('name', $taskName) ->set('hostID', $this->obj->get('id')) diff --git a/packages/web/src/Pages/ImageManagement.php b/packages/web/src/Pages/ImageManagement.php index 66e0a2e8c6..3300c9ec68 100644 --- a/packages/web/src/Pages/ImageManagement.php +++ b/packages/web/src/Pages/ImageManagement.php @@ -19,6 +19,15 @@ use FOG\Items\Image; use FOG\Items\MulticastSession; use FOG\Items\StorageGroup; +use FOG\Items\StorageNode; +use FOG\Managers\ArchitectureManager; +use FOG\Managers\ImageAssociationManager; +use FOG\Managers\ImageManager; +use FOG\Managers\ImagePartitionTypeManager; +use FOG\Managers\ImageTypeManager; +use FOG\Managers\MulticastSessionManager; +use FOG\Managers\OSManager; +use FOG\Managers\StorageGroupManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -111,7 +120,7 @@ private function _displayStorageNode($StorageGroup) if (count($masterIds) < 1) { $masterIds = $ids; } - $StorageNode = self::getClass('StorageNode', array_shift($masterIds)); + $StorageNode = new StorageNode(array_shift($masterIds)); return $StorageNode->isValid() ? $StorageNode : null; } } @@ -137,20 +146,20 @@ protected function _addFields() $sgID = @min(Route::getIds('storagegroup', false)); } $StorageGroup = new StorageGroup($sgID); - $StorageGroups = self::getClass('StorageGroupManager') + $StorageGroups = (new StorageGroupManager()) ->buildSelectBox( $sgID, '', 'id' ); $StorageNode = $this->_displayStorageNode($StorageGroup); - $OSs = self::getClass('OSManager') + $OSs = (new OSManager()) ->buildSelectBox($os); $itID = 1; if ($imagetype > 0) { $itID = $imagetype; } - $ImageTypes = self::getClass('ImageTypeManager') + $ImageTypes = (new ImageTypeManager()) ->buildSelectBox( $itID, '', @@ -162,7 +171,7 @@ protected function _addFields() } else { $iptID = 1; } - $ImagePartitionTypes = self::getClass('ImagePartitionTypeManager') + $ImagePartitionTypes = (new ImagePartitionTypeManager()) ->buildSelectBox( $iptID, '', @@ -457,7 +466,7 @@ function (&$serverFault) { $imagemanage = (int)trim( (string)filter_input(INPUT_POST, 'imagemanage') ); - $exists = self::getClass('ImageManager') + $exists = (new ImageManager()) ->exists($image); if ($exists) { throw new \Exception( @@ -469,14 +478,14 @@ function (&$serverFault) { _('Please choose a different filename/path as this is reserved') ); } - $exists = self::getClass('ImageManager') + $exists = (new ImageManager()) ->exists($path, '', 'path'); if ($exists) { throw new \Exception( _('The path requested is already in use by another image!') ); } - $Image = self::getClass('Image') + $Image = (new Image()) ->set('name', $image) ->set('description', $description) ->set('osID', $os) @@ -521,7 +530,7 @@ public function imageGeneral() filter_input(INPUT_POST, 'os') ?: ($this->obj->get('osID') ?: '') ); - $OSs = self::getClass('OSManager') + $OSs = (new OSManager()) ->buildSelectBox($osID, '', 'id'); $path = ( filter_input(INPUT_POST, 'path') ?: @@ -531,13 +540,13 @@ public function imageGeneral() filter_input(INPUT_POST, 'imagetype') ?: ($this->obj->get('imageTypeID') ?: '') ); - $ImageTypes = self::getClass('ImageTypeManager') + $ImageTypes = (new ImageTypeManager()) ->buildSelectBox($itID, '', 'id'); $iptID = (int)( filter_input(INPUT_POST, 'imagepartitiontype') ?: ($this->obj->get('imagePartitionTypeID') ?: '') ); - $ImagePartitionTypes = self::getClass('ImagePartitionTypeManager') + $ImagePartitionTypes = (new ImagePartitionTypeManager()) ->buildSelectBox($iptID, '', 'id'); // The architectures an admin may pick on an IMAGE, which is what // `architectures.archIsAccess` is for -- the same flag taskTypes uses @@ -566,7 +575,7 @@ public function imageGeneral() if (count($archIds) < 1) { $archIds = [0]; } - $Architectures = self::getClass('ArchitectureManager') + $Architectures = (new ArchitectureManager()) ->buildSelectBox($archID, 'archID', 'name', $archIds); $isprot = ( isset($_POST['isProtected']) ? 'checked' : @@ -874,7 +883,7 @@ public function imageGeneralPost() $protected = (int)isset($_POST['isProtected']); $isEnabled = (int)isset($_POST['isEnabled']); $toReplicate = (int)isset($_POST['toReplicate']); - $exists = self::getClass('ImageManager')->exists($image); + $exists = (new ImageManager())->exists($image); $compress = (int)trim( (string)filter_input(INPUT_POST, 'compression') ); @@ -987,7 +996,7 @@ public function imageStoragegroupPost() $this->obj->get('storagegroups'), [$primary] ); - self::getClass('ImageAssociationManager')->update( + (new ImageAssociationManager())->update( [ 'imageID' => $this->obj->get('id'), 'storagegroupID' => $storagegroups, @@ -997,7 +1006,7 @@ public function imageStoragegroupPost() ['primary' => '0'] ); if ($primary) { - self::getClass('ImageAssociationManager')->update( + (new ImageAssociationManager())->update( [ 'imageID' => $this->obj->get('id'), 'storagegroupID' => $primary, @@ -1278,7 +1287,7 @@ public function sessionCreateModal() ); $image = filter_input(INPUT_POST, 'image'); - $images = self::getClass('ImageManager')->buildSelectBox( + $images = (new ImageManager())->buildSelectBox( $image ); @@ -1710,7 +1719,7 @@ public function architecturesPost() if ($id < 1 || !in_array($value, $valid, true)) { continue; } - $Arch = self::getClass('Architecture', $id); + $Arch = new Architecture($id); if (!$Arch->isValid()) { continue; } @@ -1929,7 +1938,7 @@ public function sessionCreate() _('Please select a valid image') ); } - if (self::getClass('MulticastSessionManager')->exists($sessionname)) { + if ((new MulticastSessionManager())->exists($sessionname)) { throw new \Exception(_('Session with that name already exists!')); } if ($sessioncount < 1) { @@ -1941,7 +1950,7 @@ public function sessionCreate() MulticastSession::assertCapacity(); $StorageGroup = $Image->getStorageGroup(); $StorageNode = $StorageGroup->getMasterStorageNode(); - return self::getClass('MulticastSession') + return (new MulticastSession()) ->set('name', $sessionname) ->set('port', MulticastSession::allocatePort()) ->set('image', $Image->get('id')) @@ -1980,7 +1989,7 @@ public function sessionCancel() ] ); $tasks = $tasks['tasks']; - self::getClass('MulticastSessionManager')->cancel( + (new MulticastSessionManager())->cancel( $tasks ); } diff --git a/packages/web/src/Pages/IpxeManagement.php b/packages/web/src/Pages/IpxeManagement.php index 7001ab0c76..b71347dd85 100644 --- a/packages/web/src/Pages/IpxeManagement.php +++ b/packages/web/src/Pages/IpxeManagement.php @@ -14,6 +14,8 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\PXEMenuOptions; +use FOG\Managers\PXEMenuOptionsManager; /** * The Bootmenu Management Page @@ -130,7 +132,7 @@ protected function _addFields() $labelClass, 'regmenu', _('Show with') - ) => self::getClass('PXEMenuOptionsManager')->regSelect( + ) => (new PXEMenuOptionsManager())->regSelect( $regmenu, 'regmenu' ), @@ -202,7 +204,7 @@ public function add() [ 'fields' => &$fields, 'buttons' => &$buttons, - 'Ipxe' => self::getClass('PXEMenuOptions') + 'Ipxe' => new PXEMenuOptions() ] ); $rendered = self::formFields($fields); @@ -262,14 +264,14 @@ function (&$serverFault) { $keysequence = trim( (string)filter_input(INPUT_POST, 'keysequence') ); - $exists = self::getClass('PXEMenuOptionsManager') + $exists = (new PXEMenuOptionsManager()) ->exists($ipxe); if ($exists) { throw new \Exception( _('A menu entry already exists with this name!') ); } - $iPXE = self::getClass('PXEMenuOptions') + $iPXE = (new PXEMenuOptions()) ->set('name', $ipxe) ->set('description', $description) ->set('params', $params) @@ -387,7 +389,7 @@ public function ipxeGeneral() $labelClass, 'regmenu', _('Show with') - ) => self::getClass('PXEMenuOptionsManager')->regSelect( + ) => (new PXEMenuOptionsManager())->regSelect( $regmenu, 'regmenu' ), @@ -491,7 +493,7 @@ public function ipxeGeneralPost() $keysequence = trim( (string)filter_input(INPUT_POST, 'keysequence') ); - $exists = self::getClass('PXEMenuOptionsManager') + $exists = (new PXEMenuOptionsManager()) ->exists($ipxe); if ($this->obj->get('name') != $ipxe && $exists) { throw new \Exception( diff --git a/packages/web/src/Pages/ModuleManagement.php b/packages/web/src/Pages/ModuleManagement.php index e742cffa29..66bbfccf24 100644 --- a/packages/web/src/Pages/ModuleManagement.php +++ b/packages/web/src/Pages/ModuleManagement.php @@ -16,6 +16,8 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\Module; +use FOG\Managers\ModuleManager; /** * Module management page @@ -178,14 +180,14 @@ function (&$serverFault) { (string)filter_input(INPUT_POST, 'shortname') ); $isDefault = (int)isset($_POST['isDefault']); - $exists = self::getClass('ModuleManager') + $exists = (new ModuleManager()) ->exists($module); if ($exists) { throw new \Exception( _('A module already exists with this name!') ); } - $Module = self::getClass('Module') + $Module = (new Module()) ->set('name', $module) ->set('description', $description) ->set('shortName', $shortname) diff --git a/packages/web/src/Pages/PluginManagement.php b/packages/web/src/Pages/PluginManagement.php index 61895fd974..1663f63f07 100644 --- a/packages/web/src/Pages/PluginManagement.php +++ b/packages/web/src/Pages/PluginManagement.php @@ -16,6 +16,7 @@ use FOG\Auth\Authorization; use FOG\Base\FOGPage; use FOG\Items\Plugin; +use FOG\Managers\PluginManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -88,7 +89,7 @@ public function index(...$args) Route::listem('plugin'); $data = json_decode(Route::getData()); foreach ((array)$data->data as &$row) { - $plugin = self::getClass('Plugin', $row->id); + $plugin = new Plugin($row->id); $row->needsupdate = $plugin->needsSchemaUpdate() ? 1 : 0; // Why a plugin can't be turned on, rendered on the row rather // than only raised when the activate button is pressed. The @@ -551,7 +552,7 @@ public function activatePost() $this->_refuseBlocked($plugins); $ids = ['id' => $plugins]; $state = ['state' => 1]; - $PluginManager = self::getClass('PluginManager'); + $PluginManager = new PluginManager(); if (!$PluginManager->update($ids, '', $state)) { $serverFault = true; throw new \Exception(_('Activate plugins failed!')); @@ -627,7 +628,7 @@ public function installPost() $ids = ['id' => $plugins]; $state = ['state' => 1]; $install = ['installed' => 1]; - $PluginManager = self::getClass('PluginManager'); + $PluginManager = new PluginManager(); if (!$PluginManager->update($ids, '', $state)) { $serverFault = true; throw new \Exception(_('Activate plugins failed!')); @@ -640,7 +641,7 @@ public function installPost() ] ); foreach ($Plugins as &$Plugin) { - $pluginObj = self::getClass('Plugin', $Plugin->id); + $pluginObj = new Plugin($Plugin->id); if (!$pluginObj->installdb()) { throw new \Exception( _('Failed to install ') @@ -736,7 +737,7 @@ public function upgradePost() ] ); foreach ($Plugins as &$Plugin) { - $pluginObj = self::getClass('Plugin', $Plugin->id); + $pluginObj = new Plugin($Plugin->id); if (!$pluginObj->installdb()) { throw new \Exception( _('Failed to update ') @@ -814,7 +815,7 @@ public function deactivatePost() try { $ids = ['id' => $plugins]; $state = ['state' => 0]; - $PluginManager = self::getClass('PluginManager'); + $PluginManager = new PluginManager(); if (!$PluginManager->update($ids, '', $state)) { $serverFault = true; throw new \Exception(_('Deactivate plugins failed!')); @@ -893,7 +894,7 @@ public function removePost() // with no tables and every query against it threw // "Base table or view not found". $install = ['installed' => 0, 'schema' => 0]; - $PluginManager = self::getClass('PluginManager'); + $PluginManager = new PluginManager(); if (!$PluginManager->update($ids, '', $state)) { $serverFault = true; throw new \Exception(_('Deactivate plugins failed!')); @@ -1035,7 +1036,7 @@ public function forgetPost() ); } foreach ($forget as $id => $name) { - $plugin = self::getClass('Plugin', $id); + $plugin = new Plugin($id); if (!$plugin->destroy()) { $serverFault = true; throw new \Exception(_('Failed to forget ') . $name); diff --git a/packages/web/src/Pages/PrinterManagement.php b/packages/web/src/Pages/PrinterManagement.php index 5ee38a22f2..ccfafb7885 100644 --- a/packages/web/src/Pages/PrinterManagement.php +++ b/packages/web/src/Pages/PrinterManagement.php @@ -14,6 +14,9 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\Printer; +use FOG\Managers\PrinterAssociationManager; +use FOG\Managers\PrinterManager; use FOG\Router\HTTPResponseCodes; /** @@ -132,7 +135,7 @@ private function _printerFormSections(array $values) $config, true ); - $printercopySelector = self::getClass('PrinterManager') + $printercopySelector = (new PrinterManager()) ->buildSelectBox('', 'printercopy'); // Common block: copy-from, type, name and description are shared by @@ -530,7 +533,7 @@ function (&$serverFault) { _('Please enter a printer name.') ); } - $exists = self::getClass('PrinterManager') + $exists = (new PrinterManager()) ->exists($printer); if ($exists) { throw new \Exception( @@ -544,7 +547,7 @@ function (&$serverFault) { _('A TCP/IP port printer requires an IP address or hostname.') ); } - $Printer = self::getClass('Printer') + $Printer = (new Printer()) ->set('name', $printer) ->set('description', $description) ->set('config', $printertype) @@ -699,7 +702,7 @@ public function printerGeneralPost() _('Please enter a printer name.') ); } - $exists = self::getClass('PrinterManager') + $exists = (new PrinterManager()) ->exists($printer); if ($printer != $this->obj->get('name') && $exists @@ -822,7 +825,7 @@ public function printerHostPost() $this->obj->addHost($hostsToAssoc)->save(); } if (count($hosts ?: []) > 0) { - self::getClass('PrinterAssociationManager')->update( + (new PrinterAssociationManager())->update( [ 'hostID' => $hosts, 'isDefault' => 1 @@ -830,7 +833,7 @@ public function printerHostPost() '', ['isDefault' => '0'] ); - self::getClass('PrinterAssociationManager')->update( + (new PrinterAssociationManager())->update( [ 'printerID' => $this->obj->get('id'), 'hostID' => $hosts, @@ -852,7 +855,7 @@ public function printerHostPost() ); $hosts = $hosts['remitems']; if (count($hosts ?: []) > 0) { - self::getClass('PrinterAssociationManager')->update( + (new PrinterAssociationManager())->update( [ 'printerID' => $this->obj->get('id'), 'hostID' => $hosts, diff --git a/packages/web/src/Pages/RoleManagement.php b/packages/web/src/Pages/RoleManagement.php index 74bdac31cd..a80b50fc0d 100644 --- a/packages/web/src/Pages/RoleManagement.php +++ b/packages/web/src/Pages/RoleManagement.php @@ -16,6 +16,9 @@ use FOG\Auth\Authorization; use FOG\Auth\SiteScope; use FOG\Base\FOGPage; +use FOG\Items\Role; +use FOG\Items\UserGroup; +use FOG\Managers\RoleManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -189,14 +192,14 @@ function (&$serverFault) { $description = trim( (string)filter_input(INPUT_POST, 'description') ); - $exists = self::getClass('RoleManager') + $exists = (new RoleManager()) ->exists($role); if ($exists) { throw new \Exception( _('A role already exists with this name!') ); } - $Role = self::getClass('Role') + $Role = (new Role()) ->set('name', $role) ->set('description', $description); if (!$Role->save()) { @@ -292,7 +295,7 @@ public function roleGeneralPost() (string)filter_input(INPUT_POST, 'description') ); - $exists = self::getClass('RoleManager') + $exists = (new RoleManager()) ->exists($role); if ($role != $this->obj->get('name') && $exists @@ -569,7 +572,7 @@ public function roleUserGroupPost() foreach ($attached as $groupID) { $roles = array_map( 'intval', - (array)self::getClass('UserGroup', $groupID)->get('roles') + (array)(new UserGroup($groupID))->get('roles') ); $roles[] = $roleID; $groupRoles[$groupID] = array_values(array_unique($roles)); @@ -580,7 +583,7 @@ public function roleUserGroupPost() foreach (array_diff($current, $attached) as $groupID) { $roles = array_map( 'intval', - (array)self::getClass('UserGroup', $groupID)->get('roles') + (array)(new UserGroup($groupID))->get('roles') ); $groupRoles[$groupID] = array_values( array_diff($roles, [$roleID]) diff --git a/packages/web/src/Pages/SchemaUpdaterPage.php b/packages/web/src/Pages/SchemaUpdaterPage.php index 84b4d02f6b..681983c301 100644 --- a/packages/web/src/Pages/SchemaUpdaterPage.php +++ b/packages/web/src/Pages/SchemaUpdaterPage.php @@ -332,7 +332,7 @@ public function indexPost() true ) : []; - $newSchema = self::getClass('Schema', 1); + $newSchema = new Schema(1); foreach ((array)$items as $version => &$updates) { foreach ((array)$updates as &$update) { if (!$update) { diff --git a/packages/web/src/Pages/ServiceConfigurationPage.php b/packages/web/src/Pages/ServiceConfigurationPage.php index 3c5b87ff00..19c367c6f5 100644 --- a/packages/web/src/Pages/ServiceConfigurationPage.php +++ b/packages/web/src/Pages/ServiceConfigurationPage.php @@ -15,6 +15,8 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\Module; +use FOG\Items\Setting; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -262,11 +264,8 @@ private function _saveModuleTab($key, $match, $hook, $extra = null) } unset($module); } - $Module = self::getClass( - 'Module', - $Module->id - ); - $Service = self::getClass('Setting') + $Module = new Module($Module->id); + $Service = (new Setting()) ->set('name', self::$_modNames[$key]) ->load('name'); if (isset($_POST['update'])) { diff --git a/packages/web/src/Pages/SiteManagement.php b/packages/web/src/Pages/SiteManagement.php index 07235aac86..8b99094be5 100644 --- a/packages/web/src/Pages/SiteManagement.php +++ b/packages/web/src/Pages/SiteManagement.php @@ -14,6 +14,8 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\Site; +use FOG\Managers\SiteManager; /** * Site management page. @@ -153,14 +155,14 @@ function (&$serverFault) { // INSERT ... ON DUPLICATE KEY UPDATE -- so without this // check creating a site with an existing name silently // OVERWRITES that site instead of failing. - $exists = self::getClass('SiteManager') + $exists = (new SiteManager()) ->exists($site); if ($exists) { throw new \Exception( _('A site already exists with this name!') ); } - $Site = self::getClass('Site') + $Site = (new Site()) ->set('name', $site) ->set('description', $description); if (!$Site->save()) { @@ -310,7 +312,7 @@ public function siteGeneralPost() // Same silent-overwrite guard as addPost(); a rename onto an // existing name would take that site's row with it. - $exists = self::getClass('SiteManager') + $exists = (new SiteManager()) ->exists($site); if ($site != $this->obj->get('name') && $exists diff --git a/packages/web/src/Pages/SnapinManagement.php b/packages/web/src/Pages/SnapinManagement.php index 7bf7275449..aaa3bb9427 100644 --- a/packages/web/src/Pages/SnapinManagement.php +++ b/packages/web/src/Pages/SnapinManagement.php @@ -18,6 +18,10 @@ use FOG\Exception\UploadException; use FOG\Items\Snapin; use FOG\Items\StorageGroup; +use FOG\Managers\FileDeleteQueueManager; +use FOG\Managers\SnapinGroupAssociationManager; +use FOG\Managers\SnapinManager; +use FOG\Managers\StorageGroupManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -280,7 +284,7 @@ protected function _addFields() $sgID = @min(Route::getIds('storagegroup', false)); } $StorageGroup = new StorageGroup($sgID); - $StorageGroups = self::getClass('StorageGroupManager') + $StorageGroups = (new StorageGroupManager()) ->buildSelectBox($sgID, '', 'id'); self::$selected = ''; self::$selected = $snapinfileexist; @@ -1213,7 +1217,7 @@ public function snapinGeneralPost() $action = trim((string)filter_input(INPUT_POST, 'action')); $args = trim((string)filter_input(INPUT_POST, 'args')); - $exists = self::getClass('SnapinManager') + $exists = (new SnapinManager()) ->exists($snapin); if ($snapin != $this->obj->get('name') && $exists @@ -1345,7 +1349,7 @@ public function snapinGeneralPost() $storagegroupID ]; } - self::getClass('FileDeleteQueueManager')->insertBatch( + (new FileDeleteQueueManager())->insertBatch( $insert_fields, $insert_values ); @@ -1460,7 +1464,7 @@ public function snapinStoragegroupPost() $this->obj->get('storagegroups'), [$primary] ); - self::getClass('SnapinGroupAssociationManager')->update( + (new SnapinGroupAssociationManager())->update( [ 'snapinID' => $this->obj->get('id'), 'storagegroupID' => $storagegroups, @@ -1470,7 +1474,7 @@ public function snapinStoragegroupPost() ['primary' => '0'] ); if ($primary) { - self::getClass('SnapinGroupAssociationManager')->update( + (new SnapinGroupAssociationManager())->update( [ 'snapinID' => $this->obj->get('id'), 'storagegroupID' => $primary, diff --git a/packages/web/src/Pages/SoftwareManagement.php b/packages/web/src/Pages/SoftwareManagement.php index 81c5a10cab..075670b0f8 100644 --- a/packages/web/src/Pages/SoftwareManagement.php +++ b/packages/web/src/Pages/SoftwareManagement.php @@ -15,6 +15,7 @@ use FOG\Base\FOGPage; use FOG\Items\Software; +use FOG\Managers\SoftwareManager; use FOG\Router\Route; /** @@ -465,14 +466,14 @@ function (&$serverFault) { $state, $backend ); - if (self::getClass('SoftwareManager')->exists($name)) { + if ((new SoftwareManager())->exists($name)) { throw new \Exception( _('A software entry already exists with this name!') ); } $version = $this->_resolveVersion($versionPolicy, $version); - $Software = self::getClass('Software') + $Software = (new Software()) ->set('name', $name) ->set('description', $description) ->set('backend', $backend) @@ -615,7 +616,7 @@ public function softwareGeneralPost() $state, $backend ); - $exists = self::getClass('SoftwareManager')->exists($name); + $exists = (new SoftwareManager())->exists($name); if ($name != $this->obj->get('name') && $exists ) { diff --git a/packages/web/src/Pages/StorageGroupManagement.php b/packages/web/src/Pages/StorageGroupManagement.php index d7232120a0..7c97740822 100644 --- a/packages/web/src/Pages/StorageGroupManagement.php +++ b/packages/web/src/Pages/StorageGroupManagement.php @@ -14,7 +14,12 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\StorageGroup; use FOG\Items\StorageNode; +use FOG\Managers\ImageAssociationManager; +use FOG\Managers\SnapinGroupAssociationManager; +use FOG\Managers\StorageGroupManager; +use FOG\Managers\StorageNodeManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -157,14 +162,14 @@ public function addPost() $serverFault = false; try { - $exists = self::getClass('StorageGroupManager') + $exists = (new StorageGroupManager()) ->exists($storagegroup); if ($exists) { throw new \Exception( _('A storage group exists with this name!') ); } - $StorageGroup = self::getClass('StorageGroup') + $StorageGroup = (new StorageGroup()) ->set('name', $storagegroup) ->set('description', $description) ->set('trustedcidrs', $trustedcidrs); @@ -313,7 +318,7 @@ public function storagegroupGeneralPost() (string)filter_input(INPUT_POST, 'trustedcidrs') ); - $exists = self::getClass('StorageGroupManager') + $exists = (new StorageGroupManager()) ->exists($storagegroup); if ($storagegroup != $this->obj->get('name') && $exists @@ -453,7 +458,7 @@ public function storagegroupImagePost() $this->obj->addImage($imagesToAssoc)->save(); } if (count($images ?: []) > 0) { - self::getClass('ImageAssociationManager')->update( + (new ImageAssociationManager())->update( [ 'imageID' => $images, 'primary' => 1 @@ -461,7 +466,7 @@ public function storagegroupImagePost() '', ['primary' => '0'] ); - self::getClass('ImageAssociationManager')->update( + (new ImageAssociationManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'imageID' => $images, @@ -483,7 +488,7 @@ public function storagegroupImagePost() ); $images = $images['remitems']; if (count($images ?: []) > 0) { - self::getClass('ImageAssociationManager')->update( + (new ImageAssociationManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'imageID' => $images, @@ -620,7 +625,7 @@ public function storagegroupSnapinPost() $this->obj->addSnapin($snapinsToAssoc)->save(); } if (count($snapins ?: []) > 0) { - self::getClass('SnapinGroupAssociationManager')->update( + (new SnapinGroupAssociationManager())->update( [ 'snapinID' => $snapins, 'primary' => 1 @@ -628,7 +633,7 @@ public function storagegroupSnapinPost() '', ['primary' => '0'] ); - self::getClass('SnapinGroupAssociationManager')->update( + (new SnapinGroupAssociationManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'snapinID' => $snapins, @@ -650,7 +655,7 @@ public function storagegroupSnapinPost() ); $snapins = $snapins['remitems']; if (count($snapins ?: []) > 0) { - self::getClass('SnapinGroupAssociationManager')->update( + (new SnapinGroupAssociationManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'snapinID' => $snapins, @@ -767,7 +772,7 @@ public function storagegroupStoragenodePost() $this->obj->get('allnodes'), [$master] ); - self::getClass('StorageNodeManager')->update( + (new StorageNodeManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'id' => $storagenodes, @@ -777,7 +782,7 @@ public function storagegroupStoragenodePost() ['isMaster' => '0'] ); if ($master) { - self::getClass('StorageNodeManager')->update( + (new StorageNodeManager())->update( [ 'storagegroupID' => $this->obj->get('id'), 'id' => $master, diff --git a/packages/web/src/Pages/StorageNodeManagement.php b/packages/web/src/Pages/StorageNodeManagement.php index 66d833865b..8e7cbdb045 100644 --- a/packages/web/src/Pages/StorageNodeManagement.php +++ b/packages/web/src/Pages/StorageNodeManagement.php @@ -14,6 +14,9 @@ namespace FOG\Pages; use FOG\Base\FOGPage; +use FOG\Items\StorageNode; +use FOG\Managers\StorageGroupManager; +use FOG\Managers\StorageNodeManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -129,7 +132,7 @@ protected function _addFields() $labelClass, 'storagegroupID', _('Storage Group') - ) => self::getClass('StorageGroupManager') + ) => (new StorageGroupManager()) ->buildSelectBox( $storagegroupID, 'storagegroupID' @@ -503,7 +506,7 @@ public function addPost() self::$FOGSSH->host = $ip; $warning = !self::$FOGSSH->connect(); } - $exists = self::getClass('StorageNodeManager') + $exists = (new StorageNodeManager()) ->exists($storagenode); if ($exists) { throw new \Exception( @@ -519,7 +522,7 @@ public function addPost() } else { $bandwidth = ''; } - $StorageNode = self::getClass('StorageNode') + $StorageNode = (new StorageNode()) ->set('name', $storagenode) ->set('description', $description) ->set('ip', $ip) @@ -553,7 +556,7 @@ public function addPost() 'storagenode', $find ); - self::getClass('StorageNodeManager') + (new StorageNodeManager()) ->update( [ 'id' => array_diff( @@ -757,7 +760,7 @@ public function storagenodeGeneral() $labelClass, 'storagegroupID', _('Storage Group') - ) => self::getClass('StorageGroupManager') + ) => (new StorageGroupManager()) ->buildSelectBox( $storagegroupID, 'storagegroupID' @@ -1202,7 +1205,7 @@ public function storagenodeGeneralPost() if (!$storagenode) { throw new \Exception(self::$foglang['StorageNameRequired']); } - $exists = self::getClass('StorageNodeManager') + $exists = (new StorageNodeManager()) ->exists($storagenode, $this->obj->get('id')); if ($storagenode != $this->obj->get('name') && $exists @@ -1251,7 +1254,7 @@ public function storagenodeGeneralPost() 'storagenode', $find ); - self::getClass('StorageNodeManager') + (new StorageNodeManager()) ->update( [ 'id' => array_diff( @@ -1353,7 +1356,7 @@ public function edit() 'name' => _('Information'), 'id' => 'storagenode-info', 'generator' => function () { - self::getClass('ServerInfo')->index(); + (new ServerInfo())->index(); } ]; diff --git a/packages/web/src/Pages/TaskManagement.php b/packages/web/src/Pages/TaskManagement.php index 5c8e0d9fa0..9022259a67 100644 --- a/packages/web/src/Pages/TaskManagement.php +++ b/packages/web/src/Pages/TaskManagement.php @@ -17,6 +17,17 @@ use FOG\Base\FOGPage; use FOG\Items\TaskLog; use FOG\Items\TaskType; +use FOG\Managers\FileDeleteQueueManager; +use FOG\Managers\HostManager; +use FOG\Managers\ImageManager; +use FOG\Managers\MulticastSessionManager; +use FOG\Managers\ScheduledTaskManager; +use FOG\Managers\SnapinTaskManager; +use FOG\Managers\StorageNodeManager; +use FOG\Managers\TaskManager; +use FOG\Managers\TaskStateManager; +use FOG\Managers\TaskTypeManager; +use FOG\Managers\UserManager; use FOG\Router\HTTPResponseCodes; use FOG\Router\Route; @@ -957,7 +968,7 @@ public function getRecentTasks() private function _taskJoinColumns() { $columns = []; - foreach (self::getClass('TaskManager') + foreach ((new TaskManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -966,7 +977,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('HostManager') + foreach ((new HostManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -975,7 +986,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('ImageManager') + foreach ((new ImageManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -984,7 +995,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('TaskTypeManager') + foreach ((new TaskTypeManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -993,7 +1004,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('TaskStateManager') + foreach ((new TaskStateManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -1002,7 +1013,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('StorageNodeManager') + foreach ((new StorageNodeManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -1011,7 +1022,7 @@ private function _taskJoinColumns() ]; unset($real); } - foreach (self::getClass('UserManager') + foreach ((new UserManager()) ->getColumns() as $common => &$real ) { if (in_array($common, ['id', 'name'])) { @@ -1115,7 +1126,7 @@ public function getActiveMulticastTasks() LEFT OUTER JOIN `taskStates` ON `multicastSessions`.`msState` = `taskStates`.`tsID` WHERE $where"; - foreach (self::getClass('MulticastSessionManager') + foreach ((new MulticastSessionManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -1124,7 +1135,7 @@ public function getActiveMulticastTasks() ]; unset($real); } - foreach (self::getClass('TaskTypeManager') + foreach ((new TaskTypeManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -1133,7 +1144,7 @@ public function getActiveMulticastTasks() ]; unset($real); } - foreach (self::getClass('TaskStateManager') + foreach ((new TaskStateManager()) ->getColumns() as $common => &$real ) { $columns[] = [ @@ -1288,7 +1299,7 @@ public function activePost() ] ); $tasks = $tasks['tasks']; - self::getClass('TaskManager')->cancel($tasks); + (new TaskManager())->cancel($tasks); } $code = HTTPResponseCodes::HTTP_ACCEPTED; $hook = 'TASK_CANCEL_SUCCESS'; @@ -1349,8 +1360,8 @@ public function activemulticastPost() $find, 'taskID' ); - self::getClass('TaskManager')->cancel($tasks); - self::getClass('MulticastSessionManager')->cancel($mtasks); + (new TaskManager())->cancel($tasks); + (new MulticastSessionManager())->cancel($mtasks); } $code = HTTPResponseCodes::HTTP_ACCEPTED; $hook = 'TASK_CANCEL_SUCCESS'; @@ -1404,7 +1415,7 @@ public function activesnapinsPost() ] ); $tasks = $tasks['tasks']; - self::getClass('SnapinTaskManager')->cancel($tasks); + (new SnapinTaskManager())->cancel($tasks); } $code = HTTPResponseCodes::HTTP_ACCEPTED; $hook = 'TASK_CANCEL_SUCCESS'; @@ -1458,7 +1469,7 @@ public function activescheduledPost() ] ); $tasks = $tasks['tasks']; - self::getClass('ScheduledTaskManager')->cancel($tasks); + (new ScheduledTaskManager())->cancel($tasks); } $code = HTTPResponseCodes::HTTP_ACCEPTED; $hook = 'TASK_CANCEL_SUCCESS'; @@ -1512,7 +1523,7 @@ public function activescheduleddelsPost() ] ); $tasks = $tasks['tasks']; - self::getClass('FileDeleteQueueManager')->cancel($tasks); + (new FileDeleteQueueManager())->cancel($tasks); } $code = HTTPResponseCodes::HTTP_ACCEPTED; $hook = 'QUEUED_DELETION_CANCEL_SUCCESS'; diff --git a/packages/web/src/Pages/UserGroupManagement.php b/packages/web/src/Pages/UserGroupManagement.php index 2cebb8f789..243f0edd06 100644 --- a/packages/web/src/Pages/UserGroupManagement.php +++ b/packages/web/src/Pages/UserGroupManagement.php @@ -16,6 +16,8 @@ use FOG\Auth\Authorization; use FOG\Auth\SiteScope; use FOG\Base\FOGPage; +use FOG\Items\UserGroup; +use FOG\Managers\UserGroupManager; use FOG\Router\HTTPResponseCodes; /** @@ -143,14 +145,14 @@ function (&$serverFault) { $description = trim( (string)filter_input(INPUT_POST, 'description') ); - $exists = self::getClass('UserGroupManager') + $exists = (new UserGroupManager()) ->exists($usergroup); if ($exists) { throw new \Exception( _('A user group already exists with this name!') ); } - $UserGroup = self::getClass('UserGroup') + $UserGroup = (new UserGroup()) ->set('name', $usergroup) ->set('description', $description); if (!$UserGroup->save()) { @@ -247,7 +249,7 @@ public function usergroupGeneralPost() (string)filter_input(INPUT_POST, 'description') ); - $exists = self::getClass('UserGroupManager') + $exists = (new UserGroupManager()) ->exists($usergroup); if ($usergroup != $this->obj->get('name') && $exists diff --git a/packages/web/src/Pages/UserManagement.php b/packages/web/src/Pages/UserManagement.php index 8069cb1ca4..d7f1a694ea 100644 --- a/packages/web/src/Pages/UserManagement.php +++ b/packages/web/src/Pages/UserManagement.php @@ -17,6 +17,8 @@ use FOG\Base\FOGPage; use FOG\Items\APIToken; use FOG\Items\User; +use FOG\Managers\APITokenManager; +use FOG\Managers\UserManager; use FOG\Router\HTTPResponseCodes; /** @@ -341,14 +343,14 @@ public function addPost() if (!preg_match($userPat, $user)) { throw new \Exception($userErr); } - $exists = self::getClass('UserManager') + $exists = (new UserManager()) ->exists($user); if ($exists) { throw new \Exception( _('A username already exists with this name!') ); } - $User = self::getClass('User') + $User = (new User()) ->set('name', $user) ->set('password', $password) ->set('display', $friendly) @@ -627,7 +629,7 @@ public function userGeneralPost() if (!preg_match($userPat, $user)) { throw new \Exception($userErr); } - $exists = self::getClass('UserManager') + $exists = (new UserManager()) ->exists($user); if ($user != $this->obj->get('name') && $exists @@ -1177,7 +1179,7 @@ public function userAPITokenListPost() // either: scope decides what this administrator may see at all, and // the id decides which of that belongs on this page. foreach ( - self::getClass('APITokenManager') + (new APITokenManager()) ->visibleTo((int)self::$FOGUser->get('id')) as $token ) { if ($token['userID'] !== $uid) { @@ -1243,7 +1245,7 @@ public function userAPITokenDeletePost() // The owner id is passed, so this card can only ever act on the // account it is displayed under. Without it anyone who may edit one // user could revoke any token on the server by posting its number. - $revoked = self::getClass('APITokenManager')->revokeMany( + $revoked = (new APITokenManager())->revokeMany( array_map('intval', (array)($_POST['remitems'] ?? [])), (int)self::$FOGUser->get('id'), (int)$this->obj->get('id') @@ -1290,7 +1292,7 @@ public function userAPITokenEnablePost() } $enabled = (int)filter_input(INPUT_POST, 'enabled') === 1; - $changed = self::getClass('APITokenManager')->setEnabledMany( + $changed = (new APITokenManager())->setEnabledMany( array_map('intval', (array)($_POST['remitems'] ?? [])), $enabled, (int)self::$FOGUser->get('id'), diff --git a/packages/web/src/Reports/Imaging_Report.php b/packages/web/src/Reports/Imaging_Report.php index 9346dd67a9..080326c799 100644 --- a/packages/web/src/Reports/Imaging_Report.php +++ b/packages/web/src/Reports/Imaging_Report.php @@ -231,7 +231,7 @@ protected function reportRows() foreach ($rows as $row) { $stateID = (int)($row['stateID'] ?? 0); if ($stateID > 0 && !isset($states[$stateID])) { - $states[$stateID] = (string)self::getClass('TaskState', $stateID) + $states[$stateID] = (string)(new TaskState($stateID)) ->get('name'); } $data[] = [ diff --git a/packages/web/src/Reports/Run_History.php b/packages/web/src/Reports/Run_History.php index 58ef628f0d..3ae98ffcee 100644 --- a/packages/web/src/Reports/Run_History.php +++ b/packages/web/src/Reports/Run_History.php @@ -15,6 +15,7 @@ use FOG\Audit\ActivityWindow; use FOG\Audit\ReportWindow; +use FOG\Items\TaskState; use FOG\Pages\ReportManagement; /** @@ -222,7 +223,7 @@ protected function reportRows() foreach ($rows as $row) { $id = (int)($row['state'] ?? 0); if ($id > 0 && !isset($states[$id])) { - $states[$id] = (string)self::getClass('TaskState', $id) + $states[$id] = (string)(new TaskState($id)) ->get('name'); } } diff --git a/packages/web/src/Reports/Snapin_Report.php b/packages/web/src/Reports/Snapin_Report.php index b18649d258..41ea4d571f 100644 --- a/packages/web/src/Reports/Snapin_Report.php +++ b/packages/web/src/Reports/Snapin_Report.php @@ -15,6 +15,7 @@ use FOG\Audit\ReportWindow; use FOG\Audit\SnapinStats; +use FOG\Items\TaskState; use FOG\Pages\ReportManagement; /** @@ -227,7 +228,7 @@ protected function reportRows() foreach ($rows as $row) { $stateID = (int)($row['stateID'] ?? 0); if ($stateID > 0 && !isset($states[$stateID])) { - $states[$stateID] = (string)self::getClass('TaskState', $stateID) + $states[$stateID] = (string)(new TaskState($stateID)) ->get('name'); } $code = (int)($row['code'] ?? 0); diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 0f729a161a..9b296c2dde 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -32,12 +32,21 @@ use FOG\Items\Inventory; use FOG\Items\MACAddress; use FOG\Items\Plugin; +use FOG\Items\Schema; use FOG\Items\Snapin; use FOG\Items\SnapinJob; +use FOG\Items\StorageGroup; use FOG\Items\StorageNode; use FOG\Items\Task; +use FOG\Items\TaskState; +use FOG\Items\User; use FOG\Items\UserTracking; +use FOG\Managers\HostManager; use FOG\Managers\PXEMenuOptionsManager; +use FOG\Managers\SavedFilterManager; +use FOG\Managers\SnapinJobManager; +use FOG\Managers\TaskManager; +use FOG\Managers\UserPrefManager; use FOG\Net\Ping; use FOG\Util\FOGCron; @@ -2224,7 +2233,7 @@ private static function _testAuth() (string)filter_input(INPUT_SERVER, 'HTTP_FOG_USER_TOKEN') ); $usertoken = trim($usertoken); - $pwtoken = self::getClass('User') + $pwtoken = (new User()) ->set('token', $usertoken) ->load('token'); if ($pwtoken->isValid() && $pwtoken->get('api')) { @@ -2273,7 +2282,7 @@ private static function _testAuth() // Reloading also gives the acting user a fully populated object, // matching what the token branch binds -- passwordValidate() only // fills in id, name and type on the object it was called against. - $apiUser = self::getClass('User', (int)self::$FOGUser->get('id')); + $apiUser = new User((int)self::$FOGUser->get('id')); if (!$apiUser->isValid() || !$apiUser->get('api')) { // A correct password for an account that may not use the API. // Distinct from a bad credential and worth telling apart: this @@ -2470,7 +2479,7 @@ private static function _sendCaught(\Exception $e) public static function userprefs() { self::$data = [ - 'prefs' => self::getClass('UserPrefManager')->fetchAll( + 'prefs' => (new UserPrefManager())->fetchAll( (int)self::$FOGUser->get('id') ), 'msg' => _('success') @@ -2512,7 +2521,7 @@ public static function savedfilters() return; } - $manager = self::getClass('SavedFilterManager'); + $manager = new SavedFilterManager(); $method = strtoupper(self::$reqmethod ?: 'GET'); if ('GET' === $method) { self::$data = [ @@ -2575,7 +2584,7 @@ public static function savedfilter($id) return; } - $manager = self::getClass('SavedFilterManager'); + $manager = new SavedFilterManager(); $method = strtoupper(self::$reqmethod ?: 'GET'); if ('GET' === $method) { $filter = $manager->fetch((int)$id, $userID); @@ -2815,7 +2824,7 @@ public static function userpref($key) if ($method === 'GET') { self::$data = [ 'key' => $key, - 'value' => self::getClass('UserPrefManager') + 'value' => (new UserPrefManager()) ->fetch($userID, $key), 'msg' => _('success') ]; @@ -2855,7 +2864,7 @@ public static function userpref($key) return; } } - if (!self::getClass('UserPrefManager')->store($userID, $key, $value)) { + if (!(new UserPrefManager())->store($userID, $key, $value)) { self::sendResponse( HTTPResponseCodes::HTTP_BAD_REQUEST, json_encode( @@ -2998,7 +3007,7 @@ public static function agentPoll() if ('' !== $version) { $fields['agentVersion'] = $version; } - self::getClass('HostManager')->update( + (new HostManager())->update( ['id' => (int)$Host->get('id')], '', $fields @@ -3330,7 +3339,7 @@ public static function export() 'fog_backup_%s.sql', self::formatTime('now', 'Ymd_His') ); - self::getClass('Schema')->exportdb($backup_name); + (new Schema())->exportdb($backup_name); exit; } /** @@ -4757,7 +4766,7 @@ function ($d, $row) { 'formatter' => function ($d) use (&$taskStates) { $id = (int)$d; if (!isset($taskStates[$id])) { - $taskStates[$id] = self::getClass('TaskState', $id) + $taskStates[$id] = (new TaskState($id)) ->get('name'); } return $taskStates[$id] ?: self::EMPTY_CELL; @@ -4794,10 +4803,7 @@ function ($d, $row) { ? (int)$row['taskStateID'] : 0; if (!isset($taskStates[$stateId])) { - $taskStates[$stateId] = self::getClass( - 'TaskState', - $stateId - )->get('name'); + $taskStates[$stateId] = (new TaskState($stateId))->get('name'); } // An unresolvable state renders as a word, not as // nothing. The `statename` column can afford an @@ -4869,7 +4875,7 @@ function ($d, $row) { $groupFor = function ($id) use (&$storageGroups) { $id = (int) $id; if (!isset($storageGroups[$id])) { - $storageGroups[$id] = self::getClass('StorageGroup') + $storageGroups[$id] = (new StorageGroup()) ->set('id', $id) ->load(); } @@ -5959,7 +5965,7 @@ private static function _applyEditAssociations($class, $classname, $vars, $id) // not start failing the day it becomes blocked. if (isset($vars->state) && (int)$vars->state === 1 - && !(int)self::getClass('Plugin', $id)->get('state') + && !(int)(new Plugin($id))->get('state') ) { $blockers = Plugin::activationBlockers([(int)$id]); if (count($blockers)) { @@ -6360,7 +6366,7 @@ public static function createSnapinWithFile() public static function uploadSnapinFiles($id) { try { - $StorageGroup = self::getClass('StorageGroup', (int)$id); + $StorageGroup = new StorageGroup((int)$id); if (!$StorageGroup->isValid()) { self::sendResponse( HTTPResponseCodes::HTTP_NOT_FOUND, @@ -6469,7 +6475,7 @@ private static function _blockerReasons(array $blockers) public static function pluginInstall($id) { try { - $Plugin = self::getClass('Plugin', (int)$id); + $Plugin = new Plugin((int)$id); if (!$Plugin->isValid()) { // setErrorMessage(), not sendResponse(): every error this // route can answer with is documented as the Error schema, @@ -6628,7 +6634,7 @@ public static function cancel($class, $id) _('No active tasks to cancel for this group') ); } - self::getClass('TaskManager')->cancel($taskIDs); + (new TaskManager())->cancel($taskIDs); break; case 'host': self::_requireFound($class); @@ -6697,10 +6703,7 @@ public static function cancel($class, $id) // Failed task came back from the API reporting success // and stayed Failed. if (!in_array($class->get('stateID'), $states)) { - $stateName = self::getClass( - 'TaskState', - $class->get('stateID') - )->get('name'); + $stateName = (new TaskState($class->get('stateID')))->get('name'); self::_notCancellable( sprintf( '%s: %s', @@ -9281,7 +9284,7 @@ private static function _removeItemsFor($classname, $itemIDs) break; case 'image': $findWhere = ['imageID' => $itemIDs]; - self::getClass('HostManager')->update( + (new HostManager())->update( $findWhere, '', // NULL, not 0 -- see schema step 386. @@ -9321,7 +9324,7 @@ private static function _removeItemsFor($classname, $itemIDs) ] ); if (count($activeImageTaskIDs ?: [])) { - self::getClass('TaskManager') + (new TaskManager()) ->cancel($activeImageTaskIDs); } $removeItems = [ @@ -9406,7 +9409,7 @@ private static function _removeItemsFor($classname, $itemIDs) unset($sjID); } if (count($sjIDs ?: [])) { - self::getClass('SnapinJobManager')->cancel($sjIDs); + (new SnapinJobManager())->cancel($sjIDs); } break; case 'user': @@ -10717,7 +10720,7 @@ public static function availableinitrds() */ public static function logfiles($id) { - self::$data = self::getClass('StorageNode', $id)->get('logfiles'); + self::$data = (new StorageNode($id))->get('logfiles'); } /** * Return node's image files. @@ -10726,7 +10729,7 @@ public static function logfiles($id) */ public static function imagefiles($id) { - self::$data = self::getClass('StorageNode', $id)->get('images'); + self::$data = (new StorageNode($id))->get('images'); } /** * Return node's snapin files. @@ -10735,7 +10738,7 @@ public static function imagefiles($id) */ public static function snapinfiles($id) { - self::$data = self::getClass('StorageNode', $id)->get('snapinfiles'); + self::$data = (new StorageNode($id))->get('snapinfiles'); } /** * The five server facts this route publishes, in the order they are diff --git a/packages/web/src/Service/FileDeleter.php b/packages/web/src/Service/FileDeleter.php index 17dbc8c8f6..06560cce54 100644 --- a/packages/web/src/Service/FileDeleter.php +++ b/packages/web/src/Service/FileDeleter.php @@ -13,6 +13,7 @@ namespace FOG\Service; +use FOG\Items\FileDeleteQueue; use FOG\Router\Route; /** @@ -220,7 +221,7 @@ function () { $filedelete->createdTime ) ); - $Task = self::getClass('FileDeleteQueue', $filedelete->id) + $Task = (new FileDeleteQueue($filedelete->id)) ->set('stateID', self::getProgressState()) ->save(); $StorageNodes = Route::getList( diff --git a/packages/web/src/Service/MulticastManager.php b/packages/web/src/Service/MulticastManager.php index 2680642cab..fc88e0d9f3 100644 --- a/packages/web/src/Service/MulticastManager.php +++ b/packages/web/src/Service/MulticastManager.php @@ -13,6 +13,7 @@ namespace FOG\Service; +use FOG\Items\MulticastSession; use FOG\Router\Route; /** @@ -207,10 +208,7 @@ private function _isSenderAlive($pid) */ private function _senderClaimIsFree($curTask) { - $owner = (int)self::getClass( - 'MulticastSession', - $curTask->getID() - )->get('sendernode'); + $owner = (int)(new MulticastSession($curTask->getID()))->get('sendernode'); return $owner < 1 || $owner === $curTask->getNodeID(); @@ -260,7 +258,7 @@ private function _reconcileOrphanedSenders() ) ); } - self::getClass('MulticastSession', $Session->id) + (new MulticastSession($Session->id)) ->set('senderpid', 0) // senderpid is a process id and 0 is a real "no process". // sendernode is a reference, so its "none" is NULL -- diff --git a/packages/web/src/Service/MulticastTask.php b/packages/web/src/Service/MulticastTask.php index da0327c51b..aa213fe0f6 100644 --- a/packages/web/src/Service/MulticastTask.php +++ b/packages/web/src/Service/MulticastTask.php @@ -13,7 +13,10 @@ namespace FOG\Service; +use FOG\Items\Image; use FOG\Items\MulticastSession; +use FOG\Items\Task; +use FOG\Managers\MulticastSessionManager; use FOG\Router\Route; /** @@ -145,7 +148,7 @@ public static function getAllMulticastTasks( (int)$Task->sessclients ); if ($count < 1) { - self::getClass('MulticastSessionManager')->update( + (new MulticastSessionManager())->update( ['id' => $Task->id], '', [ @@ -174,7 +177,7 @@ public static function getAllMulticastTasks( // unwinds the whole collection pass and takes every other queued // session with it. Testing validity here keeps the failure scoped // to the one session it belongs to. Refs #907, ADR 0011. - if (!self::getClass('Image', $Task->image)->isValid()) { + if (!(new Image($Task->image))->isValid()) { self::outall( sprintf( ' | ' . _('Image %s for session %s is missing or invalid; skipping'), @@ -432,10 +435,7 @@ public function getImageType() */ public function getImageFormat() { - return (int)self::getClass( - 'Image', - $this->_MultiSess->get('image') - )->get('format'); + return (int)(new Image($this->_MultiSess->get('image')))->get('format'); } /** * Returns the client count @@ -500,10 +500,7 @@ public function getUDPCastLogFile() */ public function getBitrate() { - return self::getClass( - 'Image', - $this->_MultiSess->get('image') - )->getStorageGroup() + return (new Image($this->_MultiSess->get('image')))->getStorageGroup() ->getMasterStorageNode() ->get('bitrate'); } @@ -514,10 +511,7 @@ public function getBitrate() */ public function getHelloInterval() { - return self::getClass( - 'Image', - $this->_MultiSess->get('image') - )->getStorageGroup() + return (new Image($this->_MultiSess->get('image')))->getStorageGroup() ->getMasterStorageNode() ->get('helloInterval'); } @@ -1258,7 +1252,7 @@ public function clearSenderRef() ) { return false; } - self::getClass('MulticastSessionManager')->update( + (new MulticastSessionManager())->update( ['id' => $this->getID()], '', [ @@ -1284,7 +1278,7 @@ public function updateStats() ); $TaskPercent = []; foreach ($MSAssocs as $TaskID) { - $TaskPercent[] = self::getClass('Task', $TaskID)->get('percent'); + $TaskPercent[] = (new Task($TaskID))->get('percent'); } $TaskPercent = array_unique($TaskPercent); // Write the one column this owns. updateStats() runs against the @@ -1294,7 +1288,7 @@ public function updateStats() // first-seen snapshot back every tick. That silently undid the // clients counter TaskQueue::checkIn() increments as hosts arrive, // which is why a session's client count never climbed. - self::getClass('MulticastSessionManager')->update( + (new MulticastSessionManager())->update( ['id' => $this->_intID], '', ['percent' => self::maxId($TaskPercent)] diff --git a/packages/web/src/Service/PingHosts.php b/packages/web/src/Service/PingHosts.php index c31f0ed1a0..1014507357 100644 --- a/packages/web/src/Service/PingHosts.php +++ b/packages/web/src/Service/PingHosts.php @@ -14,6 +14,7 @@ namespace FOG\Service; +use FOG\Managers\HostManager; use FOG\Net\Ping; use FOG\Router\Route; @@ -479,7 +480,7 @@ private function _commonOutput() // deleted mid-cycle. Do NOT use insertBatch here -- its // INSERT ... ON DUPLICATE KEY UPDATE would resurrect a // deleted host as a blank, nameless row. - self::getClass('HostManager') + (new HostManager()) ->update( ['id' => $chunk], '', @@ -622,7 +623,7 @@ private function _verifyStored( // Cleared in one statement. The address is not merely stale, it // is known to belong to something else, so leaving it would have // every future cycle re-derive the same wrong answer. - self::getClass('HostManager')->update( + (new HostManager())->update( ['id' => array_keys($recycled)], '', ['ip' => ''] diff --git a/packages/web/src/Service/TaskScheduler.php b/packages/web/src/Service/TaskScheduler.php index d6a8a82585..6c71d8e212 100644 --- a/packages/web/src/Service/TaskScheduler.php +++ b/packages/web/src/Service/TaskScheduler.php @@ -14,6 +14,11 @@ namespace FOG\Service; use FOG\Boot\UbootTftpSync; +use FOG\Items\PowerManagement; +use FOG\Items\ScheduledTask; +use FOG\Items\Task; +use FOG\Managers\GroupPowerManagementManager; +use FOG\Managers\TaskManager; use FOG\Router\Route; /** @@ -156,7 +161,7 @@ private function _commonOutput() ' * ' . _('Checking for tasks that can never run...') ); - $reaped = self::getClass('TaskManager')->reapUnrunnable(); + $reaped = (new TaskManager())->reapUnrunnable(); foreach ($reaped as $taskID => $why) { self::outall( sprintf( @@ -182,7 +187,7 @@ private function _commonOutput() ]; $Tasks = Route::getList('task', $find); foreach ($Tasks as $Task) { - if(self::getClass('Task', $Task->id)->expireTaskCheckin()) { + if((new Task($Task->id))->expireTaskCheckin()) { self::outall( ' * ' . _('Found an expired task, resetting to queued for task of id') @@ -216,7 +221,7 @@ function () { // whose only wake schedule is a group grant has no host rows at // all, and the daemon would have thrown before ever reaching the // grant loop -- a schedule that silently never fires. - $GroupPMGrants = self::getClass('GroupPowerManagementManager') + $GroupPMGrants = (new GroupPowerManagementManager()) ->wakeGrants(); $gtaskcount = count($GroupPMGrants); @@ -238,7 +243,7 @@ function () { unset($taskCount); // Scheduled Tasks foreach ($ScheduledTasks->data as $Task) { - $Task = self::getClass('ScheduledTask', $Task->id); + $Task = new ScheduledTask($Task->id); $Timer = $Task->getTimer(); self::outall( ' * ' @@ -336,7 +341,7 @@ function () { } // Power Management Tasks. foreach ($PMTasks->data as $Task) { - $Task = self::getClass('PowerManagement', $Task->id); + $Task = new PowerManagement($Task->id); $Timer = $Task->getTimer(); self::outall( ' * ' diff --git a/packages/web/src/TaskHandling/TaskError.php b/packages/web/src/TaskHandling/TaskError.php index 64ab999d1b..73eb7dcba9 100644 --- a/packages/web/src/TaskHandling/TaskError.php +++ b/packages/web/src/TaskHandling/TaskError.php @@ -294,7 +294,7 @@ public function __construct() private static function _markFailed($Task) { $failed = TaskState::getFailedState(); - if (!self::getClass('TaskState', $failed)->isValid()) { + if (!(new TaskState($failed))->isValid()) { return; } $Task->set('stateID', $failed)->save(); @@ -308,7 +308,7 @@ private static function _logRow($Task, $type, $text) // the time the join fails the host row is gone too, which makes this // the last moment the name can be recorded at all. See TaskLog's // $databaseFields and schema step 341. - self::getClass('TaskLog') + (new TaskLog()) ->set('taskID', $Task->get('id')) ->set('stateID', $Task->get('stateID')) ->set('createdBy', 'fos') diff --git a/packages/web/src/TaskHandling/TaskQueue.php b/packages/web/src/TaskHandling/TaskQueue.php index cd98b16eb4..1076dbacd6 100644 --- a/packages/web/src/TaskHandling/TaskQueue.php +++ b/packages/web/src/TaskHandling/TaskQueue.php @@ -16,8 +16,12 @@ use FOG\Audit\Audit; use FOG\Boot\UbootTftpSync; use FOG\Items\Image; +use FOG\Items\MulticastSession; +use FOG\Items\MulticastSessionAssociation; use FOG\Items\StorageNode; +use FOG\Items\Task; use FOG\Items\TaskType; +use FOG\Managers\HostManager; use FOG\Router\Route; /** @@ -97,7 +101,7 @@ public static function ackIfAlreadyComplete() if ($taskID < 1) { return; } - $Task = self::getClass('Task', $taskID); + $Task = new Task($taskID); if (!$Task->isValid()) { return; } @@ -142,10 +146,7 @@ public function checkIn() : 'getOptimalStorageNode'; if ($this->Task->isMulticast()) { $msID = self::minId(Route::getIds('multicastsessionassociation', ['taskID' => $this->Task->get('id')], 'msID')); - $MulticastSession = self::getClass( - 'MulticastSession', - $msID - ); + $MulticastSession = new MulticastSession($msID); if (!$MulticastSession->isValid()) { throw new \Exception(_('Invalid Multicast Session')); } @@ -190,10 +191,7 @@ public function checkIn() } } else { $this->StorageNode = self::nodeFail( - self::getClass( - 'StorageNode', - $this->Task->get('storagenodeID') - ), + new StorageNode($this->Task->get('storagenodeID')), self::$Host->get('id') ); $nodeOk = $this->StorageNode instanceof StorageNode && @@ -628,7 +626,7 @@ public function checkout() 'renderable' => 1 ]); if ($this->Task->isMulticast()) { - $MCTask = self::getClass('MulticastSessionAssociation') + $MCTask = (new MulticastSessionAssociation()) ->set( 'taskID', $this->Task->get('id') @@ -697,7 +695,7 @@ public function checkout() _('Host is not valid; the task cannot be completed') ); } - $updatedHost = self::getClass('HostManager')->update( + $updatedHost = (new HostManager())->update( ['id' => self::$Host->get('id')], '', $updateFields diff --git a/packages/web/src/TaskHandling/TaskingElement.php b/packages/web/src/TaskHandling/TaskingElement.php index 4538082f3f..51b2667898 100644 --- a/packages/web/src/TaskHandling/TaskingElement.php +++ b/packages/web/src/TaskHandling/TaskingElement.php @@ -135,10 +135,7 @@ public function __construct() ['id' => $this->StorageGroup->get($getter)] ); foreach ($StorageNodes as &$StorageNode) { - $this->StorageNodes[] = self::getClass( - 'StorageNode', - $StorageNode->id - ); + $this->StorageNodes[] = new StorageNode($StorageNode->id); unset($StorageNode); } if ($this->Task->isCapture() diff --git a/packages/web/status/dbrunning.php b/packages/web/status/dbrunning.php index 47aa9606d7..16e9bbb410 100644 --- a/packages/web/status/dbrunning.php +++ b/packages/web/status/dbrunning.php @@ -13,6 +13,7 @@ use FOG\Base\FOGCore; use FOG\Db\DatabaseManager; +use FOG\Items\Schema; use FOG\Router\HTTPResponseCodes; /** @@ -31,7 +32,7 @@ $link = DatabaseManager::getLink(); $redirect = false; if ($link) { - $redirect = FOGCore::getClass('Schema', 1) + $redirect = (new Schema(1)) ->get('version') == FOG_SCHEMA; } $ret = [ diff --git a/packages/web/status/hostgetkey.php b/packages/web/status/hostgetkey.php index 64fbe2c049..39c448b4d6 100644 --- a/packages/web/status/hostgetkey.php +++ b/packages/web/status/hostgetkey.php @@ -12,6 +12,7 @@ */ use FOG\Base\FOGCore; +use FOG\Managers\HostManager; /** * Hostgetkey returns the host token for hostinfo getting @@ -57,7 +58,7 @@ throw new \Exception(_('Host token is currently in use')); } if (!FOGCore::$Host->get('token')) { - FOGCore::getClass('HostManager')->update( + (new HostManager())->update( ['id' => FOGCore::$Host->get('id')], '', [ @@ -68,7 +69,7 @@ throw new \Exception(FOGCore::$Host->get('token')); } if (FOGCore::$Host->isValid() && !FOGCore::$Host->get('tokenlock')) { - FOGCore::getClass('HostManager')->update( + (new HostManager())->update( ['id' => FOGCore::$Host->get('id')], '', ['tokenlock' => true] diff --git a/tests/activity-sources.test.php b/tests/activity-sources.test.php index 69d97ee00c..e0fcf8d623 100644 --- a/tests/activity-sources.test.php +++ b/tests/activity-sources.test.php @@ -43,6 +43,7 @@ use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -53,7 +54,7 @@ $db->pdo->rowCount = 1; $db->pdo->countValue = 1; -$user = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$user = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $user); } diff --git a/tests/db-failure-is-recorded.test.php b/tests/db-failure-is-recorded.test.php index dffb167f16..fed0a561cc 100644 --- a/tests/db-failure-is-recorded.test.php +++ b/tests/db-failure-is-recorded.test.php @@ -55,7 +55,12 @@ use FOG\Base\FOGBase; use FOG\Base\FOGCore; +use FOG\Items\Host; +use FOG\Items\Task; use FOG\Items\TaskLog; +use FOG\Items\TaskType; +use FOG\Items\User; +use FOG\Managers\TaskLogManager; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -147,7 +152,7 @@ public function insertId() // The condition every machine-facing request boots into: LoadGlobals builds // `new User(0)` because there is no session to read FOG_USER from. -$anon = FOGCore::getClass('User'); +$anon = new User(); $GLOBALS['currentUser'] = $anon; FogTestHarness::setStatic('FOGBase', 'FOGUser', $anon); if ($anon->isValid()) { @@ -199,7 +204,7 @@ function fogLogContents() // --------------------------------------------------------------------- // 1. An existing row whose write was rejected must not report success. // --------------------------------------------------------------------- -$existing = FOGCore::getClass('TaskLog'); +$existing = new TaskLog(); $existing->set('id', 4242) ->set('taskID', 7) ->set('text', 'save-failure existing row'); @@ -216,7 +221,7 @@ function fogLogContents() // 2. A failed write must leave a record with no user signed in. // --------------------------------------------------------------------- $before = fogLogContents(); -$newRow = FOGCore::getClass('TaskLog'); +$newRow = new TaskLog(); $newRow->set('taskID', 9)->set('text', 'save-failure new row'); $newRow->save(); $after = fogLogContents(); @@ -230,15 +235,15 @@ function fogLogContents() // 3. The sink itself: TaskError::_logRow() discards save()'s return, so // only a fix inside the framework can cover it. // --------------------------------------------------------------------- -$host = FOGCore::getClass('Host'); +$host = new Host(); $host->set('id', 11); FogTestHarness::setStatic('FOGBase', 'Host', $host); -$task = FOGCore::getClass('Task'); +$task = new Task(); $task->set('id', 33)->set('stateID', 3); // getTaskTypeText() dereferences the loaded TaskType object; the fake // database answers lazy loads with marker strings, so it is seeded directly. -$task->set('type', FOGCore::getClass('TaskType')->set('id', 1)->set('name', 'Deploy')); +$task->set('type', (new TaskType())->set('id', 1)->set('name', 'Deploy')); $before = fogLogContents(); $writesBefore = $db->writes; @@ -274,7 +279,7 @@ function fogLogContents() // --------------------------------------------------------------------- $db->rejectReads = true; $before = fogLogContents(); -$reader = FOGCore::getClass('TaskLog'); +$reader = new TaskLog(); $reader->set('id', 8080)->load('id'); if (fogLogContents() === $before) { $failures[] = 'a rejected SELECT in load() left no record -- an object ' @@ -286,7 +291,7 @@ function fogLogContents() // rejected read returns an EMPTY set, which the caller reads as "none of // those ids exist" rather than "the question was not asked". $before = fogLogContents(); -FOGCore::getClass('TaskLog')->loadMany([1, 2, 3], 'id'); +(new TaskLog())->loadMany([1, 2, 3], 'id'); if (fogLogContents() === $before) { $failures[] = 'a rejected SELECT in loadMany() left no record -- the ' . 'caller cannot tell an empty result from an unasked question'; @@ -318,7 +323,7 @@ function fogLogContents() $db->rejectReads = false; $db->rejectFetch = true; $before = fogLogContents(); -$fetchReader = FOGCore::getClass('TaskLog'); +$fetchReader = new TaskLog(); $fetchReader->set('id', 9090)->load('id'); if (fogLogContents() === $before) { $failures[] = 'a SELECT that ran but could not be FETCHED left no record ' @@ -329,7 +334,7 @@ function fogLogContents() // assertion -- driving load() alone leaves the bulk read's check position // unpinned. $before = fogLogContents(); -FOGCore::getClass('TaskLog')->loadMany([4, 5, 6], 'id'); +(new TaskLog())->loadMany([4, 5, 6], 'id'); if (fogLogContents() === $before) { $failures[] = 'a bulk SELECT that ran but could not be FETCHED left no ' . 'record -- the caller reads the empty set as "none of those ids ' @@ -379,7 +384,7 @@ function fogLogContents() // catch that handles both. // --------------------------------------------------------------------- $before = fogLogContents(); -FOGCore::getClass('TaskLog')->load('id'); +(new TaskLog())->load('id'); if (fogLogContents() !== $before) { $failures[] = 'loading an object with no id wrote a fault line -- that is ' . "load()'s normal control flow, not a database failure, and at that " @@ -393,7 +398,7 @@ function fogLogContents() // The API's bulk edit is its busiest caller. // --------------------------------------------------------------------- $before = fogLogContents(); -$mass = FOGCore::getClass('TaskLogManager')->update( +$mass = (new TaskLogManager())->update( ['id' => 1], 'AND', ['text' => 'save-failure mass update'] @@ -412,7 +417,7 @@ function fogLogContents() // whether to CREATE, so an unreadable database becomes a duplicate row // rather than an error. $db->rejectReads = true; -$manager = FOGCore::getClass('TaskLogManager'); +$manager = new TaskLogManager(); $before = fogLogContents(); // idField is 'name' by default and taskLog has no such column; hostName is // a real one, so the probe fails on the DATABASE rather than on the model. diff --git a/tests/event-frame-readers.test.php b/tests/event-frame-readers.test.php index 0b42e9e37c..01c883ae0c 100644 --- a/tests/event-frame-readers.test.php +++ b/tests/event-frame-readers.test.php @@ -35,6 +35,8 @@ use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\Host; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -45,7 +47,7 @@ $db->pdo->rowCount = 1; $db->pdo->countValue = 1; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } @@ -230,7 +232,7 @@ function () use ($classname) { FogTestHarness::setStatic( 'Route', 'relCache', - ['host:41' => FOGCore::getClass('Host')->set('id', 41)->set('name', 'renamed')] + ['host:41' => (new Host())->set('id', 41)->set('name', 'renamed')] ); $out = $link(41, $row); $t->check( diff --git a/tests/grid-header-column-agreement.test.php b/tests/grid-header-column-agreement.test.php index e223f88423..586adc0bdc 100644 --- a/tests/grid-header-column-agreement.test.php +++ b/tests/grid-header-column-agreement.test.php @@ -37,6 +37,8 @@ */ use FOG\Base\FOGCore; +use FOG\Items\User; +use FOG\Pages\HostManagement; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -48,7 +50,7 @@ // Unrestricted, so the header set is captured whole rather than the subset // one permission set happens to reach. -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } @@ -69,7 +71,7 @@ function hostHeaderRow($pingActive) { FogTestHarness::setStatic('FOGBase', 'fogpingactive', $pingActive); - $page = FOGCore::getClass('HostManagement'); + $page = new HostManagement(); $row = $page->buildHeaderRow(); preg_match_all('/]*>/', $row, $ths); preg_match_all('/data-col="([^"]+)"/', $row, $cols); diff --git a/tests/grid-host-name-order.test.php b/tests/grid-host-name-order.test.php index 88905c8b01..f6783a7867 100644 --- a/tests/grid-host-name-order.test.php +++ b/tests/grid-host-name-order.test.php @@ -41,6 +41,7 @@ use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -53,7 +54,7 @@ // Unrestricted, so the column table is captured whole rather than the subset // one permission set happens to reach. Same reasoning as the column contract. -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/group-capture-tasking.test.php b/tests/group-capture-tasking.test.php index 40ac603f3f..bda4452d00 100644 --- a/tests/group-capture-tasking.test.php +++ b/tests/group-capture-tasking.test.php @@ -19,6 +19,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Group; use FOG\Router\Route; require __DIR__ . '/lib/fog-test-harness.php'; @@ -219,7 +220,7 @@ function taskSelection(array $hosts, $tasktype) global $inserts; $inserts = []; $error = ''; - $Group = FOGCore::getClass('Group') + $Group = (new Group()) ->set('name', 'selection') ->set('hosts', $hosts); try { diff --git a/tests/group-grants-are-owned.test.php b/tests/group-grants-are-owned.test.php index db4600d7d3..d50cd4df8f 100644 --- a/tests/group-grants-are-owned.test.php +++ b/tests/group-grants-are-owned.test.php @@ -40,6 +40,8 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Group; +use FOG\Items\User; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -49,7 +51,7 @@ $db = FogTestHarness::fakeDb(); $root = dirname(__DIR__) . '/packages/web'; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } @@ -70,7 +72,7 @@ function runGroup($db, $method, $arg, $groupID = 3, $respond = null) { $db->log = []; $db->responder = $respond; - FOGCore::getClass('Group') + (new Group()) ->set('id', $groupID) ->{$method}($arg); $db->responder = null; diff --git a/tests/group-order-is-settable.test.php b/tests/group-order-is-settable.test.php index b187ff3ea2..a54c124854 100644 --- a/tests/group-order-is-settable.test.php +++ b/tests/group-order-is-settable.test.php @@ -38,6 +38,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Group; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -105,7 +106,7 @@ function orderMethodBody($file, $method) function saveGroupWithOrder($db) { $db->log = []; - FOGCore::getClass('Group') + (new Group()) ->set('name', 'Lab') ->set('order', 7) ->save(); @@ -121,7 +122,7 @@ function saveGroupWithOrder($db) } $t->check('saving a group writes the groupOrder column', $wrote); -$loaded = FOGCore::getClass('Group')->set('order', 0); +$loaded = (new Group())->set('order', 0); $t->check( 'an order of 0 reads back as 0 rather than as unset', $loaded->get('order') === 0 || $loaded->get('order') === '0' diff --git a/tests/history-untranslated-and-bounded.test.php b/tests/history-untranslated-and-bounded.test.php index a250efbdfd..2a1ac62497 100644 --- a/tests/history-untranslated-and-bounded.test.php +++ b/tests/history-untranslated-and-bounded.test.php @@ -45,6 +45,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\History; require __DIR__ . '/lib/fog-test-harness.php'; @@ -146,7 +147,7 @@ function methodBody($file, $sig) && false !== strpos($helper, "'%s ID: %s'") && false !== strpos($helper, "' Name: %s'") ); -$history = FOGCore::getClass('History'); +$history = new History(); $req = new \ReflectionProperty(get_class($history), 'databaseFieldsRequired'); $req->setAccessible(true); $t->check( diff --git a/tests/host-list-queue-task.test.php b/tests/host-list-queue-task.test.php index 1ef8ef90fd..1932cd081b 100644 --- a/tests/host-list-queue-task.test.php +++ b/tests/host-list-queue-task.test.php @@ -48,6 +48,7 @@ use FOG\Auth\Authorization; use FOG\Base\FOGCore; +use FOG\Items\Group; use FOG\Pages\HostManagement; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -228,7 +229,7 @@ public function get($key) $db = FogTestHarness::fakeDb(); $mark = count($db->log); -$Selection = FOGCore::getClass('Group'); +$Selection = new Group(); $Selection->set('name', '3 selected hosts'); $Selection->set('hosts', [4, 9, 17]); $hosts = $Selection->get('hosts'); @@ -263,7 +264,7 @@ static function ($sql) { return null; }; -$Real = FOGCore::getClass('Group'); +$Real = new Group(); $Real->set('id', 3); $t->check( 'a saved group still loads its members', diff --git a/tests/host-list-quick-tasks.test.php b/tests/host-list-quick-tasks.test.php index fc08cf60f2..3ae7dc1cdd 100644 --- a/tests/host-list-quick-tasks.test.php +++ b/tests/host-list-quick-tasks.test.php @@ -58,6 +58,7 @@ use FOG\Base\FOGCore; use FOG\Items\TaskType; +use FOG\Items\User; use FOG\Pages\HostManagement; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -137,7 +138,7 @@ * @return string the emitted markup */ $emit = static function (array $perms) use ($page, $items) { - $user = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); + $user = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $user); } diff --git a/tests/host-modules-are-tristate.test.php b/tests/host-modules-are-tristate.test.php index fa73dd828c..ce3acc3c43 100644 --- a/tests/host-modules-are-tristate.test.php +++ b/tests/host-modules-are-tristate.test.php @@ -44,6 +44,8 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Host; +use FOG\Items\User; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -53,7 +55,7 @@ $db = FogTestHarness::fakeDb(); $root = dirname(__DIR__) . '/packages/web'; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } @@ -85,7 +87,7 @@ function writeState($db, array $existing, $ids, $state, $hostID = 5) } return null; }; - FOGCore::getClass('Host') + (new Host()) ->set('id', $hostID) ->setModuleState($ids, $state); $db->responder = null; diff --git a/tests/lib/fog-test-harness.php b/tests/lib/fog-test-harness.php index f2f683d247..b3d0b24644 100644 --- a/tests/lib/fog-test-harness.php +++ b/tests/lib/fog-test-harness.php @@ -37,6 +37,7 @@ use FOG\Base\EventManager; use FOG\Base\FOGCore; use FOG\Base\HookManager; +use FOG\Items\User; /** * One prepared statement's worth of canned rows. @@ -387,7 +388,7 @@ public static function boot($label) // has to exist before the first hook is built, even an anonymous one. // It falls back to $GLOBALS['currentUser'] when that user is invalid, // so both are seeded. - $anon = FOGCore::getClass('User'); + $anon = new User(); $GLOBALS['currentUser'] = $anon; self::setStatic('FOGBase', 'FOGUser', $anon); diff --git a/tests/lib/rehearsal-runner.php b/tests/lib/rehearsal-runner.php index 93a1e847fe..f8697d97be 100644 --- a/tests/lib/rehearsal-runner.php +++ b/tests/lib/rehearsal-runner.php @@ -127,7 +127,7 @@ public function run($from, $to = null, $stamp = true) $ran++; } if ($stamp) { - $schema = self::getClass('Schema', 1); + $schema = new \FOG\Items\Schema(1); $schema->set('version', $landed)->save(); } diff --git a/tests/ping-alive-codes.test.php b/tests/ping-alive-codes.test.php index 88bc843cf0..c5eb1d67cb 100644 --- a/tests/ping-alive-codes.test.php +++ b/tests/ping-alive-codes.test.php @@ -33,6 +33,7 @@ use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\User; use FOG\Net\Ping; use FOG\Router\Route; @@ -128,7 +129,7 @@ function check($label, $cond, array &$failures, &$checks) $db->pdo->rowCount = 1; $db->pdo->countValue = 1; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/power-actions-are-tasks.test.php b/tests/power-actions-are-tasks.test.php index 7f1e9be345..3757f800ef 100644 --- a/tests/power-actions-are-tasks.test.php +++ b/tests/power-actions-are-tasks.test.php @@ -52,6 +52,7 @@ * @link https://fogproject.org */ +use FOG\Items\Host; use FOG\Pages\HostManagement; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -231,7 +232,7 @@ // 4. The host's Power Management tab is schedules only. // ------------------------------------------------------------------------- $host = new HostManagement(); -$obj = \FOG\Base\FOGCore::getClass('Host'); +$obj = new Host(); $obj->set('id', 5); $objProp = new \ReflectionProperty(get_class($host), 'obj'); $objProp->setAccessible(true); diff --git a/tests/printer-grants-reach-the-client.test.php b/tests/printer-grants-reach-the-client.test.php index 4f2ed8fb3d..e2078a16cb 100644 --- a/tests/printer-grants-reach-the-client.test.php +++ b/tests/printer-grants-reach-the-client.test.php @@ -53,6 +53,8 @@ use FOG\Base\FOGCore; use FOG\Client\PrinterClient; +use FOG\Items\Host; +use FOG\Items\User; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -61,7 +63,7 @@ $t = new FogChecks(); $db = FogTestHarness::fakeDb(); -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } @@ -230,7 +232,7 @@ function askServer( FogTestHarness::setStatic( 'FOGBase', 'Host', - FOGCore::getClass('Host')->set('id', $hostID)->set('printerLevel', $level) + (new Host())->set('id', $hostID)->set('printerLevel', $level) ); // newInstanceWithoutConstructor(), and this is the whole reason the // endpoint looked untestable. FOGClient's constructor resolves the host diff --git a/tests/relationship-filter-in-join.test.php b/tests/relationship-filter-in-join.test.php index fe327dd09c..8892321ce3 100644 --- a/tests/relationship-filter-in-join.test.php +++ b/tests/relationship-filter-in-join.test.php @@ -37,6 +37,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Host; require __DIR__ . '/lib/fog-test-harness.php'; @@ -67,11 +68,11 @@ function relFilterBuild($class) * and the test would be measuring nothing. */ $relProp = new \ReflectionProperty( - get_class(FOGCore::getClass('Host')), + get_class(new Host()), 'databaseFieldClassRelationships' ); $relProp->setAccessible(true); -$rels = $relProp->getValue(FOGCore::getClass('Host')); +$rels = $relProp->getValue(new Host()); $macRel = $rels['MACAddressAssociation'] ?? null; $t->check( 'Host still declares a filtered relationship to MACAddressAssociation', diff --git a/tests/route-cascade-contract.test.php b/tests/route-cascade-contract.test.php index f41f816d80..c0f89f8b05 100644 --- a/tests/route-cascade-contract.test.php +++ b/tests/route-cascade-contract.test.php @@ -51,6 +51,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -63,7 +64,7 @@ $db->pdo->rowCount = 1; $db->pdo->countValue = 1; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/route-column-contract.test.php b/tests/route-column-contract.test.php index 8aa5fc6d1c..fd10fd3cd3 100644 --- a/tests/route-column-contract.test.php +++ b/tests/route-column-contract.test.php @@ -54,6 +54,7 @@ use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -69,7 +70,7 @@ // An unrestricted user: the table must be captured whole, not the subset one // permission set happens to reach. -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/route-getter-contract.test.php b/tests/route-getter-contract.test.php index 049932ba71..4fa550e18b 100644 --- a/tests/route-getter-contract.test.php +++ b/tests/route-getter-contract.test.php @@ -46,6 +46,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -58,7 +59,7 @@ $db->pdo->rowCount = 1; $db->pdo->countValue = 1; -$admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); +$admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/route-read-path-guards.test.php b/tests/route-read-path-guards.test.php index 2217e23b71..22f171d8f9 100644 --- a/tests/route-read-path-guards.test.php +++ b/tests/route-read-path-guards.test.php @@ -56,6 +56,7 @@ use FOG\Auth\SiteScope; use FOG\Base\FOGCore; use FOG\Base\Hook; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -171,7 +172,7 @@ function () use ($class) { // The parent asserts the CLI arm, this asserts the request arm, and // between them they pin the gate rather than just one side of it. if ('scope-sql' === $case) { - $scoped = FOGCore::getClass('User')->set('id', 7)->set('name', 'scoped'); + $scoped = (new User())->set('id', 7)->set('name', 'scoped'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $scoped); } @@ -884,7 +885,7 @@ function () use ($listPayload) { * user with no site, silently. Each case below is asserted by identity. * =========================================================================== */ -$scopedUser = FOGCore::getClass('User')->set('id', 7)->set('name', 'scoped'); +$scopedUser = (new User())->set('id', 7)->set('name', 'scoped'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $scopedUser); } @@ -1350,7 +1351,7 @@ public function inject($arguments) * =========================================================================== */ $savedFogUser = FogTestHarness::getStatic('Authorization', 'FOGUser'); -$scopedUser2 = FOGCore::getClass('User')->set('id', 7)->set('name', 'scoped'); +$scopedUser2 = (new User())->set('id', 7)->set('name', 'scoped'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $scopedUser2); } diff --git a/tests/route-write-path-guards.test.php b/tests/route-write-path-guards.test.php index 0db31610e9..5bf4d73930 100644 --- a/tests/route-write-path-guards.test.php +++ b/tests/route-write-path-guards.test.php @@ -60,6 +60,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\User; use FOG\Router\Route; require_once __DIR__ . '/lib/fog-test-harness.php'; @@ -159,7 +160,7 @@ function runChild($case) return null; }; - $admin = FOGCore::getClass('User')->set('id', 1)->set('name', 'fog'); + $admin = (new User())->set('id', 1)->set('name', 'fog'); foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { FogTestHarness::setStatic($cls, 'FOGUser', $admin); } diff --git a/tests/tasklog-records-cancel.test.php b/tests/tasklog-records-cancel.test.php index aece183636..f3e9fb4bd8 100644 --- a/tests/tasklog-records-cancel.test.php +++ b/tests/tasklog-records-cancel.test.php @@ -34,6 +34,7 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Host; require __DIR__ . '/lib/fog-test-harness.php'; @@ -68,7 +69,7 @@ public function isImagingTask() } } -$host = FOGCore::getClass('Host') +$host = (new Host()) ->set('id', 42) ->set('name', 'lab-07'); diff --git a/tests/tasklog-report-retention.test.php b/tests/tasklog-report-retention.test.php index 7250039af9..7ecbc26527 100644 --- a/tests/tasklog-report-retention.test.php +++ b/tests/tasklog-report-retention.test.php @@ -39,6 +39,8 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Host; +use FOG\Items\TaskLog; require __DIR__ . '/lib/fog-test-harness.php'; @@ -60,7 +62,7 @@ (function () { $p = new \ReflectionProperty('FOG\Items\TaskLog', 'databaseFields'); $p->setAccessible(true); - $fields = $p->getValue(FOGCore::getClass('TaskLog')); + $fields = $p->getValue(new TaskLog()); return isset($fields['hostID'], $fields['hostName'], $fields['taskTypeName']) && 'logHostID' === $fields['hostID'] && 'logHostName' === $fields['hostName'] @@ -68,7 +70,7 @@ })() ); -$host = FOGCore::getClass('Host') +$host = (new Host()) ->set('id', 42) ->set('name', 'lab-07'); diff --git a/tests/tasklog-stamps-transition-time.test.php b/tests/tasklog-stamps-transition-time.test.php index 34cadf4b99..60cc220c1c 100644 --- a/tests/tasklog-stamps-transition-time.test.php +++ b/tests/tasklog-stamps-transition-time.test.php @@ -29,6 +29,8 @@ */ use FOG\Base\FOGCore; +use FOG\Items\Host; +use FOG\Items\Image; require __DIR__ . '/lib/fog-test-harness.php'; @@ -71,12 +73,12 @@ public function isImagingTask() } } -$host = FOGCore::getClass('Host') +$host = (new Host()) ->set('id', 42) ->set('name', 'lab-07'); // Image declares name/path/imageTypeID/osID required, and recordState() only // takes the name off an image that isValid() -- so all four are set here. -$image = FOGCore::getClass('Image') +$image = (new Image()) ->set('id', 9) ->set('name', 'win11-base') ->set('path', 'win11-base') From 4232aa5f7d02f5d89470e1cac97f861f034f8865 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 14:29:17 -0500 Subject: [PATCH 060/117] Backslash-prefix the built-ins in getclass-methods-exist tests/global-class-prefix.test.php refuses a bare global class reference and was failing on this file's RecursiveIteratorIterator and RecursiveDirectoryIterator -- red on this branch before the getClass merge, and unrelated to it. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- tests/getclass-methods-exist.test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/getclass-methods-exist.test.php b/tests/getclass-methods-exist.test.php index 65f08728f7..b80f746483 100644 --- a/tests/getclass-methods-exist.test.php +++ b/tests/getclass-methods-exist.test.php @@ -114,7 +114,7 @@ function getClassCalls($file) if (!is_dir($dir)) { continue; } - $it = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($dir)); + $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($dir)); foreach ($it as $f) { if ($f->isFile() && 'php' === strtolower($f->getExtension())) { $files[] = $f->getPathname(); From f37024e3cd0cbd4e5133223dd82521dd7bb47fd4 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 19:30:25 +0000 Subject: [PATCH 061/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a591667b71..e5853014cb 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,6 +10359,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 211004c5b3..911205bfd0 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,6 +10368,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ac670c5901..655d6af5ef 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,6 +10527,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c79895d6c2..2070a26faa 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,6 +10360,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da26901cb2..a28aeb921e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,6 +10352,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d7b3169c32..b603f7ed4a 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index dc1ff1b8bf..9476bef60a 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,6 +10030,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 6e57cf07ce..005a44f351 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,6 +8879,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 40818e02f7..87b82f5950 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 782e8c0d39..4b7e8276d9 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 7ebdcd268099949d2b9676889ec7eeeee3c9d84d Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 14:36:55 -0500 Subject: [PATCH 062/117] getclass-methods-exist: anchor the scan on files, not on call sites The second check asserted `$checked > 50` -- more than fifty getClass('Name')->method() pairs found -- so that a scan resolving nothing could not pass the first check for the wrong reason. That was right when the tree had over a hundred of them. ADR 0043 removed them. Counting the thing being eliminated turns every success into a step toward a failure, and it took two commits to get there: packages/web/src now has none, and CI does not run bin/fetch-plugins.sh, so packages/web/lib/plugins -- where the rest live, in bundled artifacts built before the sweep -- is absent there too. CI saw 0 sites across 326 files and failed on the anchor while the check it protects was perfectly happy. Anchored on files read instead. That still catches every failure it was written for -- a bad root, a broken tokenizer loop, a filter matching nothing -- and it was verified by pointing the root at a directory that does not exist and watching it go red. A local run still finds its 138 sites in the bundled plugins; a CI run finds 0 and passes. The header says when to retire the test: once no shipped plugin build predates the sweep, there is nothing left for it to read. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - tests/getclass-methods-exist.test.php | 26 +++++++++++++++++-- 11 files changed, 24 insertions(+), 12 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e5853014cb..a591667b71 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,7 +10359,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 911205bfd0..211004c5b3 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,7 +10368,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 655d6af5ef..ac670c5901 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,7 +10527,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2070a26faa..c79895d6c2 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,7 +10360,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a28aeb921e..da26901cb2 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,7 +10352,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b603f7ed4a..d7b3169c32 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,7 +10075,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9476bef60a..dc1ff1b8bf 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,7 +10030,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 005a44f351..6e57cf07ce 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,7 +8879,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 87b82f5950..40818e02f7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 4b7e8276d9..782e8c0d39 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/tests/getclass-methods-exist.test.php b/tests/getclass-methods-exist.test.php index b80f746483..6876df451d 100644 --- a/tests/getclass-methods-exist.test.php +++ b/tests/getclass-methods-exist.test.php @@ -19,6 +19,14 @@ * Nothing here is FOG-specific beyond the seam: it is the check the language * would give us for free if the class name were not a string. * + * Which is why the seam is closing. ADR 0043 retired the literal getClass() + * across core and fog-plugins, so a name is spelled at the call site where + * the language checks it for free. What is left for this test is the bundled + * plugin artifacts under packages/web/lib/plugins on a real install, which + * lag a plugin release behind the source -- so it finds call sites there and + * none in packages/web/src, and finds none at all in CI, which does not + * fetch them. Retire it once no shipped plugin build predates that sweep. + * * Parsed with token_get_all rather than a regex, so an argument list that * contains parentheses -- getClass('Host', self::something($x)) -- is matched * to its real closing paren instead of the first one. @@ -178,10 +186,24 @@ function getClassCalls($file) fwrite(STDERR, " unresolved: $u\n"); } +/* + * Anchored on FILES rather than on call sites, and that is the whole point + * of ADR 0043: the literals this test inspects are being eliminated, so + * counting them would turn every success into a step toward a failure. A + * source checkout now has none in packages/web/src at all, and CI never runs + * bin/fetch-plugins.sh, so packages/web/lib/plugins -- where the remaining + * ones live, in bundled artifacts built before the sweep -- is absent there + * and $checked is legitimately 0. + * + * What still has to hold is that the scan READ something. A bad root, a + * broken tokenizer loop or a filter that matches nothing all show up here, + * and all of them would otherwise let the check above pass for the wrong + * reason. + */ $t->check( - 'and the check actually reached something -- a scan that resolves nothing' + 'and the scan actually reached the tree -- a scan that reads nothing' . ' passes the check above for the wrong reason', - $checked > 50 + count($files) > 50 ); $t->finish(); From 5af60fee71ac70b1d278ca2c916af0df1caa6655 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 19:37:59 +0000 Subject: [PATCH 063/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a591667b71..e5853014cb 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,6 +10359,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 211004c5b3..911205bfd0 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,6 +10368,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ac670c5901..655d6af5ef 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,6 +10527,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c79895d6c2..2070a26faa 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,6 +10360,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da26901cb2..a28aeb921e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,6 +10352,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d7b3169c32..b603f7ed4a 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index dc1ff1b8bf..9476bef60a 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,6 +10030,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 6e57cf07ce..005a44f351 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,6 +8879,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 40818e02f7..87b82f5950 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 782e8c0d39..4b7e8276d9 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 8d6bf6bf22121646e3898779843b62cf3c888d59 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 14:49:30 -0500 Subject: [PATCH 064/117] Drop the getClass PHPStan extension; the literals are gone GetClassReturnTypeExtension resolved getClass('Name') to FOG\\Name so that a method missing from the result was a finding rather than a fatal on a live server -- the DirectoryFacts::row() failure its header records, where getClass('HostDirectoryManager')->find() passed a whole suite, a deploy and a review and then died on the first real poll carrying a directory block. ADR 0043 closed that seam at the source instead: a class named by a literal is spelled at the call site, where the language checks it for free, and tests/getclass-literals.test.php refuses the literal form outright. The extension only ever acted on literals, so it now has nothing to resolve -- phpstan.neon excludes packages/web/lib/plugins, which is where the last of them live. Verified rather than assumed: both passes, before and after, produce byte-identical results (7 and 8 errors, same files, same lines). The extension is dead by construction, not merely redundant. Removed with its wiring -- the services block, and the root autoload-dev entry that existed solely to load it. build/phpstan stays for constants.stub.php, which is a bootstrap file rather than a class, so composer.lock is untouched and no vendored artifact moves. all-classes-load.test.php still excludes build/, and still should, but its comment gave the reason as autoload-dev plus PHPStan interfaces -- both of which this commit deletes. It now says what is actually left there. Refs ADR 0043. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- build/phpstan/GetClassReturnTypeExtension.php | 135 ------------------ composer.json | 5 - .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - phpstan.neon | 9 -- tests/all-classes-load.test.php | 8 +- 14 files changed, 4 insertions(+), 163 deletions(-) delete mode 100644 build/phpstan/GetClassReturnTypeExtension.php diff --git a/build/phpstan/GetClassReturnTypeExtension.php b/build/phpstan/GetClassReturnTypeExtension.php deleted file mode 100644 index 9ef9771f14..0000000000 --- a/build/phpstan/GetClassReturnTypeExtension.php +++ /dev/null @@ -1,135 +0,0 @@ -find()` -- the 1.5 - * API, gone from 1.6's FOGManagerController -- analysed clean and died on a - * live server with "Call to undefined method" (fog-agent poll, 2026-09-03). - * Roughly a hundred call sites in packages/web/src have that shape, so the - * gap is not one line, it is the whole factory. - * - * Resolution mirrors Initiator::srcClassMap() rather than calling it: - * lowercase basename of every packages/web/src//.php maps to - * FOG\\. Plugins (FOG\Plugins\...) are not resolved here -- they - * are not in this repo's analysed paths -- and a name that maps to nothing - * falls through to PHPStan's default, exactly as getClass('DateTime') does at - * runtime. - * - * Registered in phpstan.neon under `services`, autoloaded through the root - * composer.json's autoload-dev (the repo root is never deployed, so nothing - * of this reaches a server). - * - * PHP version 7.4+ - * - * @category GetClassReturnTypeExtension - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -namespace FOG\Build\PhpStan; - -use PhpParser\Node\Expr\StaticCall; -use PHPStan\Analyser\Scope; -use PHPStan\Reflection\MethodReflection; -use PHPStan\Reflection\ReflectionProvider; -use PHPStan\Type\DynamicStaticMethodReturnTypeExtension; -use PHPStan\Type\ObjectType; -use PHPStan\Type\Type; - -/** - * Resolves getClass('Name') to FOG\\Name for PHPStan. - * - * @category GetClassReturnTypeExtension - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -class GetClassReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension -{ - /** @var array lowercase short name => FQCN */ - private $map = []; - - /** @var ReflectionProvider */ - private $reflectionProvider; - - /** - * Builds the short-name map once, from the tree on disk. - * - * @param ReflectionProvider $reflectionProvider PHPStan's class registry - */ - public function __construct(ReflectionProvider $reflectionProvider) - { - $this->reflectionProvider = $reflectionProvider; - $src = dirname(__DIR__, 2) . '/packages/web/src'; - foreach (glob($src . '/*/*.php') ?: [] as $path) { - $short = strtolower(basename($path, '.php')); - $this->map[$short] = 'FOG\\' . basename(dirname($path)) . '\\' . basename($path, '.php'); - } - } - - /** - * The class whose static method this extension answers for. Subclasses - * calling self::getClass() resolve to this declaring class, so one - * registration covers every FOGBase descendant. - * - * @return string - */ - public function getClass(): string - { - return \FOG\Base\FOGBase::class; - } - - /** - * @param MethodReflection $methodReflection the method being called - * - * @return bool - */ - public function isStaticMethodSupported(MethodReflection $methodReflection): bool - { - return 'getClass' === $methodReflection->getName(); - } - - /** - * The precise type when the name is a literal and the call is not the - * `$props === true` form (which returns an array); null otherwise, which - * hands back to PHPStan's default. - * - * @param MethodReflection $methodReflection the method being called - * @param StaticCall $methodCall the call node - * @param Scope $scope the analysis scope - * - * @return Type|null - */ - public function getTypeFromStaticMethodCall( - MethodReflection $methodReflection, - StaticCall $methodCall, - Scope $scope - ): ?Type { - $args = $methodCall->getArgs(); - if (count($args) < 1) { - return null; - } - if (isset($args[2])) { - $props = $scope->getType($args[2]->value); - if (!$props->isFalse()->yes()) { - return null; - } - } - $names = $scope->getType($args[0]->value)->getConstantStrings(); - if (1 !== count($names)) { - return null; - } - $short = strtolower(trim($names[0]->getValue())); - if (!isset($this->map[$short])) { - return null; - } - $fqcn = $this->map[$short]; - if (!$this->reflectionProvider->hasClass($fqcn)) { - return null; - } - return new ObjectType($fqcn); - } -} diff --git a/composer.json b/composer.json index 2cbbe45c6b..9c528e1444 100644 --- a/composer.json +++ b/composer.json @@ -13,10 +13,5 @@ "config": { "optimize-autoloader": false, "sort-packages": true - }, - "autoload-dev": { - "psr-4": { - "FOG\\Build\\PhpStan\\": "build/phpstan/" - } } } diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e5853014cb..a591667b71 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,7 +10359,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 911205bfd0..211004c5b3 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,7 +10368,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 655d6af5ef..ac670c5901 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,7 +10527,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2070a26faa..c79895d6c2 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,7 +10360,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a28aeb921e..da26901cb2 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,7 +10352,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b603f7ed4a..d7b3169c32 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,7 +10075,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9476bef60a..dc1ff1b8bf 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,7 +10030,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 005a44f351..6e57cf07ce 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,7 +8879,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 87b82f5950..40818e02f7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 4b7e8276d9..782e8c0d39 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/phpstan.neon b/phpstan.neon index 54172fc857..694ee42977 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -120,14 +120,5 @@ parameters: # The constants FOG defines at runtime -- see the file's own header. - build/phpstan/constants.stub.php -services: - # Resolves getClass('Name') to its FOG class so a method that does not - # exist on the result is a finding, not a fatal on a live server. See - # the extension's header for the failure it closes. - - - class: FOG\Build\PhpStan\GetClassReturnTypeExtension - tags: - - phpstan.broker.dynamicStaticMethodReturnTypeExtension - includes: - phpstan-baseline.neon diff --git a/tests/all-classes-load.test.php b/tests/all-classes-load.test.php index f9175bb712..465c218fa0 100644 --- a/tests/all-classes-load.test.php +++ b/tests/all-classes-load.test.php @@ -214,10 +214,10 @@ function ($f) { && is_readable($f) && 0 !== strpos($f, 'packages/web/vendor/') && 0 !== strpos($f, 'tests/') - // Analysis tooling, not product: build/ is loaded by the - // root composer autoload-dev and implements PHPStan - // interfaces that exist only under the root vendor/. FOG's - // own autoloader cannot declare it and is not meant to. + // Analysis tooling, not product. build/ now holds only + // constants.stub.php, which PHPStan loads as a bootstrap + // file and which declares no class at all -- FOG's own + // autoloader has nothing to find there and is not meant to. && 0 !== strpos($f, 'build/'); } ); From 7c43066676d212b373cc6fb48b3b2cc892295b2f Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 19:50:36 +0000 Subject: [PATCH 065/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a591667b71..e5853014cb 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,6 +10359,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 211004c5b3..911205bfd0 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,6 +10368,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ac670c5901..655d6af5ef 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,6 +10527,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c79895d6c2..2070a26faa 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,6 +10360,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da26901cb2..a28aeb921e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,6 +10352,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d7b3169c32..b603f7ed4a 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index dc1ff1b8bf..9476bef60a 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,6 +10030,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 6e57cf07ce..005a44f351 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,6 +8879,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 40818e02f7..87b82f5950 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 782e8c0d39..4b7e8276d9 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 913784de5b03140961b99dab5fe7b5ff652b8971 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:08:02 -0500 Subject: [PATCH 066/117] Count the ten new schema steps in the baselined $this pattern The branch adds schema steps 419 through 423 and their siblings, taking `$this` in commons/schema.php from 389 uses to 399. The baseline pins `Variable $this might not be defined` at 392 occurrences, and PHPStan fails a baselined pattern that occurs MORE often than pinned -- so the count itself became an error, and the three unaccounted occurrences were reported on top of it, hiding the errors that actually matter behind four lines of noise. Bump the count rather than regenerate the baseline, which would sweep up unrelated drift. Pass 1 goes from 7 errors to 3; the 3 that remain are real and are in the new feature code. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- .../management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/management/languages/messages.pot | 1 - .../management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - phpstan-baseline.neon | 2 +- 11 files changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e5853014cb..a591667b71 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,7 +10359,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 911205bfd0..211004c5b3 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,7 +10368,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 655d6af5ef..ac670c5901 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,7 +10527,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2070a26faa..c79895d6c2 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,7 +10360,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a28aeb921e..da26901cb2 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,7 +10352,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b603f7ed4a..d7b3169c32 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,7 +10075,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9476bef60a..dc1ff1b8bf 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,7 +10030,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 005a44f351..6e57cf07ce 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,7 +8879,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 87b82f5950..40818e02f7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 4b7e8276d9..782e8c0d39 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4745544ee2..d7faa97337 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -129,7 +129,7 @@ parameters: - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 392 + count: 395 path: packages/web/commons/schema.php - From 9773f5d59e7813ef7aeedf169235ddb22c77b8de Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 20:09:08 +0000 Subject: [PATCH 067/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a591667b71..e5853014cb 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,6 +10359,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 211004c5b3..911205bfd0 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,6 +10368,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ac670c5901..655d6af5ef 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,6 +10527,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c79895d6c2..2070a26faa 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,6 +10360,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da26901cb2..a28aeb921e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,6 +10352,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d7b3169c32..b603f7ed4a 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index dc1ff1b8bf..9476bef60a 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,6 +10030,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 6e57cf07ce..005a44f351 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,6 +8879,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 40818e02f7..87b82f5950 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 782e8c0d39..4b7e8276d9 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From d68014da121f0286be002105e840e4bb996583e6 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:26:17 -0500 Subject: [PATCH 068/117] Printers: record what a host actually has (design 0010, schema 426) FOG has never recorded which printers a machine has. Both legacy platform managers had a GetPrinters() and neither ever transmitted the result -- all three call sites are local decisions inside PrinterManager.cs -- so "did the printer I assigned actually install?" has had no answer since the feature shipped, and an install that fails is retried identically every poll, forever, silently. Two tables, not one. hostSpooler is the per-host anchor: which print subsystem the machine runs and when it last said so. A machine with CUPS and no queues has ANSWERED, and a report that could only see hostPrinter rows would show it as never having reported -- the host most worth looking at being the one that vanishes from the page. hostFactState records the same "when did this host last report kind X", but that is the poll's hash cache, and an admin-facing report built on it would break the next time the gate changes. hostPrinter holds a queue as a device URI plus a driver, which is how both spoolers already describe one (0010 section 2). That is what lets a Windows row and a CUPS row for the same physical device be recognized as the same device; pConfig never could, because it named a code path rather than a printer -- and three of its four values throw NotImplementedException on whichever platform the machine is actually running. printerAssoc gains paAppliedAt and paError, so a failed install has somewhere to live. Named for the ATTEMPT, per the hdPlacementAt lesson from step 424. Also in this step, and each one bit: - paIsDefault becomes tinyint(1) to match groupPrinterAssoc.gpaIsDefault. The UPDATE has to come first: the column holds '' on every row nobody ever set, and MariaDB in strict mode refuses to convert '' to an integer, so a bare MODIFY would fail the upgrade on essentially every existing install rather than on none of them. - pAnon2-pAnon5 and paAnon1-paAnon5 are dropped. Audited across the whole tree first, and the audit found real readers that a grep of src/ alone would have missed: four hidden DataTables export columns in fog.printer.export.js and a test fixture. Both updated here. That drop needed a tool that did not exist. schema-manifest could declare a retired TABLE but had no way to declare a retired COLUMN, so the nine drops showed as permanent MISSING COLUMN lines in the 1.5 comparison -- which trains whoever reads that output to skim past differences, and the next real one goes with it. `retired` now takes an optional `column`, reported rather than silenced exactly as a retired table is, and it will be needed again the moment design 0007 normalizes the inventory columns. The gate for it assembles schema.php's statements with token_get_all rather than grepping the raw source: a statement is written as a chain of string literals, so the table name and the column name are never in the same one, and a plain search cannot tell "printerAssoc drops paAnon2" from "some other table drops paAnon2". `plugins` has a pAnon2 that was renamed and never dropped, so that distinction is not hypothetical -- and moving the drop onto plugins is one of the two mutations this gate was made to fail on. Nine mutations run against agent-printer-facts, all caught, including the one worth naming: skipping the spooler row when a host reports no queues, which is the invisible-absence failure and shows up nowhere until someone goes looking for a host that is not on the page. Suite: 329 passed, 1 failed -- certificate-table.test.php, which fails on working-1.6 too and is untouched here. Noted and left alone: Items/Printer.php:179-180 assigns $curtype and immediately overwrites it. Pre-existing, unrelated to this change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- bin/psr4-scan.php | 5 + bin/schema-manifest.php | 36 +- ...l-integrity-is-declared-in-the-database.md | 2 +- docs/development/foreign-keys.md | 2 +- packages/web/commons/schema-constraints.php | 2 + packages/web/commons/schema-expected.php | 84 ++++- packages/web/commons/schema.php | 83 +++++ .../js/fog/printer/fog.printer.export.js | 4 - .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Agent/PrinterFacts.php | 335 ++++++++++++++++++ packages/web/src/Agent/State.php | 1 + packages/web/src/Auth/Authorization.php | 2 + packages/web/src/Base/System.php | 2 +- packages/web/src/Items/HostPrinter.php | 96 +++++ packages/web/src/Items/HostSpooler.php | 87 +++++ packages/web/src/Items/Printer.php | 6 +- packages/web/src/Items/PrinterAssociation.php | 7 +- .../web/src/Managers/HostPrinterManager.php | 35 ++ .../web/src/Managers/HostSpoolerManager.php | 35 ++ packages/web/src/Router/Route.php | 2 + tests/agent-printer-facts.test.php | 302 ++++++++++++++++ tests/fixtures/route-cascade-contract.txt | 2 + tests/fixtures/route-column-contract.txt | 28 +- tests/foreign-key-map.test.php | 2 + .../printer-grants-reach-the-client.test.php | 4 - tests/schema-retired-tables.test.php | 85 +++++ 35 files changed, 1203 insertions(+), 56 deletions(-) create mode 100644 packages/web/src/Agent/PrinterFacts.php create mode 100644 packages/web/src/Items/HostPrinter.php create mode 100644 packages/web/src/Items/HostSpooler.php create mode 100644 packages/web/src/Managers/HostPrinterManager.php create mode 100644 packages/web/src/Managers/HostSpoolerManager.php create mode 100644 tests/agent-printer-facts.test.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index f1504c5f6a..3b69f60545 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -224,6 +224,11 @@ // hostUserSession rows, it is not either of those rows. 'DirectoryFacts' => 'Agent', 'DirectoryPlacement' => 'Agent', + // The writer for what an agent reports about its installed printers + // (design 0010). Same naming reason: Printer is already an Items + // class for the assignable printer -- this writes hostPrinter and + // hostSpooler rows, it is not that row. + 'PrinterFacts' => 'Agent', 'UserSessions' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', diff --git a/bin/schema-manifest.php b/bin/schema-manifest.php index 99d8105bb8..2dad7ddc3e 100644 --- a/bin/schema-manifest.php +++ b/bin/schema-manifest.php @@ -419,15 +419,29 @@ function render($value, $indent = 1) $A[$t][$i] = $to; } } - // Tables the NEW side declares it dropped on purpose. Keyed lowercase so - // the lookup matches the comparison, which is case-insensitive because - // MySQL's own table-name casing depends on the server's filesystem. + // Tables and columns the NEW side declares it dropped on purpose. Keyed + // lowercase so the lookup matches the comparison, which is + // case-insensitive because MySQL's own table-name casing depends on the + // server's filesystem. + // + // An entry with a `column` retires that one column and leaves the table + // alone; without one it retires the whole table. Both exist because a + // rebuild drops both kinds, and the alternative to declaring a dropped + // column is a permanent difference in this output -- which trains + // whoever reads it to skim past differences, and the next real one goes + // with it. $retired = []; + $retiredCols = []; foreach ((array)($b['retired'] ?? []) as $r) { $t = strtolower($r['table'] ?? ''); if (!$t) { continue; } + $c = strtolower((string)($r['column'] ?? '')); + if ('' !== $c) { + $retiredCols[$t . '.' . $c] = (string)($r['reason'] ?? ''); + continue; + } $retired[$t] = (string)($r['reason'] ?? ''); } @@ -451,7 +465,21 @@ function render($value, $indent = 1) } $gone = array_diff($cols, $B[$table]); $added = array_diff($B[$table], $cols); - foreach ($gone as $c) { + foreach ($gone as $i => $c) { + if (isset($retiredCols[$table . '.' . $c])) { + // Reported, not silenced, exactly as for a retired table. + printf( + "RETIRED COLUMN %s.%s -- %s\n", + $table, + $c, + $retiredCols[$table . '.' . $c] ?: 'no reason recorded' + ); + // Dropped from $gone as well, so the rename heuristic below + // does not then read a deliberate drop plus an unrelated + // addition as one renamed column. + unset($gone[$i]); + continue; + } printf("MISSING COLUMN %s.%s\n", $table, $c); $found++; } diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index f1bcc5ca36..abd2b374bb 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**119 of the map's 134 relationships are declared.** The other 15 are not +**121 of the map's 136 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index 5907e4a59e..cdbd45260c 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 134 relationships live in them. +the survey and 25 of the map's 136 relationships live in them. ### Direction is the whole rule diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index 61c4dfb0e1..8030d396a5 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -352,6 +352,8 @@ ['child' => 'hostUserSession', 'column' => 'husHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'hostFactState', 'column' => 'hfsHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'hostDirectory', 'column' => 'hdHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostPrinter', 'column' => 'hpHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostSpooler', 'column' => 'hspHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'ldapUserGrant', 'column' => 'lugTargetID', 'parent' => '(lugTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ['child' => 'oidcUserGrant', 'column' => 'ougTargetID', 'parent' => '(ougTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ]; diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 9a978b59fa..71ca84ec62 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -72,6 +72,51 @@ 'table' => 'imagingLog', 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging run now, so the table was retired rather than ported', ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon1', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon2', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon3', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon4', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printerAssoc', + 'column' => 'paAnon5', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon2', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon3', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon4', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], + [ + 'table' => 'printers', + 'column' => 'pAnon5', + 'reason' => 'design 0010 -- a spare column 1.5 pre-allocated and nothing has ever written; `plugins` claimed three of its own the same way through the `renames` block above, these were never claimed. Schema step 426 has the audit that preceded the drop', + ], [ 'table' => 'virus', 'reason' => 'GH-328 -- the ClamAV scan is removed. 1.6 never carried service/av.php across from 1.5, so nothing on this branch has ever written the table and no model, manager, report or page reads it', @@ -435,6 +480,19 @@ 'hostAgentCheckin' => 'datetime DEFAULT NULL', ], ], + 'hostPrinter' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostPrinter` ( `hpID` int(11) NOT NULL AUTO_INCREMENT, `hpHostID` int(11) NOT NULL, `hpName` varchar(255) NOT NULL DEFAULT \'\', `hpURI` varchar(1024) NOT NULL DEFAULT \'\', `hpDriver` varchar(255) NOT NULL DEFAULT \'\', `hpDefault` tinyint(1) NOT NULL DEFAULT 0, `hpShared` tinyint(1) NOT NULL DEFAULT 0, `hpObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hpID`), UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), KEY `hpName` (`hpName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hpID' => 'int(11) NOT NULL', + 'hpHostID' => 'int(11) NOT NULL', + 'hpName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', + 'hpDriver' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpDefault' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpShared' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpObservedAt' => 'datetime DEFAULT NULL', + ], + ], 'hostScreenSettings' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hostScreenSettings` ( `hssID` int(11) NOT NULL AUTO_INCREMENT, `hssHostID` int(11) NOT NULL, `hssWidth` int(11) NOT NULL DEFAULT 0, `hssHeight` int(11) NOT NULL DEFAULT 0, `hssRefresh` int(11) NOT NULL DEFAULT 0, `hssOrientation` int(11) NOT NULL DEFAULT 0, `hssOther1` int(11) NOT NULL DEFAULT 0, `hssOther2` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`hssID`), UNIQUE KEY `hssHostID` (`hssHostID`), KEY `new_index` (`hssHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -464,6 +522,15 @@ 'hsRemovedAt' => 'datetime DEFAULT NULL', ], ], + 'hostSpooler' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostSpooler` ( `hspID` int(11) NOT NULL AUTO_INCREMENT, `hspHostID` int(11) NOT NULL, `hspSubsystem` varchar(16) NOT NULL DEFAULT \'\', `hspObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hspID`), UNIQUE KEY `hspHostID` (`hspHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hspID' => 'int(11) NOT NULL', + 'hspHostID' => 'int(11) NOT NULL', + 'hspSubsystem' => 'varchar(16) NOT NULL DEFAULT \'\'', + 'hspObservedAt' => 'datetime DEFAULT NULL', + ], + ], 'hostUserSession' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hostUserSession` ( `husID` int(11) NOT NULL AUTO_INCREMENT, `husHostID` int(11) NOT NULL, `husSessionKey` varchar(191) NOT NULL DEFAULT \'\', `husUserName` varchar(255) NOT NULL DEFAULT \'\', `husDomain` varchar(255) NOT NULL DEFAULT \'\', `husUserSID` varchar(191) NOT NULL DEFAULT \'\', `husType` varchar(32) NOT NULL DEFAULT \'\', `husState` varchar(32) NOT NULL DEFAULT \'\', `husRemoteHost` varchar(255) NOT NULL DEFAULT \'\', `husStartedAt` datetime NOT NULL, `husEndedAt` datetime DEFAULT NULL, `husEndReason` varchar(32) NOT NULL DEFAULT \'\', `husLastSeen` datetime DEFAULT NULL, PRIMARY KEY (`husID`), UNIQUE KEY `husHostKeyStart` (`husHostID`,`husSessionKey`,`husStartedAt`), KEY `husHostOpen` (`husHostID`,`husEndedAt`), KEY `husUserName` (`husUserName`), KEY `husStartedAt` (`husStartedAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -747,21 +814,18 @@ ], ], 'printerAssoc' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `printerAssoc` ( `paID` int(11) NOT NULL AUTO_INCREMENT, `paHostID` int(11) NOT NULL, `paPrinterID` int(11) NOT NULL, `paIsDefault` varchar(2) NOT NULL DEFAULT \'\', `paAnon1` varchar(2) NOT NULL DEFAULT \'\', `paAnon2` varchar(2) NOT NULL DEFAULT \'\', `paAnon3` varchar(2) NOT NULL DEFAULT \'\', `paAnon4` varchar(2) NOT NULL DEFAULT \'\', `paAnon5` varchar(2) NOT NULL DEFAULT \'\', PRIMARY KEY (`paID`), UNIQUE KEY `paHostID` (`paHostID`,`paPrinterID`), KEY `new_index1` (`paHostID`), KEY `new_index2` (`paPrinterID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `printerAssoc` ( `paID` int(11) NOT NULL AUTO_INCREMENT, `paHostID` int(11) NOT NULL, `paPrinterID` int(11) NOT NULL, `paIsDefault` tinyint(1) NOT NULL DEFAULT 0, `paAppliedAt` datetime DEFAULT NULL, `paError` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`paID`), UNIQUE KEY `paHostID` (`paHostID`,`paPrinterID`), KEY `new_index1` (`paHostID`), KEY `new_index2` (`paPrinterID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'paID' => 'int(11) NOT NULL', 'paHostID' => 'int(11) NOT NULL', 'paPrinterID' => 'int(11) NOT NULL', - 'paIsDefault' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon1' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon2' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon3' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon4' => 'varchar(2) NOT NULL DEFAULT \'\'', - 'paAnon5' => 'varchar(2) NOT NULL DEFAULT \'\'', + 'paIsDefault' => 'tinyint(1) NOT NULL DEFAULT 0', + 'paAppliedAt' => 'datetime DEFAULT NULL', + 'paError' => 'varchar(255) NOT NULL DEFAULT \'\'', ], ], 'printers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pAnon2` varchar(10) NOT NULL DEFAULT \'\', `pAnon3` varchar(10) NOT NULL DEFAULT \'\', `pAnon4` varchar(10) NOT NULL DEFAULT \'\', `pAnon5` varchar(10) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'pID' => 'int(11) NOT NULL', 'pPort' => 'longtext NOT NULL DEFAULT \'\'', @@ -771,10 +835,6 @@ 'pConfig' => 'varchar(10) NOT NULL DEFAULT \'\'', 'pConfigFile' => 'varchar(255) NOT NULL DEFAULT \'\'', 'pIP' => 'varchar(255) NOT NULL DEFAULT \'\'', - 'pAnon2' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon3' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon4' => 'varchar(10) NOT NULL DEFAULT \'\'', - 'pAnon5' => 'varchar(10) NOT NULL DEFAULT \'\'', 'pDesc' => 'longtext DEFAULT NULL', ], ], diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index f647b3d21c..0b11d31746 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -11248,3 +11248,86 @@ function () { . "domain rights. (Valid values: a password).' " . "WHERE `settingKey` = 'FOG_DIRECTORY_BIND_PASSWORD'", ]; + +// 426 +$this->schema[] = [ + // Design 0010: FOG has never recorded what printers a machine actually + // has. Both legacy platform managers had a GetPrinters() and neither + // ever transmitted the result -- all three call sites are local + // decisions inside PrinterManager.cs -- so "did the printer I assigned + // actually install?" has had no answer since the feature shipped. + // + // Two tables rather than one. hostSpooler is the per-host anchor: which + // print subsystem the machine runs, and when it last said so. It exists + // separately from hostPrinter because a machine with CUPS and no queues + // has REPORTED, and a report that could only see hostPrinter rows would + // show that host as never having answered -- which is precisely the + // invisible-absence failure design 0010 section 6 is built to avoid. + // + // hostFactState already records the same "when did this host last + // report kind X", but that table is the poll's hash cache. A report + // built on it would couple an admin-facing page to the protocol's + // internal bookkeeping, and would break the next time the gate changes. + "CREATE TABLE IF NOT EXISTS `hostSpooler` ( " + . "`hspID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hspHostID` int(11) NOT NULL, " + . "`hspSubsystem` varchar(16) NOT NULL DEFAULT '', " + . "`hspObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hspID`), " + . "UNIQUE KEY `hspHostID` (`hspHostID`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // One row per queue observed on a host. + // + // hpURI is the load-bearing column and the whole of design 0010 section + // 2: both spoolers already describe a printer as a device URI plus a + // driver, so recording the URI is what lets a Windows row and a CUPS row + // for the same physical device be recognized as the same device. FOG's + // pConfig could never do that -- it named a code path, not a printer. + // + // hpDriver empty is a real value meaning driverless (IPP Everywhere), + // which FOG's existing model has no way to express at all. + "CREATE TABLE IF NOT EXISTS `hostPrinter` ( " + . "`hpID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hpHostID` int(11) NOT NULL, " + . "`hpName` varchar(255) NOT NULL DEFAULT '', " + . "`hpURI` varchar(1024) NOT NULL DEFAULT '', " + . "`hpDriver` varchar(255) NOT NULL DEFAULT '', " + . "`hpDefault` tinyint(1) NOT NULL DEFAULT 0, " + . "`hpShared` tinyint(1) NOT NULL DEFAULT 0, " + . "`hpObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hpID`), " + . "UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), " + . "KEY `hpName` (`hpName`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Where a failed install gets to live. Today a printer that will not + // install produces nothing an admin can see: the client retries the same + // thing every poll, forever, silently. paAppliedAt is named for the + // ATTEMPT, not the success -- the hdPlacementAt lesson from step 424. + "ALTER TABLE `printerAssoc` " + . "ADD COLUMN `paAppliedAt` datetime DEFAULT NULL, " + . "ADD COLUMN `paError` varchar(255) NOT NULL DEFAULT ''", + // paIsDefault is a varchar(2) holding a boolean; groupPrinterAssoc's + // gpaIsDefault is a tinyint(1) holding the same idea. Same concept, two + // types, because they were added years apart. + // + // The UPDATE has to come first. The column holds '' on every row nobody + // ever set, and MariaDB in strict mode refuses to convert '' to an + // integer -- so a bare MODIFY fails the upgrade on essentially every + // existing install rather than on none of them. + "UPDATE `printerAssoc` SET `paIsDefault`='0' " + . "WHERE `paIsDefault` NOT IN ('0','1')", + "ALTER TABLE `printerAssoc` " + . "MODIFY COLUMN `paIsDefault` tinyint(1) NOT NULL DEFAULT 0", + // The pre-allocated spare columns. `plugins` had the same pAnon1-pAnon5 + // and they were renamed into real columns (pIcon, pRunfile, pLocation) + // through schema-expected.php's `renames` block; the printer ones were + // never claimed by anything, in ten years. Verified across the whole + // tree before dropping: the only readers were the two Items field maps + // and four hidden DataTables export columns, all updated in this change. + "ALTER TABLE `printerAssoc` " + . "DROP COLUMN `paAnon1`, DROP COLUMN `paAnon2`, DROP COLUMN `paAnon3`, " + . "DROP COLUMN `paAnon4`, DROP COLUMN `paAnon5`", + "ALTER TABLE `printers` " + . "DROP COLUMN `pAnon2`, DROP COLUMN `pAnon3`, DROP COLUMN `pAnon4`, " + . "DROP COLUMN `pAnon5`", +]; diff --git a/packages/web/management/js/fog/printer/fog.printer.export.js b/packages/web/management/js/fog/printer/fog.printer.export.js index 8ead4332a1..5730f64d60 100644 --- a/packages/web/management/js/fog/printer/fog.printer.export.js +++ b/packages/web/management/js/fog/printer/fog.printer.export.js @@ -8,10 +8,6 @@ {data: 'config'}, {data: 'configFile', visible: false}, {data: 'ip', visible: false}, - {data: 'pAnon2', visible: false}, - {data: 'pAnon3', visible: false}, - {data: 'pAnon4', visible: false}, - {data: 'pAnon5', visible: false}, {data: 'associations', visible: false} ]); })(jQuery); diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e5853014cb..a591667b71 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,7 +10359,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 911205bfd0..211004c5b3 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,7 +10368,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 655d6af5ef..ac670c5901 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,7 +10527,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2070a26faa..c79895d6c2 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,7 +10360,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a28aeb921e..da26901cb2 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,7 +10352,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b603f7ed4a..d7b3169c32 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,7 +10075,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9476bef60a..dc1ff1b8bf 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,7 +10030,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 005a44f351..6e57cf07ce 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,7 +8879,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 87b82f5950..40818e02f7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 4b7e8276d9..782e8c0d39 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,7 +10355,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/PrinterFacts.php b/packages/web/src/Agent/PrinterFacts.php new file mode 100644 index 0000000000..54ed34edea --- /dev/null +++ b/packages/web/src/Agent/PrinterFacts.php @@ -0,0 +1,335 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; + +/** + * Reconciles a reported printer block into `hostSpooler` and `hostPrinter` + * (design 0010 sections 3 and 4). + * + * A fact report like InventoryFacts, and registered the same way: an entry + * in State::FACT_REPORTS and a block in the poll, never a route of its own + * (the route rule, protocol-v1.md). + * + * The contrast to draw is with `printerAssoc` next door: that is what an + * admin ASSIGNED. This is what the machine says it actually HAS. FOG has + * had the first since 1.x and has never had the second, so an install that + * failed has always failed silently and the client has always retried the + * same thing on the next poll, forever. + * + * What it does NOT do is act on the difference. Deciding that an assigned + * printer is missing is the report's job (design 0010 section 6), and + * installing one is the agent's. This class only records. + * + * @category Printers + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class PrinterFacts extends FOGBase +{ + /** + * Most queues accepted from one host. + * + * A print server can legitimately carry hundreds, so this is generous + * rather than tight. It is here because the list is attacker-controlled + * input from a host that has enrolled: a machine claiming a million + * queues must fail this check rather than the database. + */ + const MAX_PRINTERS = 512; + + /** + * Column widths, so an overlong value is truncated here rather than + * failing the insert under strict mode and costing the host its poll. + * + * The URI is the long one. A CUPS device URI can carry a full IPP path + * plus query parameters, and an smb:// one carries a UNC path an admin + * chose the length of. + */ + const WIDTHS = [ + 'name' => 255, + 'uri' => 1024, + 'driver' => 255 + ]; + + /** + * The subsystems a host may report. + * + * An unrecognized value is stored as the empty string rather than passed + * through, for DirectoryFacts::KINDS' reason: the report groups on it, + * and a host inventing a value would put an uncontrolled string into a + * page an admin reads. + * + * @var string[] + */ + const SUBSYSTEMS = ['cups', 'winspool']; + + /** + * Records the host's current printer set. + * + * The list is complete by contract: any queue currently recorded for + * this host and absent from it is gone. That is why the agent sends no + * block at all when its collector could not run -- an empty list here + * means "this machine has no printers", and would clear every row it + * has (design 0006 section 6). + * + * @param Host $Host the host the certificate bound + * @param array $block the reported printer block + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return void + */ + public static function report(Host $Host, array $block) + { + $list = $block['installed'] ?? []; + if (!is_array($list)) { + $list = []; + } + if (count($list) > self::MAX_PRINTERS) { + throw new \RuntimeException('printer list too large', 413); + } + + $hostID = (int)$Host->get('id'); + $incoming = self::_clean($list, (string)($block['default'] ?? '')); + $now = self::niceDate()->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s'); + + // Replace the set, in one transaction so nothing observes the + // intermediate empty state. Unlike hostSoftware, rows are deleted + // rather than closed: a printer that is gone is gone, and "which + // hosts had this queue in March" is not a question anyone asks. + // The removal itself is in the audit line below. + self::$DB->query('START TRANSACTION'); + try { + $before = self::_currentNames($hostID); + self::$DB->query( + 'DELETE FROM `hostPrinter` WHERE `hpHostID`=:host', + [], + [':host' => $hostID] + ); + self::_insert($hostID, $incoming, $now); + self::_spooler($hostID, (string)($block['subsystem'] ?? ''), $now); + self::$DB->query('COMMIT'); + } catch (\Exception $e) { + self::$DB->query('ROLLBACK'); + throw $e; + } + + $added = array_diff(array_keys($incoming), $before); + $removed = array_diff($before, array_keys($incoming)); + if (empty($added) && empty($removed)) { + // A queue whose driver or default flag moved is a change worth + // storing and not worth a line in the audit log; the set is what + // an admin reads a history for. + return; + } + Audit::record( + [ + 'type' => 'agent.printers', + 'subjectType' => 'host', + 'subjectID' => $hostID, + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'affectedCount' => count($added) + count($removed), + // Named, not counted. A host has single digits of printers, + // so the names fit -- and "Accounts-HP4550 gone" is the line + // an admin is looking for, where "1 removed" sends them + // hunting for which one. + 'text' => substr( + 'agent reported printers: ' + . self::describe($added, $removed), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } + + /** + * Normalizes the reported list, keyed by queue name. + * + * Keying deduplicates: a host reporting the same queue twice would + * otherwise hit the unique index mid-insert and roll back the whole + * poll. A queue with no name is dropped rather than stored as a row + * nothing can act on -- not the report, not a removal, not the admin. + * + * @param array $list the reported queues + * @param string $defaultName the block's default queue name + * + * @return array name => normalized row + */ + private static function _clean(array $list, $defaultName) + { + $defaultName = trim($defaultName); + $out = []; + foreach ($list as $entry) { + if (!is_array($entry)) { + continue; + } + $row = []; + foreach (self::WIDTHS as $field => $width) { + $row[$field] = substr( + trim((string)($entry[$field] ?? '')), + 0, + $width + ); + } + if ('' === $row['name']) { + continue; + } + // The agent reports the default by NAME at the block level, + // not as a flag on each queue. Resolving it here is what keeps + // the stored flag and the reported name from ever disagreeing, + // and it drops a default naming a queue that is not in the list + // for free -- which happens after a removal that did not clear + // the setting, and which no action could resolve. + $row['isDefault'] = ($row['name'] === $defaultName) ? 1 : 0; + $row['shared'] = !empty($entry['shared']) ? 1 : 0; + $out[$row['name']] = $row; + } + + return $out; + } + + /** + * The queue names currently recorded for a host, for the audit line. + * + * @param int $hostID the host + * + * @return string[] + */ + private static function _currentNames($hostID) + { + $rows = self::$DB->query( + 'SELECT `hpName` FROM `hostPrinter` WHERE `hpHostID`=:host', + [], + [':host' => (int)$hostID] + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + $out = []; + foreach ((array)$rows as $row) { + $out[] = (string)($row['hpName'] ?? ''); + } + + return $out; + } + + /** + * Inserts the reported queues. + * + * One statement rather than a row at a time: a print server with two + * hundred queues would otherwise cost two hundred round trips on every + * poll where anything moved. No chunking, unlike SoftwareFacts -- + * MAX_PRINTERS caps the list at 512, which is well inside what a single + * statement and its placeholder count accept. + * + * @param int $hostID the host + * @param array $incoming name => normalized row + * @param string $now the timestamp for this reconcile + * + * @return void + */ + private static function _insert($hostID, array $incoming, $now) + { + if (empty($incoming)) { + return; + } + $values = []; + $binds = []; + $i = 0; + foreach ($incoming as $row) { + // A distinct placeholder name per value rather than reusing one + // for the host id and the timestamp: a real prepared statement + // binds each name once, and a driver that is not emulating them + // rejects the repeat with a bound-parameter count error. + $p = ':r' . $i++ . '_'; + $values[] = '(' . $p . 'h,' . $p . 'n,' . $p . 'u,' . $p . 'v,' + . $p . 'd,' . $p . 's,' . $p . 'o)'; + $binds[$p . 'h'] = (int)$hostID; + $binds[$p . 'n'] = $row['name']; + $binds[$p . 'u'] = $row['uri']; + $binds[$p . 'v'] = $row['driver']; + $binds[$p . 'd'] = $row['isDefault']; + $binds[$p . 's'] = $row['shared']; + $binds[$p . 'o'] = $now; + } + self::$DB->query( + 'INSERT INTO `hostPrinter` ' + . '(`hpHostID`,`hpName`,`hpURI`,`hpDriver`,`hpDefault`,' + . '`hpShared`,`hpObservedAt`) VALUES ' + . implode(',', $values), + [], + $binds + ); + } + + /** + * Upserts the host's spooler row. + * + * Written even when the host reported no queues, and that is the point: + * a machine with CUPS and nothing installed has ANSWERED, and without + * this row the report could not tell it from a machine that has never + * checked in (design 0010 section 6). + * + * @param int $hostID the host + * @param string $subsystem the reported subsystem + * @param string $now the timestamp for this reconcile + * + * @return void + */ + private static function _spooler($hostID, $subsystem, $now) + { + $subsystem = strtolower(trim($subsystem)); + if (!in_array($subsystem, self::SUBSYSTEMS, true)) { + // A host that invents a subsystem gets none, not its own string. + $subsystem = ''; + } + self::$DB->query( + 'INSERT INTO `hostSpooler` ' + . '(`hspHostID`,`hspSubsystem`,`hspObservedAt`) ' + . 'VALUES (:host,:sub,:now) ' + . 'ON DUPLICATE KEY UPDATE ' + . '`hspSubsystem`=VALUES(`hspSubsystem`),' + . '`hspObservedAt`=VALUES(`hspObservedAt`)', + [], + [':host' => (int)$hostID, ':sub' => $subsystem, ':now' => $now] + ); + } + + /** + * One line naming what changed, for the audit entry. + * + * @param string[] $added queues that appeared + * @param string[] $removed queues that went away + * + * @return string + */ + private static function describe(array $added, array $removed) + { + $parts = []; + if (!empty($added)) { + $parts[] = 'added ' . implode(', ', $added); + } + if (!empty($removed)) { + $parts[] = 'removed ' . implode(', ', $removed); + } + + return implode('; ', $parts); + } +} diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 7d2f1829a3..4151169a85 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -105,6 +105,7 @@ class State extends FOGBase 'inventory' => InventoryFacts::class, 'software' => SoftwareFacts::class, 'directory' => DirectoryFacts::class, + 'printers' => PrinterFacts::class, ]; /** diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index c999b94a85..2419037daa 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -437,6 +437,8 @@ class Authorization extends FOGBase // same reason: they are host detail, not a feature of their own. 'hostsoftware' => 'host', 'hostdirectory' => 'host', + 'hostprinter' => 'host', + 'hostspooler' => 'host', 'hostusersession' => 'host', 'hostfactstate' => 'host', 'software' => 'software', diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index e48dce896c..da22d88884 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 425); + define('FOG_SCHEMA', 426); define('FOG_BCACHE_VER', 360); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Items/HostPrinter.php b/packages/web/src/Items/HostPrinter.php new file mode 100644 index 0000000000..21c2191a28 --- /dev/null +++ b/packages/web/src/Items/HostPrinter.php @@ -0,0 +1,96 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * A printer a host reports having installed (design 0010). + * + * The contrast to draw is with `printerAssoc`, which this does not replace: + * that is INTENT, the printers an admin assigned. This is OBSERVATION, the + * queues the machine says it actually has. FOG has recorded the first since + * 1.x and has never recorded the second, which is why "did it install?" has + * had no answer. + * + * A printer is a URI and a driver (design 0010 section 2), because that is + * how both spoolers already describe one. `uri` is what makes a row portable + * between platforms; FOG's `pConfig` never could, because it named a code + * path rather than a device. + * + * One row per host per queue, and the set is replaced on each report. Not a + * history: unlike `hostSoftware`, where "which hosts had log4j in March" is + * the question the table exists for, a printer that is gone is simply gone, + * and the removal itself is in the audit log. + * + * @category Printers + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostPrinter extends FOGController +{ + /** + * The hostPrinter table. + * + * @var string + */ + protected $databaseTable = 'hostPrinter'; + /** + * The hostPrinter fields and common names. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'hpID', + 'hostID' => 'hpHostID', + 'name' => 'hpName', + 'uri' => 'hpURI', + 'driver' => 'hpDriver', + 'isDefault' => 'hpDefault', + 'shared' => 'hpShared', + 'observedAt' => 'hpObservedAt' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'hostID', + 'name' + ]; + /** + * Additional fields. + * + * @var array + */ + protected $additionalFields = [ + 'host' + ]; + + /** + * Return the associated host object. + * + * @return object + */ + public function getHost() + { + if (!array_key_exists('host', $this->data)) { + $this->set('host', new Host($this->get('hostID'))); + } + return $this->get('host'); + } +} diff --git a/packages/web/src/Items/HostSpooler.php b/packages/web/src/Items/HostSpooler.php new file mode 100644 index 0000000000..efae86393e --- /dev/null +++ b/packages/web/src/Items/HostSpooler.php @@ -0,0 +1,87 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * The print subsystem a host reports running (design 0010). + * + * One row per host, replaced in place. It is the per-host anchor for the + * printer report: a machine with CUPS and no queues has ANSWERED, and a + * report that could only see `hostPrinter` rows would show that host as + * never having reported -- the invisible-absence failure design 0010 + * section 6 exists to avoid. + * + * It also carries the fact FOG's `pConfig` column was trying to hold and + * could not (design 0010 section 1.1). `pConfig` asked an ADMIN to pick + * between "Local", "Network", "iPrint" and "Cups" -- four code paths, three + * of which throw on whichever platform the machine is actually running -- + * for a device that has no opinion on the matter. The subsystem is a fact + * about the machine, so the machine reports it. + * + * @category Printers + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostSpooler extends FOGController +{ + /** + * The hostSpooler table. + * + * @var string + */ + protected $databaseTable = 'hostSpooler'; + /** + * The hostSpooler fields and common names. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'hspID', + 'hostID' => 'hspHostID', + 'subsystem' => 'hspSubsystem', + 'observedAt' => 'hspObservedAt' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'hostID' + ]; + /** + * Additional fields. + * + * @var array + */ + protected $additionalFields = [ + 'host' + ]; + /** + * Return the associated host object. + * + * @return object + */ + public function getHost() + { + if (!array_key_exists('host', $this->data)) { + $this->set('host', new Host($this->get('hostID'))); + } + return $this->get('host'); + } +} diff --git a/packages/web/src/Items/Printer.php b/packages/web/src/Items/Printer.php index fedaf63eae..2eefbc5221 100644 --- a/packages/web/src/Items/Printer.php +++ b/packages/web/src/Items/Printer.php @@ -48,11 +48,7 @@ class Printer extends FOGController 'model' => 'pModel', 'config' => 'pConfig', 'configFile' => 'pConfigFile', - 'ip' => 'pIP', - 'pAnon2' => 'pAnon2', - 'pAnon3' => 'pAnon3', - 'pAnon4' => 'pAnon4', - 'pAnon5' => 'pAnon5' + 'ip' => 'pIP' ]; /** * The required fields diff --git a/packages/web/src/Items/PrinterAssociation.php b/packages/web/src/Items/PrinterAssociation.php index 4a36bff23a..53b05db8e2 100644 --- a/packages/web/src/Items/PrinterAssociation.php +++ b/packages/web/src/Items/PrinterAssociation.php @@ -42,11 +42,8 @@ class PrinterAssociation extends FOGController 'hostID' => 'paHostID', 'printerID' => 'paPrinterID', 'isDefault' => 'paIsDefault', - 'anon1' => 'paAnon1', - 'anon2' => 'paAnon2', - 'anon3' => 'paAnon3', - 'anon4' => 'paAnon4', - 'anon5' => 'paAnon5' + 'appliedAt' => 'paAppliedAt', + 'error' => 'paError' ]; /** * The required fields. diff --git a/packages/web/src/Managers/HostPrinterManager.php b/packages/web/src/Managers/HostPrinterManager.php new file mode 100644 index 0000000000..9e990ed7b9 --- /dev/null +++ b/packages/web/src/Managers/HostPrinterManager.php @@ -0,0 +1,35 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * The hostPrinter manager. + * + * @category Printers + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostPrinterManager extends FOGManagerController +{ + /** + * The base table name. + * + * @var string + */ + public $tablename = 'hostPrinter'; +} diff --git a/packages/web/src/Managers/HostSpoolerManager.php b/packages/web/src/Managers/HostSpoolerManager.php new file mode 100644 index 0000000000..dc9e45566a --- /dev/null +++ b/packages/web/src/Managers/HostSpoolerManager.php @@ -0,0 +1,35 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * The hostSpooler manager. + * + * @category Printers + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostSpoolerManager extends FOGManagerController +{ + /** + * The base table name. + * + * @var string + */ + public $tablename = 'hostSpooler'; +} diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 9b296c2dde..a330a5e539 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -660,6 +660,8 @@ class Route extends FOGBase 'hostscreensetting', 'hostsoftware', 'hostdirectory', + 'hostprinter', + 'hostspooler', 'hostusersession', 'image', 'imageassociation', diff --git a/tests/agent-printer-facts.test.php b/tests/agent-printer-facts.test.php new file mode 100644 index 0000000000..c463294bc4 --- /dev/null +++ b/tests/agent-printer-facts.test.php @@ -0,0 +1,302 @@ +setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +/** + * A host that needs no database to answer for itself. + * + * @param int $id the host id + * + * @return \FOG\Items\Host + */ +function pfHost($id) +{ + $Host = new \FOG\Items\Host(); + $Host->set('id', $id)->set('name', 'WS-014'); + + return $Host; +} + +/** + * Run report() against the fake connection, returning the statements and + * the binds each one carried. + * + * @param FogFakeDb $db the fake connection + * @param array $block the reported block + * @param array $current rows _currentNames should see + * + * @return array [statements, binds] + */ +function pfReport($db, array $block, array $current = []) +{ + $db->log = []; + $binds = []; + $db->responder = function ($sql, $params) use (&$binds, $current) { + $binds[] = [$sql, $params]; + if (false !== strpos($sql, 'SELECT `hpName`')) { + return $current; + } + return null; + }; + \FOG\Agent\PrinterFacts::report(pfHost(7), $block); + $db->responder = null; + + return [$db->log, $binds]; +} + +// ------------------------------------------------------------ the whitelist + +$mapped = array_keys( + (function () { + $p = new \ReflectionProperty(\FOG\Items\HostPrinter::class, 'databaseFields'); + $p->setAccessible(true); + return (array)$p->getValue(new \FOG\Items\HostPrinter()); + })() +); +$missing = array_diff( + array_keys(\FOG\Agent\PrinterFacts::WIDTHS), + $mapped +); +$t->check( + 'every width names a real HostPrinter property' + . ($missing ? ': ' . implode(', ', $missing) : ''), + empty($missing) +); + +// ----------------------------------------------------------- normalization + +$clean = pf( + '_clean', + [ + [ + ['name' => 'Accounts', 'uri' => 'socket://h:9100', 'driver' => 'd'], + // Same name twice. Without the keying this hits the unique index + // mid-insert and rolls back the whole poll. + ['name' => 'Accounts', 'uri' => 'socket://other:9100'], + // No name: nothing can act on it -- not the report, not a + // removal, not the admin. + ['name' => ' ', 'uri' => 'ipp://x/'], + ['name' => 'Reception', 'uri' => 'ipp://p/ipp/print', 'driver' => ''], + 'not an array', + ], + 'Reception', + ] +); +$t->check( + 'a queue reported twice is stored once', + 2 === count($clean) && isset($clean['Accounts'], $clean['Reception']) +); +$t->check( + 'a queue with no name is dropped', + !isset($clean[' ']) && !isset($clean['']) +); +$t->check( + 'the default is resolved from the block-level name', + 1 === $clean['Reception']['isDefault'] + && 0 === $clean['Accounts']['isDefault'] +); +$t->check( + 'an empty driver survives, because empty means driverless', + '' === $clean['Reception']['driver'] +); + +$clean = pf('_clean', [[['name' => 'Accounts']], 'Gone']); +$t->check( + 'a default naming a queue that is not installed sets no flag', + 0 === $clean['Accounts']['isDefault'] +); + +$long = pf( + '_clean', + [[['name' => str_repeat('n', 400), 'uri' => str_repeat('u', 2000)]], ''] +); +$row = array_shift($long); +$t->check( + 'an overlong value is truncated here rather than failing the insert', + 255 === strlen($row['name']) && 1024 === strlen($row['uri']) +); + +// -------------------------------------------------------------- the writes + +list($log, $binds) = pfReport( + $db, + [ + 'subsystem' => 'cups', + 'default' => 'Accounts', + 'installed' => [ + ['name' => 'Accounts', 'uri' => 'socket://h:9100', 'driver' => 'd'], + ['name' => 'Reception', 'uri' => 'ipp://p/', 'driver' => ''], + ], + ], + [['hpName' => 'Old']] +); +$joined = implode("\n", $log); + +$t->check( + 'the replace runs inside a transaction', + false !== strpos($joined, 'START TRANSACTION') + && false !== strpos($joined, 'COMMIT') +); +$order = []; +foreach ($log as $sql) { + if (0 === strpos($sql, 'START TRANSACTION')) { + $order[] = 'begin'; + } elseif (false !== strpos($sql, 'DELETE FROM `hostPrinter`')) { + $order[] = 'delete'; + } elseif (false !== strpos($sql, 'INSERT INTO `hostPrinter`')) { + $order[] = 'insert'; + } elseif (false !== strpos($sql, 'INSERT INTO `hostSpooler`')) { + $order[] = 'spooler'; + } elseif (0 === strpos($sql, 'COMMIT')) { + $order[] = 'commit'; + } +} +$t->check( + 'delete, insert and the spooler row all land between begin and commit:' + . ' ' . implode(' ', $order), + ['begin', 'delete', 'insert', 'spooler', 'commit'] === $order +); + +$insert = ''; +$insertBinds = []; +foreach ($binds as list($sql, $params)) { + if (false !== strpos($sql, 'INSERT INTO `hostPrinter`')) { + $insert = $sql; + $insertBinds = $params; + } +} +$t->check( + 'both queues go up in one statement rather than one round trip each', + 2 === substr_count($insert, '(:r') +); +$t->check( + 'every placeholder is bound exactly once, so a driver that is not' + . ' emulating prepares does not reject the repeat', + count($insertBinds) === count(array_unique(array_keys($insertBinds))) + && count($insertBinds) === substr_count($insert, ':r') +); + +// ------------------------------------------- the machine that has no queues + +list($log) = pfReport($db, ['subsystem' => 'cups', 'installed' => []]); +$t->check( + 'a machine reporting no queues still writes its spooler row -- without' + . ' it the report cannot tell "nothing installed" from "never' + . ' reported", and the host that needs looking at is the one that' + . ' vanishes from the page', + false !== strpos(implode("\n", $log), 'INSERT INTO `hostSpooler`') +); +$t->check( + 'and does not insert an empty printer row', + false === strpos(implode("\n", $log), 'INSERT INTO `hostPrinter`') +); + +// ---------------------------------------------------------- what a host may say + +foreach (['cups' => 'cups', 'WINSPOOL' => 'winspool', 'evil' => '', + '' => ''] as $reported => $want) { + $binds = []; + $db->responder = function ($sql, $params) use (&$binds) { + $binds[] = [$sql, $params]; + return null; + }; + \FOG\Agent\PrinterFacts::report( + pfHost(7), + ['subsystem' => $reported, 'installed' => []] + ); + $db->responder = null; + $got = null; + foreach ($binds as list($sql, $params)) { + if (false !== strpos($sql, 'INSERT INTO `hostSpooler`')) { + $got = $params[':sub'] ?? null; + } + } + $t->check( + "a reported subsystem of '$reported' stores '" . $want . "'", + $want === $got + ); +} + +$threw = 0; +try { + \FOG\Agent\PrinterFacts::report( + pfHost(7), + ['installed' => array_fill( + 0, + \FOG\Agent\PrinterFacts::MAX_PRINTERS + 1, + ['name' => 'x'] + )] + ); +} catch (\RuntimeException $e) { + $threw = $e->getCode(); +} +$t->check( + 'a list past MAX_PRINTERS is refused with a 413 rather than being' + . ' handed to the database', + 413 === $threw +); + +// ------------------------------------------------------------- the registry + +// A fact kind is a FACT_REPORTS entry and a poll block, never a route of its +// own (the route rule, protocol-v1.md). Left out, the class is dead code and +// every printer block a host sends is silently discarded. +$t->check( + "State::FACT_REPORTS routes 'printers' to PrinterFacts", + (\FOG\Agent\State::FACT_REPORTS['printers'] ?? null) + === \FOG\Agent\PrinterFacts::class +); + +$t->finish(); diff --git a/tests/fixtures/route-cascade-contract.txt b/tests/fixtures/route-cascade-contract.txt index 2d49cbc398..3aac5db21c 100644 --- a/tests/fixtures/route-cascade-contract.txt +++ b/tests/fixtures/route-cascade-contract.txt @@ -24,8 +24,10 @@ host task hostID hostautologout (nothing) hostdirectory (nothing) hostfactstate (nothing) +hostprinter (nothing) hostscreensetting (nothing) hostsoftware (nothing) +hostspooler (nothing) hostusersession (nothing) image imageassociation imageID imageassociation (nothing) diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index e4990a7610..dbdeb08cd5 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -134,6 +134,17 @@ hostfactstate 3 hfsHostID hostLink f:classname host hostfactstate 4 hfsKind kind - - hostfactstate 5 hfsHash hash - - hostfactstate 6 hfsUpdated updated - - +hostprinter 0 hpID id - - +hostprinter 1 hpID DT_RowId f - +hostprinter 2 hpHostID hostID - - +hostprinter 3 hpHostID hostLink f:classname host +hostprinter 4 hpName name - - +hostprinter 5 hpName mainlink f:classname,tmpcolumns - +hostprinter 6 hpURI uri - - +hostprinter 7 hpDriver driver - - +hostprinter 8 hpDefault isDefault - - +hostprinter 9 hpShared shared - - +hostprinter 10 hpObservedAt observedAt - - hostscreensetting 0 hssID id - - hostscreensetting 1 hssID DT_RowId f - hostscreensetting 2 hssHostID hostID - - @@ -158,6 +169,12 @@ hostsoftware 10 hsInstallDate installDate - - hostsoftware 11 hsFirstSeen firstSeen - - hostsoftware 12 hsLastSeen lastSeen - - hostsoftware 13 hsRemovedAt removedAt - - +hostspooler 0 hspID id - - +hostspooler 1 hspID DT_RowId f - +hostspooler 2 hspHostID hostID - - +hostspooler 3 hspHostID hostLink f:classname host +hostspooler 4 hspSubsystem subsystem - - +hostspooler 5 hspObservedAt observedAt - - hostusersession 0 husID id - - hostusersession 1 husID DT_RowId f - hostusersession 2 husHostID hostID - - @@ -379,21 +396,14 @@ printer 7 pModel model - - printer 8 pConfig config - - printer 9 pConfigFile configFile - - printer 10 pIP ip - - -printer 11 pAnon2 pAnon2 - - -printer 12 pAnon3 pAnon3 - - -printer 13 pAnon4 pAnon4 - - -printer 14 pAnon5 pAnon5 - - printerassociation 0 paID id - - printerassociation 1 paID DT_RowId f - printerassociation 2 paHostID hostID - - printerassociation 3 paHostID hostLink f:classname host printerassociation 4 paPrinterID printerID - - printerassociation 5 paIsDefault isDefault - - -printerassociation 6 paAnon1 anon1 - - -printerassociation 7 paAnon2 anon2 - - -printerassociation 8 paAnon3 anon3 - - -printerassociation 9 paAnon4 anon4 - - -printerassociation 10 paAnon5 anon5 - - +printerassociation 6 paAppliedAt appliedAt - - +printerassociation 7 paError error - - pxemenuoptions 0 pxeID id - - pxemenuoptions 1 pxeID DT_RowId f - pxemenuoptions 2 pxeName name - - diff --git a/tests/foreign-key-map.test.php b/tests/foreign-key-map.test.php index 52e4667b89..4523b9bcbe 100644 --- a/tests/foreign-key-map.test.php +++ b/tests/foreign-key-map.test.php @@ -369,6 +369,8 @@ 'hostSoftware.hsHostID', 'hostUserSession.husHostID', 'hostDirectory.hdHostID', + 'hostPrinter.hpHostID', + 'hostSpooler.hspHostID', 'hostFactState.hfsHostID', // Plugin groups, named for the plugin rather than numbered. Each // lands in that plugin's own repo, in an appended step of its diff --git a/tests/printer-grants-reach-the-client.test.php b/tests/printer-grants-reach-the-client.test.php index e2078a16cb..1668dc11f2 100644 --- a/tests/printer-grants-reach-the-client.test.php +++ b/tests/printer-grants-reach-the-client.test.php @@ -99,10 +99,6 @@ function printerRow($id) 'pConfig' => 'Network', 'pConfigFile' => '', 'pIP' => '10.0.0.' . $id, - 'pAnon2' => '', - 'pAnon3' => '', - 'pAnon4' => '', - 'pAnon5' => '', ]; } diff --git a/tests/schema-retired-tables.test.php b/tests/schema-retired-tables.test.php index 74395a8521..4e1615057b 100644 --- a/tests/schema-retired-tables.test.php +++ b/tests/schema-retired-tables.test.php @@ -68,6 +68,65 @@ // is the one thing that file must never do. $schemaSrc = file_get_contents($schemaFile); +/** + * The SQL statements schema.php builds, each one assembled. + * + * Needed because a statement is written as a chain of string literals -- + * "ALTER TABLE `printerAssoc` " . "DROP COLUMN `paAnon1`, ..." -- so the + * table name and the column name are never in the same literal, and a + * search of the raw source cannot tell "printerAssoc drops paAnon2" from + * "some other table drops paAnon2". `plugins` has a pAnon2 of its own that + * was RENAMED and never dropped, so that is not a hypothetical distinction. + * + * Lexed rather than regexed, for the reason bin/schema-manifest.php gives: + * a regex looking for string boundaries in 11,000 lines of concatenated + * literals eventually matches a span running out of one literal and into + * the next. + * + * @param string $src the schema.php source + * + * @return string[] one entry per assembled statement + */ +function schemaStatements($src) +{ + $out = []; + $cur = ''; + $joining = false; + foreach (token_get_all($src) as $tok) { + if (is_array($tok)) { + if (T_WHITESPACE === $tok[0] || T_COMMENT === $tok[0]) { + continue; + } + if (T_CONSTANT_ENCAPSED_STRING === $tok[0]) { + if ('' !== $cur && !$joining) { + $out[] = $cur; + $cur = ''; + } + $cur .= substr($tok[1], 1, -1); + $joining = false; + continue; + } + } + // A '.' between two literals continues the same statement; anything + // else ends it. + if ('.' === $tok) { + $joining = true; + continue; + } + if ('' !== $cur) { + $out[] = $cur; + $cur = ''; + } + $joining = false; + } + if ('' !== $cur) { + $out[] = $cur; + } + return $out; +} + +$schemaStatements = schemaStatements($schemaSrc); + foreach ($retired as $i => $entry) { $name = (string)($entry['table'] ?? ''); $t->check( @@ -81,6 +140,32 @@ "retired `$name` records a reason", '' !== trim((string)($entry['reason'] ?? '')) ); + // An entry carrying a `column` retires that one column and leaves the + // table in place, so none of the whole-table assertions below apply to + // it -- the table is still in the manifest, still built, never dropped. + // What DOES have to hold is that schema.php actually drops the column, + // or the manifest is claiming an end state the replay never reaches. + $column = (string)($entry['column'] ?? ''); + if ('' !== $column) { + $t->check( + "retired `$name`.`$column` is gone from the manifest's columns", + !isset($tables[strtolower($name)]['columns'][$column]) + ); + $drops = false; + foreach ($schemaStatements as $stmt) { + if (false !== stripos($stmt, 'ALTER TABLE `' . $name . '`') + && false !== stripos($stmt, 'DROP COLUMN `' . $column . '`') + ) { + $drops = true; + break; + } + } + $t->check( + "schema.php drops `$name`.`$column`", + $drops + ); + continue; + } $t->check( "retired `$name` is absent from the manifest's tables", !isset($tables[strtolower($name)]) From 29c12a23e798b135745e8359f40c8b640435e796 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 20:27:32 +0000 Subject: [PATCH 069/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a591667b71..e5853014cb 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10359,6 +10359,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 211004c5b3..911205bfd0 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10368,6 +10368,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ac670c5901..655d6af5ef 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10527,6 +10527,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c79895d6c2..2070a26faa 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10360,6 +10360,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da26901cb2..a28aeb921e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10352,6 +10352,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d7b3169c32..b603f7ed4a 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10075,6 +10075,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index dc1ff1b8bf..9476bef60a 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10030,6 +10030,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 6e57cf07ce..005a44f351 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8879,6 +8879,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 40818e02f7..87b82f5950 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 782e8c0d39..4b7e8276d9 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10355,6 +10355,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 81cb7dbe8f9c18fe04e326599a8a33765e592a3a Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:38:08 -0500 Subject: [PATCH 070/117] Printer Deployment report, and the paIsDefault type-change fallout The report FOG has never been able to produce: which assigned printers actually arrived, and which did not. Host, mode, spooler, assigned, installed, missing, default, state, last error, reported. Verified on the lab install, not just in the suite. Deployed to the local webroot, applied schema 426 through the UI, and drove State::facts() -- the real poll path -- against the real database with three shapes: 1. two queues reported, one assigned -> extra, nothing missing 2. the assigned queue stops being reported -> missing, and it names which 3. an empty installed list -> the host is STILL in the report, with a state, rather than dropping off the page Seventeen checks, all green (background_scripts/prove_printer_facts_end_to_end.php, which cleans up its host and printer on the way out including on failure). Pass 3 is the one that matters, and it is why hostSpooler is a separate table. Joining the report to hostPrinter instead reads as an obvious simplification and loses every machine that answered "nothing installed" -- the host most worth looking at becomes an absence. Proven by mutating the LIVE copy: the wrong join fails nine of the seventeen checks. The unit suite could not see it (no database), so it now checks the join as source; that mutation and an INNER-join one both fail it. Assigned printers resolve through Resolver::resolvePrinters, the same call PrinterClient makes to build what actually goes down the wire, so the report cannot disagree with what the host is being told to have. Re-implementing the group-grant and default-precedence rules here is how a report starts quietly contradicting the thing it reports on. Verdict order is deliberate and gated: never-reported outranks everything (it is FOG not knowing, and an empty State column would let it pass for agreement); a recorded error outranks a missing printer because it says why; missing outranks extra because somebody asked for it and did not get it. Route::getNames, not getIds -- getIds returns a FLAT list of one field and the id-to-name map needs both. Signature read rather than recalled, which is the same class of mistake the getclass-methods-exist gate was built for. Also here, because schema 426 caused it: three call sites still described paIsDefault as the varchar(2) it no longer is. Resolver compared it as a string with a comment explaining why casting would be wrong; Host and PrinterManagement filtered on ['0', ''], where the empty string was the 1.5-origin column's "never set" and the upgrade now normalizes it to 0. All three moved to the tinyint idiom the gpaIsDefault branch two blocks down was already using. Noted and left alone: PrinterAssociation::isDefault() is `(bool)($this->get('isDefault') === 1)`, which with the old varchar compared '1' === 1 and has therefore always returned false. It has no callers, so this type change alters nothing. Pre-existing. Suite: 329 passed, 1 failed -- certificate-table.test.php, which fails on working-1.6 too and is untouched here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 43 +- .../en_US.UTF-8/LC_MESSAGES/messages.po | 43 +- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 43 +- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 43 +- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 43 +- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 43 +- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 47 ++- .../web/management/languages/messages.pot | 37 +- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 43 +- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 43 +- packages/web/src/Assign/Resolver.php | 12 +- packages/web/src/Auth/Authorization.php | 1 + packages/web/src/Items/Host.php | 8 +- packages/web/src/Pages/PrinterManagement.php | 11 +- packages/web/src/Pages/ReportManagement.php | 1 + .../web/src/Reports/Printer_Deployment.php | 383 ++++++++++++++++++ tests/agent-printer-facts.test.php | 88 ++++ 17 files changed, 907 insertions(+), 25 deletions(-) create mode 100644 packages/web/src/Reports/Printer_Deployment.php diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e5853014cb..17593fa854 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -1336,6 +1336,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "zugeordneter Host" + #, fuzzy msgid "Assigned Group" msgstr "Name der Speichergruppe" @@ -4386,6 +4390,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "Hosts" @@ -5511,6 +5518,10 @@ msgstr "" msgid "Last deployed" msgstr "Zuletzt verteilt" +#, fuzzy +msgid "Last error" +msgstr "Fehler" + msgid "Last flush" msgstr "" @@ -6075,6 +6086,9 @@ msgstr "Minutenwert ist nicht gültig" msgid "Minutes field is invalid" msgstr "Minutenwert ist nicht gültig" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Ein temporärer Ordner fehlt" @@ -7618,6 +7632,10 @@ msgstr "Drucker erstellen fehlgeschlagen!" msgid "Printer Create Success" msgstr "Drucker hinzufügen erfolgreich." +#, fuzzy +msgid "Printer Deployment" +msgstr "Druckerverwaltung" + msgid "Printer Description" msgstr "Druckerbeschreibung" @@ -9207,6 +9225,9 @@ msgstr "Bereichsvariable muss boolean sein" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10359,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11849,6 +11869,10 @@ msgstr "da der FOG versuchen wird, mit dem Internet zu verbinden, " msgid "as its primary group" msgstr "als primäre Gruppe finden" +#, fuzzy +msgid "assigned" +msgstr "zugeordneter Host" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11974,10 +11998,20 @@ msgstr "Fehler" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "fehlgeschlagen" + #, fuzzy msgid "failed to execute, image file" msgstr "Löschen der Imagedateien fehlgeschlagen" @@ -12317,6 +12351,10 @@ msgstr "Minuten" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Version" + msgid "moments from now" msgstr "" @@ -12423,6 +12461,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Dieser Host ist bereits vorhanden." diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 911205bfd0..b60f7636d1 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -1340,6 +1340,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "No node associated" + #, fuzzy msgid "Assigned Group" msgstr "Storage Group Name" @@ -4388,6 +4392,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "Hosts" @@ -5511,6 +5518,10 @@ msgstr "" msgid "Last deployed" msgstr "Last Deployed" +#, fuzzy +msgid "Last error" +msgstr "Error" + msgid "Last flush" msgstr "" @@ -6087,6 +6098,9 @@ msgstr "Minute value is not valid" msgid "Minutes field is invalid" msgstr "Minute value is not valid" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Missing a temporary folder" @@ -7629,6 +7643,10 @@ msgstr "Printer update failed!" msgid "Printer Create Success" msgstr "Printer already exists" +#, fuzzy +msgid "Printer Deployment" +msgstr "Printer Management" + msgid "Printer Description" msgstr "Printer Description" @@ -9218,6 +9236,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10368,7 +10389,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11855,6 +11875,10 @@ msgstr "" msgid "as its primary group" msgstr "Update Primary Group" +#, fuzzy +msgid "assigned" +msgstr "No node associated" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11980,10 +12004,20 @@ msgstr "Error" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "Failed" + #, fuzzy msgid "failed to execute, image file" msgstr "Failed to delete image files" @@ -12322,6 +12356,10 @@ msgstr "minutes" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Version" + msgid "moments from now" msgstr "" @@ -12428,6 +12466,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Printer already exists" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 655d6af5ef..6ba69784ec 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -1354,6 +1354,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "No nodo asociado" + #, fuzzy msgid "Assigned Group" msgstr "Nombre del grupo de almacenamiento" @@ -4453,6 +4457,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "Hospedadores" @@ -5605,6 +5612,10 @@ msgstr "" msgid "Last deployed" msgstr "última Desplegado" +#, fuzzy +msgid "Last error" +msgstr "Error" + msgid "Last flush" msgstr "" @@ -6185,6 +6196,9 @@ msgstr "tipo de tarea no es válida" msgid "Minutes field is invalid" msgstr "tipo de tarea no es válida" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Falta una carpeta temporal" @@ -7745,6 +7759,10 @@ msgstr "actualización de la impresora ha fallado!" msgid "Printer Create Success" msgstr "Impresora ya existe" +#, fuzzy +msgid "Printer Deployment" +msgstr "Sin administración de la impresora" + msgid "Printer Description" msgstr "Descripción de la impresora" @@ -9356,6 +9374,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10527,7 +10548,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -12016,6 +12036,10 @@ msgstr "" msgid "as its primary group" msgstr "Actualización de Grupo Primario" +#, fuzzy +msgid "assigned" +msgstr "No nodo asociado" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -12141,10 +12165,20 @@ msgstr "Error" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "Ha fallado" + #, fuzzy msgid "failed to execute, image file" msgstr "No se pudo crear la tarea" @@ -12483,6 +12517,10 @@ msgstr "minutos" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Versión" + msgid "moments from now" msgstr "" @@ -12588,6 +12626,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Impresora ya existe" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2070a26faa..534cdbe532 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -1336,6 +1336,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "zugeordneter Host" + #, fuzzy msgid "Assigned Group" msgstr "Name der Speichergruppe" @@ -4386,6 +4390,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "Hosts" @@ -5512,6 +5519,10 @@ msgstr "" msgid "Last deployed" msgstr "Zuletzt verteilt" +#, fuzzy +msgid "Last error" +msgstr "Fehler" + msgid "Last flush" msgstr "" @@ -6076,6 +6087,9 @@ msgstr "Minutenwert ist nicht gültig" msgid "Minutes field is invalid" msgstr "Minutenwert ist nicht gültig" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Ein temporärer Ordner fehlt" @@ -7619,6 +7633,10 @@ msgstr "Drucker erstellen fehlgeschlagen!" msgid "Printer Create Success" msgstr "Drucker hinzufügen erfolgreich." +#, fuzzy +msgid "Printer Deployment" +msgstr "Druckerverwaltung" + msgid "Printer Description" msgstr "Druckerbeschreibung" @@ -9208,6 +9226,9 @@ msgstr "Bereichsvariable muss boolean sein" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10360,7 +10381,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11850,6 +11870,10 @@ msgstr "da der FOG versuchen wird, mit dem Internet zu verbinden, " msgid "as its primary group" msgstr "als primäre Gruppe finden" +#, fuzzy +msgid "assigned" +msgstr "zugeordneter Host" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11975,10 +11999,20 @@ msgstr "Fehler" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "fehlgeschlagen" + #, fuzzy msgid "failed to execute, image file" msgstr "Löschen der Imagedateien fehlgeschlagen" @@ -12318,6 +12352,10 @@ msgstr "Minuten" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Version" + msgid "moments from now" msgstr "" @@ -12424,6 +12462,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Dieser Host ist bereits vorhanden." diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a28aeb921e..1e301fc1e4 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -1341,6 +1341,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "Aucun noeud associé" + #, fuzzy msgid "Assigned Group" msgstr "Nom du groupe de stockage" @@ -4388,6 +4392,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "hôtes" @@ -5511,6 +5518,10 @@ msgstr "" msgid "Last deployed" msgstr "Dernière Déployé" +#, fuzzy +msgid "Last error" +msgstr "Erreur" + msgid "Last flush" msgstr "" @@ -6073,6 +6084,9 @@ msgstr "valeur de la minute est pas valide" msgid "Minutes field is invalid" msgstr "valeur de la minute est pas valide" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Manquer un dossier temporaire" @@ -7614,6 +7628,10 @@ msgstr "mise à jour de l'imprimante a échoué!" msgid "Printer Create Success" msgstr "Imprimante existe déjà" +#, fuzzy +msgid "Printer Deployment" +msgstr "Gestion des imprimantes" + msgid "Printer Description" msgstr "Printer description" @@ -9202,6 +9220,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10352,7 +10373,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11840,6 +11860,10 @@ msgstr "" msgid "as its primary group" msgstr "Mise à jour de groupe principal" +#, fuzzy +msgid "assigned" +msgstr "Aucun noeud associé" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11965,10 +11989,20 @@ msgstr "Erreur" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "Échoué" + #, fuzzy msgid "failed to execute, image file" msgstr "Échec de la suppression des fichiers d'image" @@ -12307,6 +12341,10 @@ msgstr "minutes" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Version" + msgid "moments from now" msgstr "" @@ -12413,6 +12451,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Imprimante existe déjà" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index b603f7ed4a..d4d3efea54 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -1310,6 +1310,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "Host associato" + #, fuzzy msgid "Assigned Group" msgstr "Nome gruppo di archiviazione" @@ -4286,6 +4290,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "host" @@ -5364,6 +5371,10 @@ msgstr "" msgid "Last deployed" msgstr "Ultima Distribuita" +#, fuzzy +msgid "Last error" +msgstr "Errore" + msgid "Last flush" msgstr "" @@ -5909,6 +5920,9 @@ msgstr "valore dei minuti non è valido" msgid "Minutes field is invalid" msgstr "valore dei minuti non è valido" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Manca una cartella temporanea" @@ -7415,6 +7429,10 @@ msgstr "Creazione stampante fallita" msgid "Printer Create Success" msgstr "Creazione stampante riuscita" +#, fuzzy +msgid "Printer Deployment" +msgstr "Printer Management" + msgid "Printer Description" msgstr "Descrizione della stampante" @@ -8958,6 +8976,9 @@ msgstr "La variabile spazio deve essere boolean" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10075,7 +10096,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11522,6 +11542,10 @@ msgstr "come FOG proverà ad andare in internet" msgid "as its primary group" msgstr "come il suo gruppo primario" +#, fuzzy +msgid "assigned" +msgstr "Host associato" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11643,10 +11667,20 @@ msgstr "Errore" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "Fallito" + #, fuzzy msgid "failed to execute, image file" msgstr "Impossibile eliminare i file di immagine" @@ -11967,6 +12001,10 @@ msgstr "minuti" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Versione" + msgid "moments from now" msgstr "" @@ -12068,6 +12106,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Questo host esiste già" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 9476bef60a..1f8d5f6778 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -1294,6 +1294,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "関連付けられたホスト" + #, fuzzy msgid "Assigned Group" msgstr "管理者グループ" @@ -4262,6 +4266,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "ホスト" @@ -5334,6 +5341,10 @@ msgstr "タスクチェックイン日" msgid "Last deployed" msgstr "最終展開" +#, fuzzy +msgid "Last error" +msgstr "エラー" + msgid "Last flush" msgstr "" @@ -5880,6 +5891,9 @@ msgstr "分の値が無効です" msgid "Minutes field is invalid" msgstr "分の値が無効です" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "一時フォルダーがありません" @@ -7394,6 +7408,10 @@ msgstr "プリンターの作成に失敗しました" msgid "Printer Create Success" msgstr "プリンターの作成に成功しました" +#, fuzzy +msgid "Printer Deployment" +msgstr "プリンター管理" + msgid "Printer Description" msgstr "プリンターの説明" @@ -8920,6 +8938,9 @@ msgstr "Space 変数はブール値である必要があります" msgid "Specified download URL not allowed!" msgstr "指定されたダウンロード URL は許可されていません!" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10030,7 +10051,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11476,6 +11496,10 @@ msgstr "FOG がインターネットへ接続を試みるため" msgid "as its primary group" msgstr "プライマリグループとして" +#, fuzzy +msgid "assigned" +msgstr "関連付けられたホスト" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11594,10 +11618,20 @@ msgstr "エラー" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "失敗しました" + #, fuzzy msgid "failed to execute, image file" msgstr "実行に失敗しました。イメージファイル: " @@ -11923,6 +11957,10 @@ msgstr "分" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "バージョン" + msgid "moments from now" msgstr "" @@ -12024,6 +12062,10 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +#, fuzzy +msgid "off" +msgstr "の" + #, fuzzy msgid "off means an account must already exist" msgstr "このホストは既に存在します" @@ -14855,9 +14897,6 @@ msgstr "" #~ msgid "not found on disk" #~ msgstr "このノード上に見つかりません" -#~ msgid "of" -#~ msgstr "の" - #~ msgid "on the following pages of this document" #~ msgstr "このドキュメントの次のページ" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 005a44f351..a0bbf7b7fe 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -1167,6 +1167,9 @@ msgstr "" msgid "Area" msgstr "" +msgid "Assigned" +msgstr "" + msgid "Assigned Group" msgstr "" @@ -3772,6 +3775,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + msgid "Hosts:" msgstr "" @@ -4716,6 +4722,9 @@ msgstr "" msgid "Last deployed" msgstr "" +msgid "Last error" +msgstr "" + msgid "Last flush" msgstr "" @@ -5198,6 +5207,9 @@ msgstr "" msgid "Minutes field is invalid" msgstr "" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "" @@ -6529,6 +6541,9 @@ msgstr "" msgid "Printer Create Success" msgstr "" +msgid "Printer Deployment" +msgstr "" + msgid "Printer Description" msgstr "" @@ -7887,6 +7902,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -8879,7 +8897,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10200,6 +10217,9 @@ msgstr "" msgid "as its primary group" msgstr "" +msgid "assigned" +msgstr "" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -10311,10 +10331,19 @@ msgstr "" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +msgid "failed" +msgstr "" + msgid "failed to execute, image file" msgstr "" @@ -10606,6 +10635,9 @@ msgstr "" msgid "mismatched" msgstr "" +msgid "missing" +msgstr "" + msgid "moments from now" msgstr "" @@ -10701,6 +10733,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + msgid "off means an account must already exist" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 87b82f5950..a9d1ecb462 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -1340,6 +1340,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "No nó associado" + #, fuzzy msgid "Assigned Group" msgstr "Nome do grupo de armazenamento" @@ -4388,6 +4392,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "Hosts" @@ -5511,6 +5518,10 @@ msgstr "" msgid "Last deployed" msgstr "Última Implantado" +#, fuzzy +msgid "Last error" +msgstr "Erro" + msgid "Last flush" msgstr "" @@ -6074,6 +6085,9 @@ msgstr "valor do minuto não é válido" msgid "Minutes field is invalid" msgstr "valor do minuto não é válido" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "Faltando uma pasta temporária" @@ -7616,6 +7630,10 @@ msgstr "atualização da impressora falhou!" msgid "Printer Create Success" msgstr "Impressora já existe" +#, fuzzy +msgid "Printer Deployment" +msgstr "Gerenciamento de impressora" + msgid "Printer Description" msgstr "Descrição Printer" @@ -9205,6 +9223,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10355,7 +10376,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11843,6 +11863,10 @@ msgstr "" msgid "as its primary group" msgstr "Grupo primário de actualização" +#, fuzzy +msgid "assigned" +msgstr "No nó associado" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11968,10 +11992,20 @@ msgstr "Erro" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "fracassado" + #, fuzzy msgid "failed to execute, image file" msgstr "Falha ao excluir arquivos de imagem" @@ -12310,6 +12344,10 @@ msgstr "minutos" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "Versão" + msgid "moments from now" msgstr "" @@ -12416,6 +12454,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "Impressora já existe" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 4b7e8276d9..63fee9b433 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -1340,6 +1340,10 @@ msgstr "" msgid "Area" msgstr "" +#, fuzzy +msgid "Assigned" +msgstr "无关联的节点" + #, fuzzy msgid "Assigned Group" msgstr "存储组名称" @@ -4388,6 +4392,9 @@ msgstr "" msgid "Hosts set to use Active Directory, with what each one last reported about itself. An OU difference is not corrected by the legacy client, which only reads the OU when it first joins the machine." msgstr "" +msgid "Hosts with printer management switched on, with what each one last reported about itself. A printer in Missing was assigned and is not on the machine." +msgstr "" + #, fuzzy msgid "Hosts:" msgstr "主机" @@ -5511,6 +5518,10 @@ msgstr "" msgid "Last deployed" msgstr "最后部署" +#, fuzzy +msgid "Last error" +msgstr "错误" + msgid "Last flush" msgstr "" @@ -6074,6 +6085,9 @@ msgstr "分钟值无效" msgid "Minutes field is invalid" msgstr "分钟值无效" +msgid "Missing" +msgstr "" + msgid "Missing a temporary folder" msgstr "缺少一个临时文件夹" @@ -7616,6 +7630,10 @@ msgstr "打印机更新失败!" msgid "Printer Create Success" msgstr "打印机已经存在" +#, fuzzy +msgid "Printer Deployment" +msgstr "打印机管理" + msgid "Printer Description" msgstr "打印机说明" @@ -9205,6 +9223,9 @@ msgstr "" msgid "Specified download URL not allowed!" msgstr "" +msgid "Spooler" +msgstr "" + msgid "Stale" msgstr "" @@ -10355,7 +10376,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11843,6 +11863,10 @@ msgstr "" msgid "as its primary group" msgstr "更新主要组" +#, fuzzy +msgid "assigned" +msgstr "无关联的节点" + msgid "attr could not be run -- SELinux may be denying it" msgstr "" @@ -11968,10 +11992,20 @@ msgstr "错误" msgid "every column on this table will be treated as untyped" msgstr "" +msgid "exclusive" +msgstr "" + #, php-format msgid "exited abnormally with code %d; canceling task" msgstr "" +msgid "extra" +msgstr "" + +#, fuzzy +msgid "failed" +msgstr "失败" + #, fuzzy msgid "failed to execute, image file" msgstr "无法删除图像文件" @@ -12310,6 +12344,10 @@ msgstr "分钟" msgid "mismatched" msgstr "" +#, fuzzy +msgid "missing" +msgstr "版" + msgid "moments from now" msgstr "" @@ -12416,6 +12454,9 @@ msgstr "" msgid "of how much disk space the image is using." msgstr "" +msgid "off" +msgstr "" + #, fuzzy msgid "off means an account must already exist" msgstr "打印机已经存在" diff --git a/packages/web/src/Assign/Resolver.php b/packages/web/src/Assign/Resolver.php index ecd3e0a463..a88e11dbca 100644 --- a/packages/web/src/Assign/Resolver.php +++ b/packages/web/src/Assign/Resolver.php @@ -248,12 +248,12 @@ public static function resolvePrinters(array $hostIDs) $hostID = (int)$row['paHostID']; $printerID = (int)$row['paPrinterID']; $direct[$hostID][] = $printerID; - // paIsDefault is varchar(2) on a 1.5-origin database and carries - // '1', '0' and '' -- it predates booleans-are-tinyint. Compare it - // as the string the writers actually store rather than casting, - // which would make the empty string a 0 and read the same either - // way, but only by luck. - if ('1' === (string)$row['paIsDefault'] + // Schema 426 made paIsDefault a tinyint(1), so it now carries + // 0 or 1 like gpaIsDefault below and is read the same way. It + // was a varchar(2) carrying '1', '0' and '' -- a 1.5-origin + // column that predated booleans-are-tinyint -- and the upgrade + // normalizes the empty string to 0 before retyping. + if ((int)$row['paIsDefault'] > 0 && !isset($directDefault[$hostID]) ) { $directDefault[$hostID] = $printerID; diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 2419037daa..02a99d5fd0 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -134,6 +134,7 @@ class Authorization extends FOGBase // reads, gated on host.view. Same reasoning. 'installed_software' => 'host', 'directory_membership' => 'host', + 'printer_deployment' => 'host', 'user_sessions' => 'host', // Storage Report reads `images`, `imageGroupAssoc`, `nfsGroups` and // `nfsGroupMembers`. Group and node names are the part not already diff --git a/packages/web/src/Items/Host.php b/packages/web/src/Items/Host.php index 7aefa7d799..946b778018 100644 --- a/packages/web/src/Items/Host.php +++ b/packages/web/src/Items/Host.php @@ -514,7 +514,7 @@ public function updateDefault($printerid) [ 'printerID' => $printers, 'hostID' => $this->get('id'), - 'isDefault' => '1' + 'isDefault' => 1 ], '', ['isDefault' => 0] @@ -524,8 +524,12 @@ public function updateDefault($printerid) ->update( [ 'printerID' => $printerid, + // Schema 426 made paIsDefault a tinyint(1). The + // empty string this used to also match was the + // 1.5-origin varchar's "never set", and the upgrade + // normalizes it to 0. 'hostID' => $this->get('id'), - 'isDefault' => ['0', ''] + 'isDefault' => 0 ], '', ['isDefault' => 1] diff --git a/packages/web/src/Pages/PrinterManagement.php b/packages/web/src/Pages/PrinterManagement.php index ccfafb7885..81c2310f95 100644 --- a/packages/web/src/Pages/PrinterManagement.php +++ b/packages/web/src/Pages/PrinterManagement.php @@ -831,16 +831,19 @@ public function printerHostPost() 'isDefault' => 1 ], '', - ['isDefault' => '0'] + ['isDefault' => 0] ); (new PrinterAssociationManager())->update( [ 'printerID' => $this->obj->get('id'), + // Schema 426 made paIsDefault a tinyint(1); the + // empty string this also matched was the 1.5-origin + // varchar's "never set", normalized to 0 on upgrade. 'hostID' => $hosts, - 'isDefault' => ['0', ''] + 'isDefault' => 0 ], '', - ['isDefault' => '1'] + ['isDefault' => 1] ); } } @@ -862,7 +865,7 @@ public function printerHostPost() 'isDefault' => 1, ], '', - ['isDefault' => '0'] + ['isDefault' => 0] ); } } diff --git a/packages/web/src/Pages/ReportManagement.php b/packages/web/src/Pages/ReportManagement.php index bcffd9cf89..4bef78c466 100644 --- a/packages/web/src/Pages/ReportManagement.php +++ b/packages/web/src/Pages/ReportManagement.php @@ -180,6 +180,7 @@ public static function reportTitles() 'imaging report' => _('Imaging Report'), 'installed software' => _('Installed Software'), 'directory membership' => _('Directory Membership'), + 'printer deployment' => _('Printer Deployment'), 'user sessions' => _('User Sessions'), 'pending mac list' => _('Pending MAC Addresses'), 'product keys' => _('Host Product Keys'), diff --git a/packages/web/src/Reports/Printer_Deployment.php b/packages/web/src/Reports/Printer_Deployment.php new file mode 100644 index 0000000000..d08638cd6f --- /dev/null +++ b/packages/web/src/Reports/Printer_Deployment.php @@ -0,0 +1,383 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Reports; + +use FOG\Assign\Resolver; +use FOG\Pages\ReportManagement; +use FOG\Router\Route; + +/** + * Which assigned printers actually arrived, and which did not. + * + * The report FOG has never been able to produce. `printerAssoc` is intent -- + * what an admin assigned -- and until design 0010 nothing recorded the other + * half, so "did the printer I assigned install?" had no answer at any price. + * An install that failed failed silently, and the client retried the same + * thing on the next poll, forever. + * + * It matters most on Linux. UnixPrinterManager::Remove() runs `lpstat`, + * which is CUPS' status QUERY tool and has never been able to remove a + * printer -- so mode 2, whose entire content is removal, has reported + * success every poll while removing nothing. Every host in that mode is in + * this report and has never been visible before. + * + * A FLEET SNAPSHOT, not a history -- the same test that puts User_Sessions + * and Directory_Membership under Lists rather than in + * ReportManagement::AGGREGATIONS. + * + * GATED ON `host`, like those two and for the same reason: reports share the + * `report` node by default (the defect ADR 0023 opens with), and this is + * host data. + * + * @category Printer_Deployment + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class Printer_Deployment extends ReportManagement +{ + /** + * The most rows a grid or export will carry back. + * + * @var int + */ + const MAX_ROWS = 5000; + + /** + * Display page. + * + * @return void + */ + public function file() + { + $this->title = self::reportTitle(); + + $this->headerData = [ + _('Host'), + _('Mode'), + _('Spooler'), + _('Assigned'), + _('Installed'), + _('Missing'), + _('Default'), + _('State'), + _('Last error'), + _('Reported') + ]; + $this->attributes = [ + [], [], [], [], [], [], [], [], [], [] + ]; + + $payload = $this->reportRows(); + + echo '
    '; + echo '
    '; + echo '

    '; + echo $this->title; + echo '

    '; + echo '
    '; + echo '
    '; + + printf( + '

    %s

    ', + \Initiator::e( + _( + 'Hosts with printer management switched on, with what ' + . 'each one last reported about itself. A printer in ' + . 'Missing was assigned and is not on the machine.' + ) + ) + ); + + echo self::renderReportCap( + $payload['truncated'], + self::MAX_ROWS + ); + + $this->render(12, 'printerdeploymentreport-table'); + + echo '
    '; + echo '
    '; + } + + /** + * The rows this report serves. + * + * Split from the emit so the grid and the CSV export run the same query + * -- see ReportManagement::exportAll(). + * + * @return array + */ + protected function reportRows() + { + // Every host with printer management ON, left-joined to what it + // reported. A LEFT JOIN rather than an inner one on purpose: a host + // that has never reported is the most interesting row here, and an + // inner join would hide exactly the machines nobody has heard from. + // + // hostSpooler rather than hostPrinter is what the join hangs off, + // and that is the whole reason the table exists: a machine with a + // working spooler and no queues has ANSWERED, and joining to + // hostPrinter would file it with the machines that never did. + $sql = "SELECT `hostID`, + `hostName`, + `hostPrinterLevel`, + `hostAgentCheckin`, + `hspSubsystem`, + `hspObservedAt` + FROM `hosts` + LEFT OUTER JOIN `hostSpooler` ON `hspHostID` = `hostID` + WHERE `hostPrinterLevel` <> '' + AND `hostPrinterLevel` <> '0' + ORDER BY `hostName` ASC + LIMIT " . (self::MAX_ROWS + 1); + + $rows = (array)self::$DB->query($sql) + ->fetch(\PDO::FETCH_ASSOC, 'fetch_all') + ->get(); + + $rows = array_slice($rows, 0, self::MAX_ROWS + 1); + $truncated = count($rows) > self::MAX_ROWS; + $rows = array_slice($rows, 0, self::MAX_ROWS); + + $hostIDs = []; + foreach ($rows as $row) { + $hostIDs[] = (int)$row['hostID']; + } + + $assigned = self::_assigned($hostIDs); + $installed = self::_installed($hostIDs); + $errors = self::_errors($hostIDs); + + $data = []; + foreach ($rows as $row) { + $hostID = (int)$row['hostID']; + $want = $assigned[$hostID] ?? []; + $have = $installed[$hostID] ?? []; + $reported = null !== ($row['hspObservedAt'] ?? null); + + $missing = array_diff(array_keys($want), array_keys($have)); + $extra = array_diff(array_keys($have), array_keys($want)); + $error = $errors[$hostID] ?? ''; + + $default = ''; + foreach ($have as $name => $queue) { + if (!empty($queue['default'])) { + $default = $name; + } + } + + $data[] = [ + 'hostName' => (string)($row['hostName'] ?? ''), + 'mode' => self::mode($row['hostPrinterLevel'] ?? ''), + 'spooler' => (string)($row['hspSubsystem'] ?? ''), + 'assigned' => implode(', ', array_keys($want)), + 'installed' => implode(', ', array_keys($have)), + 'missing' => implode(', ', $missing), + 'default' => $default, + 'state' => self::state($reported, $missing, $extra, $error), + 'error' => $error, + 'observedAt' => (string)($row['hspObservedAt'] ?? '') + ]; + } + + // A capped fetch is not a complete answer, and a CSV taken from one + // looks like a complete file once it is on disk. + return [ + 'data' => $data, + 'truncated' => $truncated + ]; + } + + /** + * The printers assigned to each host, by name. + * + * Resolved through Resolver::resolvePrinters -- the same call + * PrinterClient makes to build what actually goes down the wire -- so + * the report cannot disagree with what the host is being told to have. + * Re-implementing the group-grant and default-precedence rules here is + * how a report starts quietly contradicting the thing it reports on. + * + * @param int[] $hostIDs the hosts on this page + * + * @return array hostID => [name => printerID] + */ + private static function _assigned(array $hostIDs) + { + if (empty($hostIDs)) { + return []; + } + $resolved = Resolver::resolvePrinters($hostIDs); + + $wanted = []; + foreach ($resolved as $set) { + foreach ((array)($set['printers'] ?? []) as $id) { + $wanted[(int)$id] = true; + } + } + // getNames, not getIds. getIds returns a FLAT list of one field -- + // the names with nothing tying them to their ids -- and the join + // below needs both. getNames answers with stdClass rows carrying + // ->id and ->name, which is what makes the mapping possible at all. + $names = []; + if (!empty($wanted)) { + foreach (Route::getNames('printer', ['id' => array_keys($wanted)]) as $r) { + $names[(int)($r->id ?? 0)] = (string)($r->name ?? ''); + } + } + + $out = []; + foreach ($resolved as $hostID => $set) { + foreach ((array)($set['printers'] ?? []) as $id) { + $name = (string)($names[(int)$id] ?? ''); + if ('' === $name) { + continue; + } + $out[(int)$hostID][$name] = (int)$id; + } + } + + return $out; + } + + /** + * The printers each host says it actually has. + * + * @param int[] $hostIDs the hosts on this page + * + * @return array hostID => [name => ['uri' => ..., 'default' => bool]] + */ + private static function _installed(array $hostIDs) + { + if (empty($hostIDs)) { + return []; + } + $rows = (array)self::$DB->query( + 'SELECT `hpHostID`,`hpName`,`hpURI`,`hpDefault` ' + . 'FROM `hostPrinter` WHERE `hpHostID` IN (' + . implode(',', array_map('intval', $hostIDs)) + . ') ORDER BY `hpName` ASC' + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + + $out = []; + foreach ($rows as $row) { + $out[(int)$row['hpHostID']][(string)$row['hpName']] = [ + 'uri' => (string)($row['hpURI'] ?? ''), + 'default' => !empty($row['hpDefault']) + ]; + } + + return $out; + } + + /** + * The most recent install error recorded against each host. + * + * The column this report exists for as much as any other. Today a + * printer that will not install produces nothing an admin can see. + * + * @param int[] $hostIDs the hosts on this page + * + * @return array hostID => message + */ + private static function _errors(array $hostIDs) + { + if (empty($hostIDs)) { + return []; + } + $rows = (array)self::$DB->query( + 'SELECT `paHostID`,`paError` FROM `printerAssoc` ' + . 'WHERE `paHostID` IN (' + . implode(',', array_map('intval', $hostIDs)) + . ") AND `paError` <> '' ORDER BY `paAppliedAt` DESC" + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + + $out = []; + foreach ($rows as $row) { + $hostID = (int)$row['paHostID']; + if (isset($out[$hostID])) { + // Ordered newest first, so the first one seen is the one to + // show. An admin fixing a host wants the current failure, + // not a list of everything that has ever gone wrong on it. + continue; + } + $out[$hostID] = (string)($row['paError'] ?? ''); + } + + return $out; + } + + /** + * The host's printer mode, in words. + * + * `hostPrinterLevel` stores 0, 1 or 2 and the wire has always sent 0, + * `a` or `ar` -- two vocabularies for one setting, neither written down + * anywhere an admin can see (design 0010 section 1.3). This is the + * third and the only one meant for a person. + * + * @param string $level the stored level + * + * @return string + */ + protected static function mode($level) + { + switch ((int)$level) { + case 1: + return _('assigned'); + case 2: + return _('exclusive'); + default: + return _('off'); + } + } + + /** + * The verdict for one host. + * + * Five states, and the distinction between "ok" and "never reported" is + * the whole point: "never reported" is FOG not knowing, and an empty + * State column would let it pass for agreement. + * + * Ordered by what an admin should act on first. A recorded error beats + * a missing printer because it says WHY; a missing printer beats an + * extra one because somebody asked for it and did not get it. + * + * @param bool $reported whether anything was ever reported + * @param string[] $missing assigned and not installed + * @param string[] $extra installed and not assigned + * @param string $error the last recorded install error + * + * @return string + */ + protected static function state($reported, array $missing, array $extra, $error) + { + if (!$reported) { + return _('never reported'); + } + if ('' !== trim($error)) { + return _('failed'); + } + if (!empty($missing)) { + return _('missing'); + } + if (!empty($extra)) { + // Not a fault on its own: only mode 2 claims to own every + // printer on the machine, and in mode 1 an extra queue is + // somebody's own printer that FOG was never asked to manage. + return _('extra'); + } + return _('ok'); + } +} diff --git a/tests/agent-printer-facts.test.php b/tests/agent-printer-facts.test.php index c463294bc4..8b3b5a6dff 100644 --- a/tests/agent-printer-facts.test.php +++ b/tests/agent-printer-facts.test.php @@ -299,4 +299,92 @@ function pfReport($db, array $block, array $current = []) === \FOG\Agent\PrinterFacts::class ); +// ---------------------------------------------------- the report's verdicts + +/** + * Call a protected static on the report. + * + * @param string $name the method + * @param array $args the arguments + * + * @return mixed + */ +function pd($name, array $args) +{ + $m = new \ReflectionMethod(\FOG\Reports\Printer_Deployment::class, $name); + $m->setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +// "never reported" is FOG not knowing, and it has to outrank everything: a +// host that has said nothing has no missing printers and no extra ones, and +// letting it fall through to 'ok' would report agreement with a machine +// nobody has heard from. +$t->check( + 'a host that never reported says so, whatever else is true of it', + _('never reported') === pd('state', [false, ['A'], ['B'], 'boom']) +); +// Ordered by what to act on first. An error says WHY; a missing printer is +// something somebody asked for and did not get; an extra one in mode 1 is +// just somebody's own printer. +$t->check( + 'a recorded error outranks a missing printer', + _('failed') === pd('state', [true, ['A'], [], 'boom']) +); +$t->check( + 'a missing printer outranks an extra one', + _('missing') === pd('state', [true, ['A'], ['B'], '']) +); +$t->check( + 'an extra printer alone is reported as extra', + _('extra') === pd('state', [true, [], ['B'], '']) +); +$t->check( + 'and a host with neither is ok', + _('ok') === pd('state', [true, [], [], '']) +); +$t->check( + 'whitespace is not an error', + _('ok') === pd('state', [true, [], [], ' ']) +); + +// The join hangs off hostSpooler, and nothing above can see it: these +// checks run against a fake connection, so the SQL is never executed. It is +// checked as source because getting it wrong is silent and expensive -- a +// join to hostPrinter loses every host that answered "nothing installed", +// which is the failure the second table exists to prevent. Proven on the lab +// database too (background_scripts/prove_printer_facts_end_to_end.php, where +// this same mutation fails nine checks); this is the half that runs in CI. +$reportSrc = (string)file_get_contents( + dirname(__DIR__) . '/packages/web/src/Reports/Printer_Deployment.php' +); +$t->check( + 'the report LEFT JOINs hostSpooler -- joining hostPrinter instead would' + . ' silently drop every host that reported no queues, which is the' + . ' one this report most needs to show', + false !== strpos( + $reportSrc, + 'LEFT OUTER JOIN `hostSpooler` ON `hspHostID` = `hostID`' + ) +); +$t->check( + 'and it is a LEFT join, so a host that never reported is a row rather' + . ' than an absence', + false === strpos($reportSrc, 'INNER JOIN `hostSpooler`') +); + +// hostPrinterLevel stores 0/1/2 and the wire has always sent 0/a/ar -- two +// vocabularies for one setting, neither written down where an admin can see +// it (design 0010 section 1.3). This is the third and the only one meant for +// a person, so it has to cover every stored value including the empty string +// a 1.5-origin row carries. +foreach ([0 => 'off', 1 => 'assigned', 2 => 'exclusive', '' => 'off', + '9' => 'off'] as $level => $want) { + $t->check( + "printer level '" . $level . "' reads as " . $want, + _($want) === pd('mode', [$level]) + ); +} + $t->finish(); From 8e1f603275ef38902123a7ffcc5e6927aeb27104 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 20:39:19 +0000 Subject: [PATCH 071/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 17593fa854..6c47017fcf 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index b60f7636d1..08367a9fd3 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10389,6 +10389,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 6ba69784ec..71a7c3872f 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10548,6 +10548,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 534cdbe532..431ba2c8fe 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10381,6 +10381,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 1e301fc1e4..94042afdfe 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10373,6 +10373,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index d4d3efea54..ad4b664da7 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10096,6 +10096,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 1f8d5f6778..7e0baa8c0d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10051,6 +10051,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index a0bbf7b7fe..ca073bcfb2 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8897,6 +8897,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index a9d1ecb462..347e33b336 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10376,6 +10376,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 63fee9b433..915e4b8289 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10376,6 +10376,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From b75a378b8be1c36f3148d49508183666c9ae9866 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:26:38 -0500 Subject: [PATCH 072/117] getSetting() returns null for a missing key; say so and cast at the users The docblock promised `string|array`, but both return paths hand back null when the row is absent -- the array form does it deliberately, with a comment explaining that null rather than '' keeps the single-key and multi-key shapes agreeing. That mattered here. PHPStan read the annotation and called UserSessions::compatWrites()'s `null === $set` branch dead, and the obvious way to clear the error would have been to delete the check. The check is load-bearing: without it an unset FOG_AGENT_USERSESSION_COMPAT returns (bool)null, so the setting the docblock calls "Defaults to ON" would have defaulted to off. Correcting the annotation surfaced nine daemons assigning the value straight into an `@var int` property. Cast at those sites, which is what each already meant -- every one of them immediately compares `< 1`, and null and '0' and 0 all took the disabled branch before, so no behavior moves. Also drop \LDAP\Connection from FOGLdap::$_ld. ldap_connect() returns that object from PHP 8.1 but a resource on 7.4, and phpstan.neon pins phpVersion.min at 70400, where the class does not exist. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- packages/web/src/Base/FOGBase.php | 4 +++- packages/web/src/Net/FOGLdap.php | 7 ++++++- packages/web/src/Service/FOGItemScanner.php | 2 +- packages/web/src/Service/FOGReplicator.php | 2 +- packages/web/src/Service/FOGService.php | 2 +- packages/web/src/Service/FileDeleter.php | 2 +- packages/web/src/Service/MulticastManager.php | 2 +- packages/web/src/Service/PingHosts.php | 2 +- packages/web/src/Service/PluginRunner.php | 2 +- packages/web/src/Service/RetentionRunner.php | 2 +- packages/web/src/Service/TaskScheduler.php | 2 +- 11 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/web/src/Base/FOGBase.php b/packages/web/src/Base/FOGBase.php index e4c5a2d09f..d2361409fc 100644 --- a/packages/web/src/Base/FOGBase.php +++ b/packages/web/src/Base/FOGBase.php @@ -3666,7 +3666,9 @@ private static function _warmFromFileCache() * * @throws Exception * - * @return string|array + * @return string|array|null the value, null when the key has no row; the + * array form holds one entry per requested key, + * null in the slots that are missing */ public static function getSetting($key) { diff --git a/packages/web/src/Net/FOGLdap.php b/packages/web/src/Net/FOGLdap.php index fd76d752b7..d4b6242965 100644 --- a/packages/web/src/Net/FOGLdap.php +++ b/packages/web/src/Net/FOGLdap.php @@ -58,7 +58,12 @@ class FOGLdap /** * The connection, once bound. * - * @var resource|\LDAP\Connection|null + * ldap_connect() hands back a resource on PHP 7.4 and an LDAP\Connection + * object from 8.1 on. The analyzed range starts at 7.4, where that class + * does not exist, so it cannot be named here -- phpstan.neon pins + * phpVersion.min at 70400 and reports it as an unknown class. + * + * @var resource|null */ private $_ld = null; diff --git a/packages/web/src/Service/FOGItemScanner.php b/packages/web/src/Service/FOGItemScanner.php index eefa4bb8ea..cfe81461d3 100644 --- a/packages/web/src/Service/FOGItemScanner.php +++ b/packages/web/src/Service/FOGItemScanner.php @@ -210,7 +210,7 @@ private function _commonOutput() try { // Re-read every pass: a daemon must notice the setting being // turned off without needing a restart. - self::$_scanOn = self::getSetting( + self::$_scanOn = (int) self::getSetting( $this->d('prefix') . 'GLOBALENABLED' ); if (self::$_scanOn < 1) { diff --git a/packages/web/src/Service/FOGReplicator.php b/packages/web/src/Service/FOGReplicator.php index 533076f694..dbb4f7b6be 100644 --- a/packages/web/src/Service/FOGReplicator.php +++ b/packages/web/src/Service/FOGReplicator.php @@ -201,7 +201,7 @@ private function _commonOutput() try { // Re-read every pass: a daemon must notice the setting being // turned off without needing a restart. - self::$_repOn = self::getSetting( + self::$_repOn = (int) self::getSetting( $this->_d('prefix') . 'GLOBALENABLED' ); if (self::$_repOn < 1) { diff --git a/packages/web/src/Service/FOGService.php b/packages/web/src/Service/FOGService.php index db5425b254..5c205973c0 100644 --- a/packages/web/src/Service/FOGService.php +++ b/packages/web/src/Service/FOGService.php @@ -436,7 +436,7 @@ public function serviceStart() public function serviceRun() { $this->waitDbReady(); - $tmpTime = self::getSetting(static::$sleeptime); + $tmpTime = (int) self::getSetting(static::$sleeptime); if (static::$zzz != $tmpTime) { static::$zzz = $tmpTime; self::outall( diff --git a/packages/web/src/Service/FileDeleter.php b/packages/web/src/Service/FileDeleter.php index 06560cce54..511ab01a35 100644 --- a/packages/web/src/Service/FileDeleter.php +++ b/packages/web/src/Service/FileDeleter.php @@ -162,7 +162,7 @@ private static function formatRunTime($time) private function _commonOutput() { try { - self::$_schedOn = self::getSetting('FILEDELETEQUEUEGLOBALENABLED'); + self::$_schedOn = (int) self::getSetting('FILEDELETEQUEUEGLOBALENABLED'); if (self::$_schedOn < 1) { throw new \Exception(_(' * File delete queue is globally disabled')); } diff --git a/packages/web/src/Service/MulticastManager.php b/packages/web/src/Service/MulticastManager.php index fc88e0d9f3..adb56cced1 100644 --- a/packages/web/src/Service/MulticastManager.php +++ b/packages/web/src/Service/MulticastManager.php @@ -311,7 +311,7 @@ private function _serviceLoop() ]; // Check if status changed. - self::$_mcOn = self::getSetting('MULTICASTGLOBALENABLED'); + self::$_mcOn = (int) self::getSetting('MULTICASTGLOBALENABLED'); try { // Any sender still recorded against a node we master diff --git a/packages/web/src/Service/PingHosts.php b/packages/web/src/Service/PingHosts.php index 1014507357..b295dbe30b 100644 --- a/packages/web/src/Service/PingHosts.php +++ b/packages/web/src/Service/PingHosts.php @@ -125,7 +125,7 @@ public function __construct() private function _commonOutput() { try { - self::$_pingOn = self::getSetting('PINGHOSTGLOBALENABLED'); + self::$_pingOn = (int) self::getSetting('PINGHOSTGLOBALENABLED'); if (self::$_pingOn < 1) { throw new \Exception(_(' * Ping hosts is globally disabled')); } diff --git a/packages/web/src/Service/PluginRunner.php b/packages/web/src/Service/PluginRunner.php index 6250d157ff..61b9085e7d 100644 --- a/packages/web/src/Service/PluginRunner.php +++ b/packages/web/src/Service/PluginRunner.php @@ -333,7 +333,7 @@ private function _runTask($key, PluginTask $task) public function serviceRun() { try { - self::$_runnerOn = self::getSetting('PLUGINRUNNERGLOBALENABLED'); + self::$_runnerOn = (int) self::getSetting('PLUGINRUNNERGLOBALENABLED'); if (self::$_runnerOn < 1) { throw new \Exception( _('Plugin runner is globally disabled') diff --git a/packages/web/src/Service/RetentionRunner.php b/packages/web/src/Service/RetentionRunner.php index eee6bfc57b..ab28853455 100644 --- a/packages/web/src/Service/RetentionRunner.php +++ b/packages/web/src/Service/RetentionRunner.php @@ -244,7 +244,7 @@ public function serviceRun() // THREW is a fault and must not be throttled away behind an // unchanged-reason check. try { - self::$_runnerOn = self::getSetting('RETENTIONGLOBALENABLED'); + self::$_runnerOn = (int) self::getSetting('RETENTIONGLOBALENABLED'); if (self::$_runnerOn < 1) { throw new \Exception( _('Retention is globally disabled') diff --git a/packages/web/src/Service/TaskScheduler.php b/packages/web/src/Service/TaskScheduler.php index 6c71d8e212..0339ea0de2 100644 --- a/packages/web/src/Service/TaskScheduler.php +++ b/packages/web/src/Service/TaskScheduler.php @@ -100,7 +100,7 @@ public function __construct() private function _commonOutput() { try { - self::$_schedOn = self::getSetting('SCHEDULERGLOBALENABLED'); + self::$_schedOn = (int) self::getSetting('SCHEDULERGLOBALENABLED'); if (self::$_schedOn < 1) { throw new \Exception(_(' * Task Scheduler is globally disabled')); } From 2c4e7811432e4fa5c227fe2d7bf7fe229c7ec3ae Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:26:42 -0500 Subject: [PATCH 073/117] Baseline the eight folded assertions in the two agent test files PHPStan reads both sides of these comparisons statically and calls them always-true, but each one is a pin: `KINDS === ['ad','entra','workgroup', 'none']`, `FACT_REPORTS['directory'] === DirectoryFacts::class`, `REPORT_NODES['directory_membership'] === 'host'`, and the negative `!in_array(END_INFERRED, AGENT_END_REASONS)`. They are exactly what a gate test is supposed to look like, and rewriting them to satisfy the analyser would delete the thing they protect. Dropping 'none' from DirectoryFacts::KINDS turns agent-directory.test.php red on that check, which is the behavior to keep. So they go in the baseline, where this repository already keeps the same identifiers for tests/ -- fourteen identical.alwaysTrue, sixteen function.alreadyNarrowedType, six nullCoalesce.offset and five function.impossibleType were there before these eight. Entries are inserted in path order rather than regenerated, so no unrelated drift is swept in. Both passes are clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- phpstan-tests-baseline.neon | 48 +++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/phpstan-tests-baseline.neon b/phpstan-tests-baseline.neon index 28f45811ce..cca8767f8d 100644 --- a/phpstan-tests-baseline.neon +++ b/phpstan-tests-baseline.neon @@ -12,6 +12,54 @@ parameters: count: 1 path: tests/activity-sources.test.php + - + message: '#^Offset ''directory'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-directory.test.php + + - + message: '#^Offset ''directory_membership'' on array\{hosts_and_users\: ''usertracking'', run_history\: ''task'', imaging_report\: ''task'', snapin_report\: ''snapin'', software_report\: ''software'', fleet_report\: ''host'', hardware_report\: ''host'', installed_software\: ''host'', \.\.\.\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-directory.test.php + + - + message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\DirectoryFacts'' and ''FOG\\\\Agent\\\\DirectoryFacts'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-directory.test.php + + - + message: '#^Strict comparison using \=\=\= between ''host'' and ''host'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-directory.test.php + + - + message: '#^Strict comparison using \=\=\= between array\{''ad'', ''entra'', ''workgroup'', ''none''\} and array\{''ad'', ''entra'', ''workgroup'', ''none''\} will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-directory.test.php + + - + message: '#^Call to function in_array\(\) with arguments ''inferred'', array\{''logout'', ''disconnect'', ''service_stop''\} and true will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: tests/agent-user-sessions.test.php + + - + message: '#^Call to function method_exists\(\) with ''FOG\\\\Agent\\\\State'' and ''sessions'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + count: 1 + path: tests/agent-user-sessions.test.php + + - + message: '#^Strict comparison using \=\=\= between ''usertracker'' and ''usertracker'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-user-sessions.test.php + - message: '#^Call to new Initiator\(\) on a separate line has no effect\.$#' identifier: new.resultUnused From 4effee335e1f6d163240e73c109724d878e0f365 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:26:43 -0500 Subject: [PATCH 074/117] Record the eleven constraints this branch adds in the rehearsal baseline The branch's new tables bring the map's applicable relationships from 83 to 94, and all eleven land, so present moves 81 to 92 with them. MISSING stays at the same two entries the fixture has always carried -- the hostMAC bigint and the one nfsGroupMembers orphan -- and the integrity block below them does not move at all, so nothing regressed and nothing new was refused. Verified by running tests/upgrade-rehearsal-ci.test.sh against a throwaway MariaDB 11.8, which reproduced CI's diff exactly before the edit and matches after it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- tests/fixtures/upgrade-rehearsal-baseline.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/upgrade-rehearsal-baseline.txt b/tests/fixtures/upgrade-rehearsal-baseline.txt index ce719b4ab7..b652c3f6f8 100644 --- a/tests/fixtures/upgrade-rehearsal-baseline.txt +++ b/tests/fixtures/upgrade-rehearsal-baseline.txt @@ -1,5 +1,5 @@ - constraints declared and applicable here: 83 - constraints actually present: 81 + constraints declared and applicable here: 94 + constraints actually present: 92 MISSING: 2 fk_hostMAC_hmHostID CASCADE orphan rows: 0 fk_nfsGroupMembers_ngmGroupID RESTRICT orphan rows: 1 From 4957881fd2944cd1bf543f135a984cc33a2862d5 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:38:34 -0500 Subject: [PATCH 075/117] Carry the printer tables through the three baselines d68014da1 landed schema 426 and the printer fact report while this branch was being cleaned up, and it moves all three of the numbers the gates pin: - commons/schema.php gains a step, so the ignored $this pattern occurs 396 times rather than 395; - State::FACT_REPORTS gains a 'printers' key, which rewrites the message text of an already-baselined offset check in agent-directory.test.php and adds two of the same folded-assertion kind in the new agent-printer-facts.test.php; - the printer tables declare two more foreign keys, both of which land, so the rehearsal report reads 96 applicable and 94 present. MISSING is still the same two entries and the integrity block below them does not move, so nothing regressed. Both PHPStan passes are clean and the rehearsal matches, verified against MariaDB 11.8. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- phpstan-baseline.neon | 2 +- phpstan-tests-baseline.neon | 14 +++++++++++++- tests/fixtures/upgrade-rehearsal-baseline.txt | 4 ++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index d7faa97337..422f76feb8 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -129,7 +129,7 @@ parameters: - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 395 + count: 396 path: packages/web/commons/schema.php - diff --git a/phpstan-tests-baseline.neon b/phpstan-tests-baseline.neon index cca8767f8d..c7aae2e2e5 100644 --- a/phpstan-tests-baseline.neon +++ b/phpstan-tests-baseline.neon @@ -13,7 +13,7 @@ parameters: path: tests/activity-sources.test.php - - message: '#^Offset ''directory'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts''\} on left side of \?\? always exists and is not nullable\.$#' + message: '#^Offset ''directory'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts''\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset count: 1 path: tests/agent-directory.test.php @@ -42,6 +42,18 @@ parameters: count: 1 path: tests/agent-directory.test.php + - + message: '#^Offset ''printers'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\PrinterFacts'' and ''FOG\\\\Agent\\\\PrinterFacts'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-printer-facts.test.php + - message: '#^Call to function in_array\(\) with arguments ''inferred'', array\{''logout'', ''disconnect'', ''service_stop''\} and true will always evaluate to false\.$#' identifier: function.impossibleType diff --git a/tests/fixtures/upgrade-rehearsal-baseline.txt b/tests/fixtures/upgrade-rehearsal-baseline.txt index b652c3f6f8..4aaaea8655 100644 --- a/tests/fixtures/upgrade-rehearsal-baseline.txt +++ b/tests/fixtures/upgrade-rehearsal-baseline.txt @@ -1,5 +1,5 @@ - constraints declared and applicable here: 94 - constraints actually present: 92 + constraints declared and applicable here: 96 + constraints actually present: 94 MISSING: 2 fk_hostMAC_hmHostID CASCADE orphan rows: 0 fk_nfsGroupMembers_ngmGroupID RESTRICT orphan rows: 1 From 8888e113d58777067165583bdf31c62297306711 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:47:51 -0500 Subject: [PATCH 076/117] Seed paIsDefault with a value the column can now hold Schema step 426 converts printerAssoc.paIsDefault from varchar(2) to tinyint(1), normalizing '' to '0' first so the upgrade survives strict mode. The resolver fixture still seeded '' directly into the converted column, so MariaDB 11.8 refused the INSERT with 1366 and the test died before it asserted anything. The '' was deliberate -- it stood for the spelling a 1.5-origin column really held, and the resolver must not read it as truthy. That state is no longer reachable: the column is an integer, and any row that held '' was rewritten by 426's own UPDATE. So the row seeds 0 and the comment records why the case retired rather than leaving a fixture that reads like coverage and is really an impossible state. The quoted '0' stays, because a loose comparison reading it as truthy would still pick the wrong default, and that is the check this block exists for. Verified against MariaDB 11.8: the old fixture reproduces CI's PDOException, the new one passes all 30 checks. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- tests/assign-resolver.test.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/tests/assign-resolver.test.php b/tests/assign-resolver.test.php index cfa8b11df1..19e0945dfa 100644 --- a/tests/assign-resolver.test.php +++ b/tests/assign-resolver.test.php @@ -339,11 +339,15 @@ function () use ($pdo, $db) { * holds rather than ints that happen to compare equal. * * host 10: three direct printers, one of them the default -> direct wins. - * The two that are NOT the default carry '0' and '' respectively, - * which are both spellings a 1.5-origin varchar(2) column really - * holds. A comparison loose enough to read either as truthy makes - * the FIRST printer the default, which is a wrong answer nothing - * would report. + * The two that are NOT the default carry '0' and 0. It used to be + * '0' and '', the two spellings a 1.5-origin varchar(2) really + * held, but schema step 426 normalizes '' to '0' and then MODIFYs + * the column to tinyint(1) -- so '' is no longer a value this + * column can hold, and seeding it is refused outright by MariaDB + * in strict mode rather than exercising anything. The check that + * remains is the one that still can fail: a comparison loose + * enough to read a quoted '0' as truthy makes the FIRST printer + * the default, which is a wrong answer nothing would report. * host 11: no direct printers; group 4 (alpha, first) names no default, * group 5 (beta) does -> beta's default, and only because alpha * declined it. @@ -351,7 +355,7 @@ function () use ($pdo, $db) { */ $pdo->exec( "INSERT INTO `printerAssoc` (`paID`,`paHostID`,`paPrinterID`,`paIsDefault`) " - . "VALUES (1,10,900,'0'),(2,10,901,'1'),(3,10,902,'')" + . "VALUES (1,10,900,'0'),(2,10,901,'1'),(3,10,902,0)" ); $pdo->exec( "INSERT INTO `groupPrinterAssoc` " From e7474efd1a08d803b1508258054c3d3f9f0a9aba Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:49:40 -0500 Subject: [PATCH 077/117] =?UTF-8?q?Printers:=20the=20desired=20state=20and?= =?UTF-8?q?=20the=20result=20(design=200010=20=C2=A75,=20schema=20427)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The other half of the capability. PrinterFacts records what a machine SAYS IT HAS; this sends what it SHOULD have, and keeps what happened when it tried. FOG has had neither until now: an install that failed produced nothing an admin could see, and the client retried the same thing on the next poll, forever. pURI is the column that makes design 0010 §2 real -- a printer described the way both spoolers already describe one, so a Windows row and a CUPS row for the same physical device can be recognized as the same device. It is also the only way to say DRIVERLESS (IPP Everywhere): FOG has pDefFile and pModel and no way to say "neither", which for a lot of estates is now the common case. DELIBERATELY NOT BACKFILLED, which is a change from what design 0010 §7 first proposed and the one decision here worth arguing with. Deriving pConfig/pIP/pPort into a stored URI once, on upgrade, bakes the derivation in -- and pPort is a longtext that has held whatever an admin typed for a decade, so some rows WILL derive wrong. A wrong answer written into a column has to be found and corrected by hand on every install; a wrong answer computed in Printer::uri() is fixed for everybody by fixing the method. The column holds only what an admin explicitly set, an empty one keeps following the type fields, and the form field says so. Proven on the live install rather than only in the suite: a printer created with no pURI at all -- a printer as every existing install has them -- comes down the wire as socket://10.0.4.20:9100, and setting pURI overrides it. Thirteen checks (background_scripts/prove_printer_desired_state.php), which also walks the result path: a failure lands in paError, a later success CLEARS it, a host reporting on a printer it was never assigned gets a 404, and an invented status gets a 400. The capability is gated on FOG's EXISTING printermanager module, not a new switch. Admins have been turning that one off for a decade and know where it is, so every host's current choice carries over untouched. Mutating it to a new short name fails the suite. Results are written onto the ASSOCIATION rather than a status table of their own, because the outcome belongs to "this host was told to have this printer" and should die with it -- unassigning takes the failure with it through the CASCADE, for free. A printer reaching the host through a GROUP grant has no host-direct row to stamp; that result still gets its audit line. errorFor() is a method rather than an inline condition so the clearing rule could be tested as behaviour instead of by reading the source for it: seven mutations run against this change, all caught, including "failed counted as settled" -- which would mean an error could never be recorded at all, and which no source-text check would have noticed. Suite: 329 passed, 1 failed -- certificate-table.test.php, which fails on working-1.6 too and is untouched here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- bin/psr4-scan.php | 1 + packages/web/commons/schema-expected.php | 3 +- packages/web/commons/schema.php | 26 ++ .../js/fog/printer/fog.printer.export.js | 1 + .../de_DE.UTF-8/LC_MESSAGES/messages.po | 7 +- .../en_US.UTF-8/LC_MESSAGES/messages.po | 7 +- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 7 +- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 7 +- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 7 +- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 7 +- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 11 +- .../web/management/languages/messages.pot | 7 +- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 7 +- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 7 +- packages/web/src/Agent/PrinterSet.php | 275 ++++++++++++++++++ packages/web/src/Agent/State.php | 16 +- packages/web/src/Base/System.php | 2 +- packages/web/src/Items/Printer.php | 78 ++++- packages/web/src/Pages/PrinterManagement.php | 44 ++- tests/agent-printer-facts.test.php | 155 ++++++++++ tests/fixtures/route-column-contract.txt | 1 + 21 files changed, 657 insertions(+), 19 deletions(-) create mode 100644 packages/web/src/Agent/PrinterSet.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 3b69f60545..9f0368cb96 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -229,6 +229,7 @@ // class for the assignable printer -- this writes hostPrinter and // hostSpooler rows, it is not that row. 'PrinterFacts' => 'Agent', + 'PrinterSet' => 'Agent', 'UserSessions' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 71ca84ec62..4e303467d3 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -825,7 +825,7 @@ ], ], 'printers' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `printers` ( `pID` int(11) NOT NULL AUTO_INCREMENT, `pPort` longtext NOT NULL DEFAULT \'\', `pDefFile` longtext NOT NULL DEFAULT \'\', `pModel` varchar(250) NOT NULL DEFAULT \'\', `pAlias` varchar(250) NOT NULL, `pConfig` varchar(10) NOT NULL DEFAULT \'\', `pConfigFile` varchar(255) NOT NULL DEFAULT \'\', `pIP` varchar(255) NOT NULL DEFAULT \'\', `pDesc` longtext DEFAULT NULL, `pURI` varchar(1024) NOT NULL DEFAULT \'\', PRIMARY KEY (`pID`), UNIQUE KEY `pAlias` (`pAlias`), KEY `new_index1` (`pModel`), KEY `new_index2` (`pAlias`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'pID' => 'int(11) NOT NULL', 'pPort' => 'longtext NOT NULL DEFAULT \'\'', @@ -836,6 +836,7 @@ 'pConfigFile' => 'varchar(255) NOT NULL DEFAULT \'\'', 'pIP' => 'varchar(255) NOT NULL DEFAULT \'\'', 'pDesc' => 'longtext DEFAULT NULL', + 'pURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', ], ], 'pxeMenu' => [ diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 0b11d31746..7facbdf99e 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -11331,3 +11331,29 @@ function () { . "DROP COLUMN `pAnon2`, DROP COLUMN `pAnon3`, DROP COLUMN `pAnon4`, " . "DROP COLUMN `pAnon5`", ]; + +// 427 +$this->schema[] = [ + // Design 0010 section 2: a printer is a device URI and a driver, which + // is how both spoolers already describe one. This is the column that + // makes a printer row portable -- the same physical device is a TCP/IP + // port on Windows and a socket:// device URI on CUPS, and until they are + // written the same way nothing can tell they are the same printer. + // + // It is also the only way to express a DRIVERLESS printer (IPP + // Everywhere), where the device describes its own capabilities and no + // driver file exists. FOG's model has pDefFile and pModel and no way to + // say "neither", which for a lot of estates is now the common case. + // + // DELIBERATELY NOT BACKFILLED, and this is a change from what design + // 0010 section 7 first proposed. Deriving pConfig/pIP/pPort into a + // stored URI once, on upgrade, bakes the derivation in: pPort is a + // longtext that has held whatever an admin typed for a decade, so some + // rows will derive wrong, and a stored wrong answer has to be found and + // corrected by hand on every install. Items\Printer::uri() derives on + // read instead, so this column holds only what an admin explicitly set, + // an empty one keeps following the type-specific fields, and fixing the + // derivation fixes every printer at once. + "ALTER TABLE `printers` " + . "ADD COLUMN `pURI` varchar(1024) NOT NULL DEFAULT ''", +]; diff --git a/packages/web/management/js/fog/printer/fog.printer.export.js b/packages/web/management/js/fog/printer/fog.printer.export.js index 5730f64d60..506f796ace 100644 --- a/packages/web/management/js/fog/printer/fog.printer.export.js +++ b/packages/web/management/js/fog/printer/fog.printer.export.js @@ -8,6 +8,7 @@ {data: 'config'}, {data: 'configFile', visible: false}, {data: 'ip', visible: false}, + {data: 'uri', visible: false}, {data: 'associations', visible: false} ]); })(jQuery); diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 6c47017fcf..340aa40578 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -2733,6 +2733,9 @@ msgstr "Zerstörung fehlgeschlagen: %s" msgid "Details" msgstr "Details" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Gerätename muss eine Zeichenfolge sein." @@ -7034,6 +7037,9 @@ msgstr "Operationsfeld nicht gesetzt: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10380,7 +10386,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 08367a9fd3..eb2ed87467 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -2736,6 +2736,9 @@ msgstr "Destroy failed: %s" msgid "Details" msgstr "Snapin Return Detail" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Event must be a string" @@ -7045,6 +7048,9 @@ msgstr "Operation Field not set: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10389,7 +10395,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 71a7c3872f..ce7e31df14 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -2763,6 +2763,9 @@ msgstr "Destruir fallado: %s" msgid "Details" msgstr "" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Evento debe ser una cadena" @@ -7152,6 +7155,9 @@ msgstr "Operación de no activado: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10548,7 +10554,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 431ba2c8fe..157fa3b315 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -2733,6 +2733,9 @@ msgstr "Zerstörung fehlgeschlagen: %s" msgid "Details" msgstr "Details" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Gerätename muss eine Zeichenfolge sein." @@ -7035,6 +7038,9 @@ msgstr "Operationsfeld nicht gesetzt: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10381,7 +10387,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 94042afdfe..a96f82d532 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -2736,6 +2736,9 @@ msgstr "Destroy a échoué: %s" msgid "Details" msgstr "Snapin Retour Détail" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Événement doit être une chaîne" @@ -7030,6 +7033,9 @@ msgstr "Opération pas définie: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10373,7 +10379,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index ad4b664da7..0a5ff78081 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -2672,6 +2672,9 @@ msgstr "Deistruzione fallita" msgid "Details" msgstr "Dettagli della macchina" +msgid "Device URI" +msgstr "" + msgid "Device must be a string" msgstr "Dispositivo deve essere una stringa" @@ -6845,6 +6848,9 @@ msgstr "Campo operazione non impostato" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10096,7 +10102,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 7e0baa8c0d..fd105b3ff6 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -2659,6 +2659,10 @@ msgstr "解除に失敗しました" msgid "Details" msgstr "マシン詳細" +#, fuzzy +msgid "Device URI" +msgstr "HD デバイス" + msgid "Device must be a string" msgstr "デバイスは文字列で指定してください" @@ -6821,6 +6825,9 @@ msgstr "操作フィールドが設定されていません" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10051,7 +10058,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13249,9 +13255,6 @@ msgstr "" #~ msgid "Groups are not allowed to schedule upload tasks" #~ msgstr "グループではアップロードタスクをスケジュールできません" -#~ msgid "HD Device" -#~ msgstr "HD デバイス" - #~ msgid "HD Firmware" #~ msgstr "HD ファームウェア" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index ca073bcfb2..fa768e4fb1 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -2361,6 +2361,9 @@ msgstr "" msgid "Details" msgstr "" +msgid "Device URI" +msgstr "" + msgid "Device must be a string" msgstr "" @@ -6032,6 +6035,9 @@ msgstr "" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -8897,7 +8903,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 347e33b336..5c579bc68c 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -2736,6 +2736,9 @@ msgstr "Destrua falhou: %s" msgid "Details" msgstr "Detalhe Snapin Retorno" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "Evento deve ser uma string" @@ -7032,6 +7035,9 @@ msgstr "Campo operação não definido: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10376,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 915e4b8289..ae783ac9f2 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -2736,6 +2736,9 @@ msgstr "摧毁失败: %s" msgid "Details" msgstr "管理单元返回详细" +msgid "Device URI" +msgstr "" + #, fuzzy msgid "Device must be a string" msgstr "事件必须是字符串" @@ -7032,6 +7035,9 @@ msgstr "操作字段没有设置: %s" msgid "Optional. A feed URL, folder or share passed to the package manager as --source. Leave empty for the manager's own configured sources." msgstr "" +msgid "Optional. Leave empty to derive it from the type and address above. Examples:" +msgstr "" + msgid "Optional. Pick a type to pre-fill the command fields below; you can still edit them afterward. The Chocolatey entry expects the uploaded file to be a Chocolatey packages.config naming the packages to install." msgstr "" @@ -10376,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/PrinterSet.php b/packages/web/src/Agent/PrinterSet.php new file mode 100644 index 0000000000..cc1bd05af3 --- /dev/null +++ b/packages/web/src/Agent/PrinterSet.php @@ -0,0 +1,275 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Assign\Resolver; +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; +use FOG\Items\Printer; +use FOG\Items\PrinterAssociation; +use FOG\Router\Route; + +/** + * The printers capability (design 0010 section 5): a desired set of queues + * the host is held to, described as a device URI and a driver, with an + * outcome recorded per assignment. + * + * Like SoftwareSet and unlike a snapin, nothing here is a task: the set is + * read fresh on every state fetch, the agent converges it, and a report + * refreshes one row per host and printer. + * + * The contrast to draw is with PrinterFacts next door: that records what the + * machine SAYS IT HAS. This sends what it SHOULD have, and keeps what + * happened when it tried. FOG has had neither half until now -- an install + * that failed produced nothing an admin could see, and the client retried + * the same thing on the next poll, forever. + * + * @category PrinterSet + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class PrinterSet extends FOGBase +{ + /** + * The three modes, in words. + * + * `hostPrinterLevel` stores 0, 1 or 2, and the legacy wire has always + * sent 0, `a` or `ar` -- two vocabularies for one setting, neither of + * them written down anywhere an admin can see (design 0010 section 1.3). + * The agent gets a third that says what it means, and the legacy + * endpoint keeps sending what it always sent. + */ + const MODE_OFF = 'off'; + const MODE_ASSIGNED = 'assigned'; + const MODE_EXCLUSIVE = 'exclusive'; + const MODES = [0 => self::MODE_OFF, 1 => self::MODE_ASSIGNED, + 2 => self::MODE_EXCLUSIVE]; + + /** + * What the agent may report for one printer. + * + * `converged` means nothing needed doing, which is the resting state and + * the overwhelmingly common report. The rest are one action each, plus + * the two ways a provider can decline to act at all. + */ + const STATUS_CONVERGED = 'converged'; + const STATUSES = [ + 'converged', 'installed', 'updated', 'removed', 'failed', + 'unsupported' + ]; + + /** + * The statuses that mean the printer is now as it should be, so any + * error recorded against it is stale. + */ + const SETTLED_STATUSES = ['converged', 'installed', 'updated', 'removed']; + + /** + * Longest error message kept. A provider's stderr can run to pages; the + * column is a varchar(255) because this is a line an admin reads in a + * report, not a log. + */ + const MAX_ERROR = 255; + + /** + * The desired set for a host, with the mode. + * + * Resolved through Resolver::resolvePrinters -- the same call + * PrinterClient makes for the legacy client -- so the two clients cannot + * be told different things about the same host. + * + * @param Host $Host the principal + * + * @return array + */ + public static function desired(Host $Host) + { + $hostID = (int)$Host->get('id'); + $level = (int)$Host->get('printerLevel'); + $resolved = Resolver::resolvePrinters([$hostID])[$hostID] + ?? ['printers' => [], 'default' => null]; + + $printers = []; + $default = ''; + foreach ((array)($resolved['printers'] ?? []) as $id) { + $Printer = new Printer((int)$id); + if (!$Printer->isValid()) { + continue; + } + $name = (string)$Printer->get('name'); + $printers[] = [ + 'id' => (int)$Printer->get('id'), + 'name' => $name, + // Derived on read when nothing was set explicitly, so a + // printer created years ago against pConfig/pIP/pPort works + // without anybody editing it (Items\Printer::uri()). + 'uri' => $Printer->uri(), + // Empty means driverless (IPP Everywhere), which is a value + // and not a missing field. + 'driver' => $Printer->driver() + ]; + if (null !== ($resolved['default'] ?? null) + && (int)$resolved['default'] === (int)$id + ) { + $default = $name; + } + } + + return [ + 'manage' => self::MODES[$level] ?? self::MODE_OFF, + 'default' => $default, + 'printers' => $printers + ]; + } + + /** + * Records what the agent did about one assigned printer. + * + * Written onto the ASSOCIATION rather than a status table of its own, + * because the outcome belongs to "this host was told to have this + * printer" and dies with it: unassigning the printer should take the + * failure with it, and a CASCADE on printerAssoc does that for free. + * + * @param Host $Host the host the certificate bound + * @param int $printerID the printer reported on + * @param array $body the reported result + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return string the status recorded + */ + public static function report(Host $Host, $printerID, array $body) + { + $hostID = (int)$Host->get('id'); + $printerID = (int)$printerID; + + // The host may only report on printers it was actually told to + // have. Checked against the resolver rather than against + // printerAssoc directly, so a printer reaching the host through a + // GROUP grant is accepted -- and one reaching it through neither is + // a host reporting on somebody else's row. + $resolved = Resolver::resolvePrinters([$hostID])[$hostID] + ?? ['printers' => []]; + $set = array_map('intval', (array)($resolved['printers'] ?? [])); + if (!in_array($printerID, $set, true)) { + throw new \RuntimeException('not in this host\'s printer set', 404); + } + + $status = (string)($body['status'] ?? ''); + if (!in_array($status, self::STATUSES, true)) { + throw new \RuntimeException('unknown status', 400); + } + $error = self::errorFor($status, (string)($body['error'] ?? '')); + + $ids = Route::getIds( + 'printerassociation', + ['hostID' => $hostID, 'printerID' => $printerID], + 'id' + ); + $id = (int)(array_shift($ids) ?: 0); + if ($id < 1) { + // Resolved through a group, so there is no host-direct row to + // stamp. The result is still real and still worth an audit line; + // it simply has nowhere on the association to live. + self::_audit($Host, $printerID, $status, $error); + return $status; + } + $Assoc = new PrinterAssociation($id); + if (!$Assoc->isValid()) { + self::_audit($Host, $printerID, $status, $error); + return $status; + } + $Assoc + // Named for the ATTEMPT, not the success: this is stamped + // whenever the agent acted, so a name like paInstalledAt would + // claim an install happened on every occasion one did not. + ->set( + 'appliedAt', + self::niceDate()->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s') + ) + ->set('error', $error) + ->save(); + + // A converged heartbeat is not news. Auditing every poll that + // reported the same nothing would bury the results that matter. + if (self::STATUS_CONVERGED !== $status) { + self::_audit($Host, $printerID, $status, $error); + } + + return $status; + } + + /** + * The error to record for a reported status. + * + * A settled status CLEARS whatever was there. Leaving it would make the + * report show an error against a printer that is now installed, which + * is worse than showing nothing at all -- an admin chasing a stale + * message is worse off than one chasing none. + * + * A failure keeps its message, truncated: a provider's stderr runs to + * pages and this is a line somebody reads in a report, not a log. + * + * @param string $status the reported status + * @param string $error the reported message + * + * @return string + */ + protected static function errorFor($status, $error) + { + if (in_array($status, self::SETTLED_STATUSES, true)) { + return ''; + } + + return substr(trim($error), 0, self::MAX_ERROR); + } + /** + * One audit line for a printer result. + * + * @param Host $Host the host + * @param int $printerID the printer + * @param string $status what the agent said it did + * @param string $error the message, if any + * + * @return void + */ + private static function _audit(Host $Host, $printerID, $status, $error) + { + $Printer = new Printer((int)$printerID); + Audit::record( + [ + 'type' => 'agent.result', + 'subjectType' => 'host', + 'subjectID' => (int)$Host->get('id'), + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'text' => substr( + sprintf( + 'printer "%s" %s%s', + (string)$Printer->get('name'), + $status, + '' === $error ? '' : ': ' . $error + ), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } +} diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 4151169a85..8f7914cf58 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -54,7 +54,12 @@ class State extends FOGBase 'taskreboot' => 'taskreboot', 'snapin' => 'snapinclient', 'software' => 'software', - 'power' => 'powermanagement' + 'power' => 'powermanagement', + // Gated on the EXISTING printermanager module, not a new switch: + // admins have been turning that one off for a decade and know + // where it is, so a host's current choice carries over untouched + // (design 0010 section 5). + 'printers' => 'printermanager' ]; /** @@ -74,6 +79,7 @@ class State extends FOGBase const ITEM_REPORTS = [ 'snapin' => Snapins::class, 'software' => SoftwareSet::class, + 'printers' => PrinterSet::class, ]; /** @@ -193,6 +199,14 @@ public static function desired(Host $Host) // it, so a reporting host does not move its own revision. $state['software'] = SoftwareSet::desired($Host); } + if (in_array('printers', $capabilities, true)) { + // The resolved printer set and the mode, in words (design 0010 + // section 5). Resolved through the same call PrinterClient + // makes, so the agent and the legacy client cannot be told + // different things about one host. Results do not touch it, so + // a reporting host does not move its own revision. + $state['printers'] = PrinterSet::desired($Host); + } if (in_array('power', $capabilities, true)) { // Design 0004. Schedules are what Client\PM hands the legacy // client: the host's own rows and its groups' grants through diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index da22d88884..836fb90a04 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 426); + define('FOG_SCHEMA', 427); define('FOG_BCACHE_VER', 360); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Items/Printer.php b/packages/web/src/Items/Printer.php index 2eefbc5221..edd3afadd6 100644 --- a/packages/web/src/Items/Printer.php +++ b/packages/web/src/Items/Printer.php @@ -48,7 +48,8 @@ class Printer extends FOGController 'model' => 'pModel', 'config' => 'pConfig', 'configFile' => 'pConfigFile', - 'ip' => 'pIP' + 'ip' => 'pIP', + 'uri' => 'pURI' ]; /** * The required fields @@ -180,6 +181,81 @@ public function isValid() } return parent::isValid(); } + /** + * The device URI this printer is reached at (design 0010 section 2). + * + * Both print subsystems already describe a printer this way: CUPS takes + * a device URI directly, and a Windows Standard TCP/IP port is the same + * information written differently. One URI therefore serves both + * platforms, where `pConfig` could serve only one -- it named a code + * path, and three of its four values throw on whichever platform the + * machine happens to be running. + * + * DERIVED ON READ when nothing was explicitly set, rather than + * backfilled once on upgrade. `pPort` is a longtext that has held + * whatever an admin typed for a decade, so a derivation WILL be wrong + * for some rows -- and a wrong answer written into a column has to be + * found and corrected by hand on every install, where a wrong answer + * computed here is fixed for everybody by fixing this method. An admin + * who sets `pURI` overrides it and is never second-guessed. + * + * Empty when nothing can be derived, which is an honest answer: a Local + * printer with no address recorded has no URI, and inventing one would + * send the agent at a machine nobody named. + * + * @return string + */ + public function uri() + { + $explicit = trim((string)$this->get('uri')); + if ('' !== $explicit) { + return $explicit; + } + $ip = trim((string)$this->get('ip')); + $port = trim((string)$this->get('port')); + switch (strtolower(trim((string)$this->get('config')))) { + case 'local': + // A TCP/IP port printer. 9100 is the RAW default and what every + // port monitor uses when no port number was recorded. + return '' === $ip ? '' : 'socket://' . $ip . ':9100'; + case 'network': + // pPort holds a UNC path: \\server\share. + $unc = str_replace('\\', '/', $port); + $unc = ltrim($unc, '/'); + return '' === $unc ? '' : 'smb://' . $unc; + case 'cups': + // The CUPS branch pointed lpadmin at lpd:///, with + // the printer's own alias as the queue name. + $name = trim((string)$this->get('name')); + return '' === $ip ? '' : 'lpd://' . $ip . '/' . $name; + case 'iprint': + // Novell/Micro Focus iPrint, driven by iprntcmd.exe and Windows + // only. Given a scheme of its own rather than forced into one of + // the others, so a provider that cannot handle it can say so + // (design 0010 section 2). + return '' === $port ? '' : 'iprint://' . $port; + default: + return ''; + } + } + /** + * The driver to print with, or empty for driverless. + * + * Empty is a VALUE here, not a missing field: modern CUPS and Windows + * both support IPP Everywhere, where the printer describes its own + * capabilities and no driver file is involved. FOG's model assumed a + * driver always exists, which is why this needs saying out loud. + * + * @return string + */ + public function driver() + { + $model = trim((string)$this->get('model')); + if ('' !== $model) { + return $model; + } + return trim((string)$this->get('file')); + } /** * Builds the printer type selector * diff --git a/packages/web/src/Pages/PrinterManagement.php b/packages/web/src/Pages/PrinterManagement.php index 81c2310f95..8023cbb571 100644 --- a/packages/web/src/Pages/PrinterManagement.php +++ b/packages/web/src/Pages/PrinterManagement.php @@ -85,6 +85,7 @@ public function getPrinterInfo() 'port' => $this->obj->get('port'), 'model' => $this->obj->get('model'), 'ip' => $this->obj->get('ip'), + 'uri' => $this->obj->get('uri'), 'config' => strtolower($this->obj->get('config')), 'configFile' => $this->obj->get('configFile') ] @@ -117,6 +118,7 @@ private function _printerFormSections(array $values) $config = $values['config'] ?? ''; $configFile = $values['configFile'] ?? ''; $model = $values['model'] ?? ''; + $uri = $values['uri'] ?? ''; if (!$config) { $config = 'Local'; } @@ -178,6 +180,30 @@ private function _printerFormSections(array $values) _('Printer Description'), 'description', $description + ), + // Design 0010 section 2. Optional, and empty is the normal + // state: Items\Printer::uri() derives one from the type and the + // address fields, so every printer created before this column + // existed keeps working with nobody editing it. Filling it in + // is the override, and it is the only way to express a + // driverless IPP printer -- which FOG's four types cannot say + // at all. + self::makeLabel( + $labelClass, + 'uri', + _('Device URI') + . '
    ' + . _('Optional. Leave empty to derive it from the type and ' + . 'address above. Examples:') + . ' socket://10.0.4.20:9100, ipp://printer.corp/ipp/print, ' + . 'smb://srv/HP4550' + ) => self::makeInput( + 'form-control printeruri-input', + 'uri', + 'socket://10.0.4.20:9100', + 'text', + 'uri', + $uri ) ]; @@ -435,6 +461,7 @@ public function add() 'port' => filter_input(INPUT_POST, 'port'), 'inf' => filter_input(INPUT_POST, 'inf'), 'ip' => filter_input(INPUT_POST, 'ip'), + 'uri' => filter_input(INPUT_POST, 'uri'), 'config' => filter_input(INPUT_POST, 'printertype'), 'configFile' => filter_input(INPUT_POST, 'configFile'), 'model' => filter_input(INPUT_POST, 'model') @@ -472,6 +499,7 @@ public function addModal() 'port' => filter_input(INPUT_POST, 'port'), 'inf' => filter_input(INPUT_POST, 'inf'), 'ip' => filter_input(INPUT_POST, 'ip'), + 'uri' => filter_input(INPUT_POST, 'uri'), 'config' => filter_input(INPUT_POST, 'printertype'), 'configFile' => filter_input(INPUT_POST, 'configFile'), 'model' => filter_input(INPUT_POST, 'model') @@ -527,6 +555,9 @@ function (&$serverFault) { $model = trim( (string)filter_input(INPUT_POST, 'model') ); + $uri = trim( + (string)filter_input(INPUT_POST, 'uri') + ); if ($printer === '') { throw new \Exception( @@ -555,7 +586,8 @@ function (&$serverFault) { ->set('port', $port) ->set('file', $inf) ->set('configFile', $configFile) - ->set('ip', $ip); + ->set('ip', $ip) + ->set('uri', $uri); if (!$Printer->save()) { $serverFault = true; throw new \Exception(_('Add printer failed!')); @@ -604,6 +636,10 @@ public function printerGeneral() 'model' => ( filter_input(INPUT_POST, 'model') ?: $this->obj->get('model') + ), + 'uri' => ( + filter_input(INPUT_POST, 'uri') ?: + $this->obj->get('uri') ) ] ); @@ -696,6 +732,9 @@ public function printerGeneralPost() $model = trim( (string)filter_input(INPUT_POST, 'model') ); + $uri = trim( + (string)filter_input(INPUT_POST, 'uri') + ); if ($printer === '') { throw new \Exception( @@ -726,7 +765,8 @@ public function printerGeneralPost() ->set('port', $port) ->set('file', $inf) ->set('configFile', $configFile) - ->set('ip', $ip); + ->set('ip', $ip) + ->set('uri', $uri); } /** * Printer hosts display. diff --git a/tests/agent-printer-facts.test.php b/tests/agent-printer-facts.test.php index 8b3b5a6dff..73504eacec 100644 --- a/tests/agent-printer-facts.test.php +++ b/tests/agent-printer-facts.test.php @@ -387,4 +387,159 @@ function pd($name, array $args) ); } +// ------------------------------------------- the desired set (design 0010 §5) + +// Every printer created before schema 427 has an empty pURI, and every one +// created after it may still. Deriving on READ rather than backfilling once +// on upgrade is the decision this exercises: pPort is a longtext that has +// held whatever an admin typed for a decade, so some derivations WILL be +// wrong -- and a wrong answer stored in a column has to be corrected by hand +// on every install, where a wrong answer computed here is fixed for +// everybody by fixing the method. +/** + * A printer built in memory. + * + * @param array $fields property => value + * + * @return \FOG\Items\Printer + */ +function pfPrinter(array $fields) +{ + $Printer = new \FOG\Items\Printer(); + foreach ($fields as $k => $v) { + $Printer->set($k, $v); + } + + return $Printer; +} + +$cases = [ + 'a TCP/IP port printer becomes socket:// on the RAW default port' => [ + ['config' => 'Local', 'ip' => '10.0.4.20'], + 'socket://10.0.4.20:9100', + ], + 'a network printer\'s UNC path becomes smb://' => [ + ['config' => 'Network', 'port' => '\\\\srv\\HP4550'], + 'smb://srv/HP4550', + ], + 'a CUPS printer keeps the lpd:// the legacy client built' => [ + ['config' => 'Cups', 'ip' => '10.0.4.20', 'name' => 'Accounts'], + 'lpd://10.0.4.20/Accounts', + ], + 'iPrint gets a scheme of its own rather than being forced into another' => [ + ['config' => 'iPrint', 'port' => 'ipp://novell/ipp/x'], + 'iprint://ipp://novell/ipp/x', + ], + 'an explicit URI overrides the derivation and is never second-guessed' => [ + ['config' => 'Local', 'ip' => '10.0.4.20', + 'uri' => 'ipps://printer.corp/ipp/print'], + 'ipps://printer.corp/ipp/print', + ], + 'a Local printer with no address has no URI, and none is invented' => [ + ['config' => 'Local', 'ip' => ''], + '', + ], + 'an unrecognized type derives nothing rather than guessing' => [ + ['config' => 'Something', 'ip' => '10.0.4.20'], + '', + ], +]; +foreach ($cases as $what => list($fields, $want)) { + $got = pfPrinter($fields)->uri(); + $t->check($what . ' [' . $got . ']', $want === $got); +} + +$t->check( + 'the driver is the model when there is one', + 'HP UPD PCL 6' === pfPrinter( + ['model' => 'HP UPD PCL 6', 'file' => 'C:\\d\\x.inf'] + )->driver() +); +$t->check( + 'and falls back to the driver file', + 'C:\\d\\x.inf' === pfPrinter(['file' => 'C:\\d\\x.inf'])->driver() +); +$t->check( + 'an empty driver is a VALUE -- driverless IPP Everywhere, which FOG\'s' + . ' four printer types cannot express at all', + '' === pfPrinter(['config' => 'Local'])->driver() +); + +// The mode the agent is sent says what it means. hostPrinterLevel stores +// 0/1/2 and the legacy wire sends 0/a/ar; neither is written down where an +// admin can see it. +$t->check( + 'the desired mode vocabulary is words, and covers every stored level', + [0 => 'off', 1 => 'assigned', 2 => 'exclusive'] === \FOG\Agent\PrinterSet::MODES +); +$t->check( + 'a level outside 0-2 falls back to off rather than to a mode that acts', + !isset(\FOG\Agent\PrinterSet::MODES[3]) +); + +// The capability is gated on FOG's EXISTING printermanager module, not a new +// switch: admins have been turning that one off for a decade and know where +// it is, so a host's current choice carries over untouched. +$t->check( + "the printers capability is gated on the existing printermanager module", + 'printermanager' === (\FOG\Agent\State::CAPABILITIES['printers'] ?? null) +); +$t->check( + "State::ITEM_REPORTS routes a printer result to PrinterSet", + (\FOG\Agent\State::ITEM_REPORTS['printers'] ?? null) + === \FOG\Agent\PrinterSet::class +); + +// A success has to CLEAR the previous failure, or the report shows an error +// against a printer that is now installed -- an admin chasing a stale message +// is worse off than one chasing none. Exercised through the decision itself +// rather than by reading the source for it. +/** + * Call a protected static on PrinterSet. + * + * @param string $name the method + * @param array $args the arguments + * + * @return mixed + */ +function ps($name, array $args) +{ + $m = new \ReflectionMethod(\FOG\Agent\PrinterSet::class, $name); + $m->setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +foreach (\FOG\Agent\PrinterSet::STATUSES as $status) { + $settled = in_array( + $status, + \FOG\Agent\PrinterSet::SETTLED_STATUSES, + true + ); + $got = ps('errorFor', [$status, 'lpadmin: bad device-uri']); + $t->check( + "'" . $status . "' " . ($settled ? 'clears' : 'keeps') + . ' the error [' . $got . ']', + $settled ? '' === $got : 'lpadmin: bad device-uri' === $got + ); +} +$t->check( + 'failed and unsupported are NOT settled, or an error could never be' + . ' recorded at all', + !in_array('failed', \FOG\Agent\PrinterSet::SETTLED_STATUSES, true) + && !in_array('unsupported', \FOG\Agent\PrinterSet::SETTLED_STATUSES, true) +); +$t->check( + 'every settled status is a status the agent may actually report', + [] === array_diff( + \FOG\Agent\PrinterSet::SETTLED_STATUSES, + \FOG\Agent\PrinterSet::STATUSES + ) +); +$t->check( + 'a provider that wrote a novel keeps a line, not a log', + \FOG\Agent\PrinterSet::MAX_ERROR + === strlen(ps('errorFor', ['failed', str_repeat('x', 4000)])) +); + $t->finish(); diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index dbdeb08cd5..4faf4e2bf1 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -396,6 +396,7 @@ printer 7 pModel model - - printer 8 pConfig config - - printer 9 pConfigFile configFile - - printer 10 pIP ip - - +printer 11 pURI uri - - printerassociation 0 paID id - - printerassociation 1 paID DT_RowId f - printerassociation 2 paHostID hostID - - From c06fe0800e326438de6068223c607a7f461c48de Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 20:51:00 +0000 Subject: [PATCH 078/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 340aa40578..32c2565f7a 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10386,6 +10386,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index eb2ed87467..6f739aaf58 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10395,6 +10395,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ce7e31df14..e3a7782e03 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10554,6 +10554,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 157fa3b315..d88796b059 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10387,6 +10387,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a96f82d532..ff77790632 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10379,6 +10379,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 0a5ff78081..56680d8f84 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10102,6 +10102,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index fd105b3ff6..21f721d000 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10058,6 +10058,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index fa768e4fb1..e9d9778627 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8903,6 +8903,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 5c579bc68c..c85a82ca20 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ae783ac9f2..daba14f94c 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 26c9a7092cc5484cf8d07bb5bd9a00f3ef6b6af6 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 15:59:15 -0500 Subject: [PATCH 079/117] Carry schema 427 and the printer set tests through the baselines e7474efd1 adds a schema step, so the ignored $this pattern in commons/schema.php occurs 397 times, and nine more folded assertions in agent-printer-facts.test.php -- constant pins of PrinterSet::STATES, MODULES['printers'] and FACT_REPORTS['printers'], the same kind already carried for the other agent tests. The rehearsal report does not move: 427 declares no new foreign key. Worth noting because it will keep happening: the $this entry is a per-occurrence count on a file that gains one occurrence per schema step, so every branch that adds a step must edit the same line, and two branches that both add one conflict there. That is the shape the version constant used to have. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- phpstan-baseline.neon | 2 +- phpstan-tests-baseline.neon | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 422f76feb8..b0846ec8f4 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -129,7 +129,7 @@ parameters: - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 396 + count: 397 path: packages/web/commons/schema.php - diff --git a/phpstan-tests-baseline.neon b/phpstan-tests-baseline.neon index c7aae2e2e5..04c4a448d9 100644 --- a/phpstan-tests-baseline.neon +++ b/phpstan-tests-baseline.neon @@ -42,18 +42,72 @@ parameters: count: 1 path: tests/agent-directory.test.php + - + message: '#^Call to function in_array\(\) with arguments ''failed'', array\{''converged'', ''installed'', ''updated'', ''removed''\} and true will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Call to function in_array\(\) with arguments ''unsupported'', array\{''converged'', ''installed'', ''updated'', ''removed''\} and true will always evaluate to false\.$#' + identifier: function.impossibleType + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', printers\: ''printermanager''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + - message: '#^Offset ''printers'' on array\{inventory\: ''FOG\\\\Agent\\\\InventoryFacts'', software\: ''FOG\\\\Agent\\\\SoftwareFacts'', directory\: ''FOG\\\\Agent\\\\DirectoryFacts'', printers\: ''FOG\\\\Agent\\\\PrinterFacts''\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset count: 1 path: tests/agent-printer-facts.test.php + - + message: '#^Offset ''printers'' on array\{snapin\: ''FOG\\\\Agent\\\\Snapins'', software\: ''FOG\\\\Agent\\\\SoftwareSet'', printers\: ''FOG\\\\Agent\\\\PrinterSet''\} on left side of \?\? always exists and is not nullable\.$#' + identifier: nullCoalesce.offset + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Offset 3 on array\{''off'', ''assigned'', ''exclusive''\} in isset\(\) does not exist\.$#' + identifier: isset.offset + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Result of && is always true\.$#' + identifier: booleanAnd.alwaysTrue + count: 1 + path: tests/agent-printer-facts.test.php + - message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\PrinterFacts'' and ''FOG\\\\Agent\\\\PrinterFacts'' will always evaluate to true\.$#' identifier: identical.alwaysTrue count: 1 path: tests/agent-printer-facts.test.php + - + message: '#^Strict comparison using \=\=\= between ''FOG\\\\Agent\\\\PrinterSet'' and ''FOG\\\\Agent\\\\PrinterSet'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Strict comparison using \=\=\= between ''printermanager'' and ''printermanager'' will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-printer-facts.test.php + + - + message: '#^Strict comparison using \=\=\= between array\{''off'', ''assigned'', ''exclusive''\} and array\{''off'', ''assigned'', ''exclusive''\} will always evaluate to true\.$#' + identifier: identical.alwaysTrue + count: 1 + path: tests/agent-printer-facts.test.php + - message: '#^Call to function in_array\(\) with arguments ''inferred'', array\{''logout'', ''disconnect'', ''service_stop''\} and true will always evaluate to false\.$#' identifier: function.impossibleType From d39038038fc47a4cdd20388e740b700578aee3df Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 16:10:02 -0500 Subject: [PATCH 080/117] Stop counting $this in schema.php, so adding a step is not a merge conflict The baselined `Variable $this might not be defined` entry pinned an occurrence count on commons/schema.php. That file is one long list of `$this->schema[] = [...]` steps at file scope, so it gains an occurrence every time anyone adds a schema step -- and a counted entry then has to be edited, on the same line, by every branch that adds one. Two such branches conflict there and each has to be re-resolved and re-tested. That is not hypothetical: this line moved 392 to 395 to 396 to 397 in a single afternoon on this branch alone, three of those because another branch landed a step underneath. It is the shape FOG_VERSION had before it became a generated file. Dropping the count leaves the pattern ignored for that path however many times it occurs. Nothing is lost: nobody is defending a budget of $this uses in this file, and PHPStan is only right that they are undefined because schema.php is included into a method rather than being one. Verified by appending a schema step: with the count, PHPStan reports the over-count plus the unaccounted occurrence; without it, both passes stay clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01684dJC9hw1jY4KjzV81BLC --- phpstan-baseline.neon | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index b0846ec8f4..43fb65b065 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -126,10 +126,18 @@ parameters: count: 1 path: packages/web/commons/schema.php + # No count. commons/schema.php is one long list of `$this->schema[] =` + # steps at file scope, so it gains an occurrence of this every time a + # schema step is added -- and a counted entry then has to be edited on + # the same line by every branch that adds one, which makes two such + # branches conflict there. That is the shape FOG_VERSION had before it + # became a generated file. A count buys nothing here: nobody is + # defending a budget of $this uses in this file, and PHPStan is only + # right that they are undefined because the file is included into a + # method rather than being one. - message: '#^Variable \$this might not be defined\.$#' identifier: variable.undefined - count: 397 path: packages/web/commons/schema.php - From 71a69a74d9cc037fdb5664d63ff6f8cafa94008d Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 16:11:49 -0500 Subject: [PATCH 081/117] Printers: a result's message arrives in `details`, like every other item Every item report on /agent/v1/result carries the provider's output in `details` -- a snapin's tail, a package manager's log -- and a printer's failure message is the same thing: lpadmin's own words. PrinterSet was reading `error`, a spelling nothing else on the route uses. Caught wiring the agent half up to it: the first real end-to-end run posted `details` and the message went nowhere. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/management/languages/messages.pot | 1 - .../languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Agent/PrinterSet.php | 7 ++++++- 11 files changed, 6 insertions(+), 11 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 32c2565f7a..340aa40578 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10386,7 +10386,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 6f739aaf58..eb2ed87467 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10395,7 +10395,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index e3a7782e03..ce7e31df14 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10554,7 +10554,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index d88796b059..157fa3b315 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10387,7 +10387,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index ff77790632..a96f82d532 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10379,7 +10379,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 56680d8f84..0a5ff78081 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10102,7 +10102,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 21f721d000..fd105b3ff6 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10058,7 +10058,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e9d9778627..fa768e4fb1 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8903,7 +8903,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index c85a82ca20..5c579bc68c 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10382,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index daba14f94c..ae783ac9f2 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10382,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/PrinterSet.php b/packages/web/src/Agent/PrinterSet.php index cc1bd05af3..9aa05a4d53 100644 --- a/packages/web/src/Agent/PrinterSet.php +++ b/packages/web/src/Agent/PrinterSet.php @@ -173,7 +173,12 @@ public static function report(Host $Host, $printerID, array $body) if (!in_array($status, self::STATUSES, true)) { throw new \RuntimeException('unknown status', 400); } - $error = self::errorFor($status, (string)($body['error'] ?? '')); + // `details` and not `error`: every item report on this route carries + // the provider's output in `details` (a snapin's tail, a package + // manager's log), and a printer's failure message is the same thing + // -- lpadmin's own words. One field name for one meaning across the + // protocol beats a per-capability spelling. + $error = self::errorFor($status, (string)($body['details'] ?? '')); $ids = Route::getIds( 'printerassociation', From de7d547bea4690fd0f7514d6b16430a24816dd97 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 21:13:14 +0000 Subject: [PATCH 082/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 340aa40578..32c2565f7a 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10386,6 +10386,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index eb2ed87467..6f739aaf58 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10395,6 +10395,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index ce7e31df14..e3a7782e03 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10554,6 +10554,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 157fa3b315..d88796b059 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10387,6 +10387,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index a96f82d532..ff77790632 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10379,6 +10379,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 0a5ff78081..56680d8f84 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10102,6 +10102,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index fd105b3ff6..21f721d000 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10058,6 +10058,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index fa768e4fb1..e9d9778627 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8903,6 +8903,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 5c579bc68c..c85a82ca20 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index ae783ac9f2..daba14f94c 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From aee2ba87025cf36dbc0f4b451b035a7b84c3beb1 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 17:08:38 -0500 Subject: [PATCH 083/117] Directory: the agent joins, and the credential stops being ambient `Client\HostnameChanger::json()` puts ADUser and ADPass in the answer to every check-in of every host with useAD set -- joined or not, forever. A joined estate is one where every machine permanently holds a credential that can create computer objects in the directory, for no reason: it is already joined. `FOG\Agent\DirectoryJoin` sends it only to a host the server believes is unjoined, only while that is true, and not again for an hour after an attempt. Null is returned -- no block at all -- when the host is not set to use AD, names no domain, has never reported its membership (the server does not know, and a credential is not something to send on a guess), is already in the right domain, is joined to a different one (the agent would refuse, so sending it exposes the account for nothing), or is cooling. The cooldown is not politeness. A join that fails on a bad password is a failed authentication against a domain controller, and one per host per poll is how a service account with a lockout policy gets locked out, taking every other host's join with it. The result rides the ITEM half of /agent/v1/result rather than the plain one, because the join has its own vocabulary -- joined, already_joined, failed, unsupported, refused -- and `failed` means two different things in the two places. The item is the host's own membership row, addressed by host id, and a host reporting on somebody else's gets a 404. hostDirectory gains hdJoinAt and hdJoinError so an outcome has somewhere to live; FOG has never recorded a join result at all. Both surface in the Directory Membership report's new Join column, and a settled status clears a stale error -- an admin chasing a message against a machine that is now joined is worse off than one chasing none. DirectoryPlacement::decodeStored() is made public rather than copying the three-shape base64 dance a third time. The legacy client's copy keeps its non-strict-base64 bug: changing what the legacy client is sent is a separate and riskier change. Proved on the live install by prove_directory_join.php -- 20 checks through State::desired() and State::result(), including that turning the hostnamechanger module off stops the credential entirely. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- packages/web/commons/schema-expected.php | 6 +- packages/web/commons/schema.php | 27 ++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 3 + .../en_US.UTF-8/LC_MESSAGES/messages.po | 3 + .../es_ES.UTF-8/LC_MESSAGES/messages.po | 3 + .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 3 + .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 3 + .../it_IT.UTF-8/LC_MESSAGES/messages.po | 3 + .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 7 +- .../web/management/languages/messages.pot | 3 + .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 3 + .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 3 + packages/web/src/Agent/DirectoryJoin.php | 401 +++++++++++++++++ packages/web/src/Agent/DirectoryPlacement.php | 20 +- packages/web/src/Agent/State.php | 24 + packages/web/src/Base/System.php | 2 +- packages/web/src/Items/HostDirectory.php | 4 +- .../web/src/Reports/Directory_Membership.php | 36 +- tests/agent-directory-join.test.php | 413 ++++++++++++++++++ tests/fixtures/route-column-contract.txt | 2 + 20 files changed, 959 insertions(+), 10 deletions(-) create mode 100644 packages/web/src/Agent/DirectoryJoin.php create mode 100644 tests/agent-directory-join.test.php diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 4e303467d3..5afc0be8cd 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -391,7 +391,7 @@ ], ], 'hostDirectory' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hostDirectory` ( `hdID` int(11) NOT NULL AUTO_INCREMENT, `hdHostID` int(11) NOT NULL, `hdJoined` tinyint(1) NOT NULL DEFAULT 0, `hdKind` varchar(32) NOT NULL DEFAULT \'\', `hdDomain` varchar(255) NOT NULL DEFAULT \'\', `hdNetbios` varchar(64) NOT NULL DEFAULT \'\', `hdComputerDN` varchar(1024) NOT NULL DEFAULT \'\', `hdMachineAccount` varchar(255) NOT NULL DEFAULT \'\', `hdSite` varchar(255) NOT NULL DEFAULT \'\', `hdObservedAt` datetime DEFAULT NULL, `hdPlacementAt` datetime DEFAULT NULL, `hdPlacementError` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`hdID`), UNIQUE KEY `hdHostID` (`hdHostID`), KEY `hdDomain` (`hdDomain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'create' => 'CREATE TABLE IF NOT EXISTS `hostDirectory` ( `hdID` int(11) NOT NULL AUTO_INCREMENT, `hdHostID` int(11) NOT NULL, `hdJoined` tinyint(1) NOT NULL DEFAULT 0, `hdKind` varchar(32) NOT NULL DEFAULT \'\', `hdDomain` varchar(255) NOT NULL DEFAULT \'\', `hdNetbios` varchar(64) NOT NULL DEFAULT \'\', `hdComputerDN` varchar(1024) NOT NULL DEFAULT \'\', `hdMachineAccount` varchar(255) NOT NULL DEFAULT \'\', `hdSite` varchar(255) NOT NULL DEFAULT \'\', `hdObservedAt` datetime DEFAULT NULL, `hdPlacementAt` datetime DEFAULT NULL, `hdPlacementError` varchar(255) NOT NULL DEFAULT \'\', `hdJoinAt` datetime DEFAULT NULL, `hdJoinError` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`hdID`), UNIQUE KEY `hdHostID` (`hdHostID`), KEY `hdDomain` (`hdDomain`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ 'hdID' => 'int(11) NOT NULL', 'hdHostID' => 'int(11) NOT NULL', @@ -404,7 +404,9 @@ 'hdSite' => 'varchar(255) NOT NULL DEFAULT \'\'', 'hdObservedAt' => 'datetime DEFAULT NULL', 'hdPlacementAt' => 'datetime DEFAULT NULL', - 'hdPlacementError' => 'varchar(255) NOT NULL DEFAULT \'\'' + 'hdPlacementError' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hdJoinAt' => 'datetime DEFAULT NULL', + 'hdJoinError' => 'varchar(255) NOT NULL DEFAULT \'\'' ], ], 'hostFactState' => [ diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 7facbdf99e..2f4f783d2c 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -11357,3 +11357,30 @@ function () { "ALTER TABLE `printers` " . "ADD COLUMN `pURI` varchar(1024) NOT NULL DEFAULT ''", ]; + +// 428 +$this->schema[] = [ + // Design 0009 section 6: the agent joins a machine to the domain the + // host record asks for, and what happened is recorded here. + // + // These two columns are the whole reason the join is safe to automate. + // A join that fails on a bad password is a FAILED AUTHENTICATION + // against somebody's domain controller, and without a stamp to hold a + // cooldown against it is one per host per poll -- which is how a + // service account with a lockout policy gets locked out, taking every + // other host's join with it. `hdJoinAt` is what + // Agent\DirectoryJoin::RETRY_AFTER reads. + // + // Named for the ATTEMPT, like hdPlacementAt beside it: this is stamped + // whenever the agent acted, so a name like hdJoinedAt would claim a + // join happened on every occasion one did not. + // + // Deliberately separate from hdPlacementAt/hdPlacementError rather than + // reusing them. They are different operations by different actors -- + // the machine joins, the server moves -- and a report that showed one + // error against both would be a report that lies about which half is + // broken. + "ALTER TABLE `hostDirectory` " + . "ADD COLUMN `hdJoinAt` datetime DEFAULT NULL, " + . "ADD COLUMN `hdJoinError` varchar(255) NOT NULL DEFAULT ''", +]; diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 32c2565f7a..82cbde9bba 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -5280,6 +5280,9 @@ msgstr "Es weist den Client an, Snapins vom hostdefinierten " msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 6f739aaf58..61cd4ac03d 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -5282,6 +5282,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index e3a7782e03..457a3600df 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -5368,6 +5368,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index d88796b059..c0d258a24b 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -5281,6 +5281,9 @@ msgstr "Es weist den Client an, Snapins vom hostdefinierten " msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index ff77790632..da4a4e9e3f 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -5282,6 +5282,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 56680d8f84..74e587b351 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -5144,6 +5144,9 @@ msgstr "indica al client di scaricare gli snapins da" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 21f721d000..91a800020b 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -5112,6 +5112,10 @@ msgstr "クライアントに次の場所からスナップインをダウンロ msgid "It will operate based on the fields the area typcially requires." msgstr "通常その領域で必要とされるフィールドに基づいて動作します" +#, fuzzy +msgid "Join" +msgstr "AD 参加" + #, fuzzy msgid "Join Domain after image task" msgstr "展開後にドメイン参加" @@ -12502,9 +12506,6 @@ msgstr "" #~ msgid "A subnetgroup already exists with this name!" #~ msgstr "この名前のサブネットグループは既に存在します!" -#~ msgid "AD Join" -#~ msgstr "AD 参加" - #~ msgid "AD OU" #~ msgstr "AD OU" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e9d9778627..d1eeba7d62 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -4527,6 +4527,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index c85a82ca20..15d3a42fe7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -5282,6 +5282,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index daba14f94c..2c9e059e4e 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -5282,6 +5282,9 @@ msgstr "" msgid "It will operate based on the fields the area typcially requires." msgstr "" +msgid "Join" +msgstr "" + msgid "Join Domain after image task" msgstr "" diff --git a/packages/web/src/Agent/DirectoryJoin.php b/packages/web/src/Agent/DirectoryJoin.php new file mode 100644 index 0000000000..8ef112a255 --- /dev/null +++ b/packages/web/src/Agent/DirectoryJoin.php @@ -0,0 +1,401 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; +use FOG\Items\HostDirectory; +use FOG\Router\Route; + +/** + * What the agent is told about joining a domain, and what it reports back + * (design 0009 section 6). + * + * The half only the machine can do. Membership is a property of the machine + * -- its computer account, its secure channel, its Kerberos keytab -- so it + * is the machine that joins, and the server's job is to decide whether it + * should and to hand over the credential for exactly as long as that takes. + * + * The contrast with the legacy client is the whole point of this class. + * `Client\HostnameChanger::json()` puts `ADUser` and `ADPass` in the answer + * to EVERY check-in of EVERY host with `useAD` set -- joined or not, forever, + * in cleartext once the client decrypts it. A joined estate is an estate + * where every machine holds a credential that can create computer objects in + * the directory, and it holds it permanently, for no reason: it is already + * joined. + * + * Here the credential is sent only to a host the server BELIEVES is not + * joined, only while that is true, and not again for an hour after an + * attempt. Most hosts in most estates never receive it at all. + * + * @category DirectoryMembership + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class DirectoryJoin extends FOGBase +{ + /** + * Seconds before a host is sent the credential again after an attempt. + * + * Not politeness. A join that fails on a bad password is a FAILED + * AUTHENTICATION against somebody's domain controller, and without a + * cooldown it is one per host per poll -- which is how a service account + * with a lockout policy gets locked out, taking every other host's join + * with it. An hour is short enough that fixing the password is not a + * long wait and long enough that a fleet cannot trip a lockout. + * + * It also covers the gap after a SUCCESSFUL join: the machine's own + * report of its new membership arrives on a later poll, and until it + * does the server would otherwise still believe the host unjoined and + * send the credential once more. + */ + const RETRY_AFTER = 3600; + + /** + * What the agent may report for a join. + * + * `refused` is the one worth explaining: it is the agent declining to + * act on what it was sent, rather than trying and failing. A machine + * already in a DIFFERENT domain is the case that matters -- getting it + * to the right one means leaving the wrong one, which resets the + * computer account's password and can cost the object its SID, and the + * agent will not do that as a side effect of an edit. + */ + const STATUS_JOINED = 'joined'; + const STATUS_ALREADY_JOINED = 'already_joined'; + const STATUSES = [ + 'joined', 'already_joined', 'failed', 'unsupported', 'refused' + ]; + + /** + * The statuses that mean the machine is where it should be, so any + * error recorded against it is stale. + */ + const SETTLED_STATUSES = ['joined', 'already_joined']; + + /** + * Longest error kept: the column is a varchar(255) because this is a + * line an admin reads in a report, not a log. + */ + const MAX_ERROR = 255; + + /** + * The join block for a host, or null when there is nothing to send. + * + * Null is the answer for the overwhelming majority of hosts and every + * one of the reasons is a reason NOT to put a credential on a machine: + * + * - The host is not set to use AD, or names no domain. Nothing to join. + * - The host has never reported its membership. The server does not + * know whether it is joined, and a credential is not something to + * send on a guess. It arrives one poll later, once the machine has + * said where it is (facts are recorded after this runs, by design -- + * see Route::agentPoll). + * - The host is already in the domain it should be in. This is the + * resting state of a working estate. + * - The host is joined to some OTHER domain. The agent would refuse, + * so sending the credential would achieve nothing and expose it; the + * Directory Membership report shows the mismatch instead. + * - An attempt was made within RETRY_AFTER. + * + * @param Host $Host the principal + * + * @return array|null + */ + public static function desired(Host $Host) + { + return self::blockFor($Host, self::observed($Host)); + } + + /** + * The decision, given the host and what it last reported. + * + * Split from the lookup for the reason ReportManagement splits its + * fetch from its emit: the rule about when a credential leaves this + * server is the part worth testing, and it needs no database to state. + * + * @param Host $Host the principal + * @param HostDirectory|null $Observed what it last reported, or null + * + * @return array|null + */ + public static function blockFor(Host $Host, HostDirectory $Observed = null) + { + if (!(bool)$Host->get('useAD')) { + return null; + } + $domain = trim((string)$Host->get('ADDomain')); + if ('' === $domain) { + return null; + } + + if (null === $Observed) { + // Never reported. Ask again next poll, when it has. + return null; + } + if ((bool)$Observed->get('joined')) { + // Joined to something. Either it is where it belongs, or it is + // somewhere else and the agent would refuse; neither is a + // reason to hand over a credential. + return null; + } + if (self::cooling($Observed)) { + return null; + } + + $user = self::joinUser($Host, $domain); + $pass = self::joinPassword($Host); + if ('' === $user || '' === $pass) { + // No credential to send. Deliberately still returns a block: + // the agent reports `refused` with a message naming the missing + // fields, which is how an admin finds out. Sending nothing at + // all would look identical to a host that is already joined. + $user = $pass = ''; + } + + return [ + 'domain' => $domain, + // The short name where the host's own report supplied it. Used + // by the agent only to recognize that it is already in this + // domain, never to join. + 'netbios' => (string)$Observed->get('netbios'), + // The container the object is CREATED in. Semicolons are + // stripped the way the legacy client's block does: hostADOU has + // always been allowed to hold a list and only the first is a + // container. + 'ou' => str_replace(';', '', (string)$Host->get('ADOU')), + 'username' => $user, + 'password' => $pass, + // The host's existing "Enforce Hostname | AD Join Reboots" + // flag: may the agent reboot to finish the join. The agent's + // reboot coordinator still owns the when. + 'reboot' => (bool)$Host->get('enforce') + ]; + } + + /** + * Records what the agent did about the join. + * + * @param Host $Host the host the certificate bound + * @param int $hostID the host the agent says it is reporting about + * @param array $body the reported result + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return string the status recorded + */ + public static function report(Host $Host, $hostID, array $body) + { + // The row a join result is about is the host's own membership, and + // the agent addresses it by its host id. Checked rather than + // ignored: a host reporting on somebody else's membership is a host + // writing a row that is not its own. + if ((int)$hostID !== (int)$Host->get('id')) { + throw new \RuntimeException('not this host\'s membership', 404); + } + $status = (string)($body['status'] ?? ''); + if (!in_array($status, self::STATUSES, true)) { + throw new \RuntimeException('unknown status', 400); + } + $error = self::errorFor($status, (string)($body['details'] ?? '')); + + $Observed = self::observed($Host); + if (null === $Observed) { + // A result about a host with no membership row. Possible only + // if the row was deleted between the state fetch and the + // report; the outcome is still worth an audit line, it simply + // has nowhere to be stamped. + self::_audit($Host, $status, $error); + return $status; + } + + $Observed + // Named for the ATTEMPT, not the join: this is stamped whenever + // the agent acted, so a name like hdJoinedAt would claim a join + // happened on every occasion one did not -- and it is what the + // RETRY_AFTER cooldown reads. + ->set('joinAt', self::stamp()) + ->set('joinError', $error) + ->save(); + + // An already_joined heartbeat is not news, and it is what a working + // estate reports forever. Auditing it would bury the results that + // matter. + if (self::STATUS_ALREADY_JOINED !== $status) { + self::_audit($Host, $status, $error); + } + + return $status; + } + + /** + * The error to record for a reported status. + * + * A settled status clears whatever was there: an admin chasing a stale + * message against a machine that is now joined is worse off than one + * chasing none. + * + * @param string $status the reported status + * @param string $error the reported message + * + * @return string + */ + protected static function errorFor($status, $error) + { + if (in_array($status, self::SETTLED_STATUSES, true)) { + return ''; + } + + return substr(trim($error), 0, self::MAX_ERROR); + } + + /** + * Whether an attempt is too recent to make another. + * + * @param HostDirectory $Observed the membership row + * + * @return bool + */ + protected static function cooling(HostDirectory $Observed) + { + $at = trim((string)$Observed->get('joinAt')); + // validDate() rather than a literal: there stays one definition of + // what an empty date is, and MySQL's zero date is only one of the + // shapes an untouched column comes back as. + if ('' === $at || !self::validDate($at)) { + return false; + } + + return (self::niceDate()->getTimestamp() + - self::niceDate($at, self::storageTimeZone())->getTimestamp()) + < self::RETRY_AFTER; + } + + /** + * The joining account, domain-qualified. + * + * Same rule the legacy client's block uses, so an admin who has typed + * `CORP\svc-join` or `svc-join@corp.example.com` into the host record + * gets what they typed, and a bare name is qualified with the domain. + * The agent strips the qualifier again for adcli and realm, which want + * the bare sAMAccountName -- one spelling on the wire, each consumer + * adapting it, rather than the server sending two. + * + * @param Host $Host the host + * @param string $domain the domain being joined + * + * @return string + */ + protected static function joinUser(Host $Host, $domain) + { + $user = trim((string)$Host->get('ADUser')); + if ('' === $user) { + return ''; + } + if (false !== strpos($user, '\\') || false !== strpos($user, '@')) { + return $user; + } + + return $domain . '\\' . $user; + } + + /** + * The joining account's password, as typed. + * + * Reuses DirectoryPlacement's decoder rather than repeating the + * three-shape dance a third time. The legacy client's copy in + * `Client\HostnameChanger::json()` has the non-strict base64 bug that + * one documents -- it is left alone here because changing what the + * legacy client is sent is a separate, riskier change. + * + * @param Host $Host the host + * + * @return string + */ + protected static function joinPassword(Host $Host) + { + return DirectoryPlacement::decodeStored((string)$Host->get('ADPass')); + } + + /** + * The host's reported membership row, or null when it has never + * reported. + * + * @param Host $Host the host + * + * @return HostDirectory|null + */ + protected static function observed(Host $Host) + { + $ids = Route::getIds( + 'hostdirectory', + ['hostID' => (int)$Host->get('id')], + 'id' + ); + $id = (int)(array_shift($ids) ?: 0); + if ($id < 1) { + return null; + } + $Observed = new HostDirectory($id); + + return $Observed->isValid() ? $Observed : null; + } + + /** + * Now, in storage time. + * + * @return string + */ + protected static function stamp() + { + return self::niceDate() + ->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s'); + } + + /** + * One audit line for a join result. + * + * @param Host $Host the host + * @param string $status what the agent said it did + * @param string $error the message, if any + * + * @return void + */ + private static function _audit(Host $Host, $status, $error) + { + Audit::record( + [ + 'type' => 'agent.result', + 'subjectType' => 'host', + 'subjectID' => (int)$Host->get('id'), + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'text' => substr( + sprintf( + 'directory join %s%s', + $status, + '' === $error ? '' : ': ' . $error + ), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } +} diff --git a/packages/web/src/Agent/DirectoryPlacement.php b/packages/web/src/Agent/DirectoryPlacement.php index 7005d075bd..82d4257a0f 100644 --- a/packages/web/src/Agent/DirectoryPlacement.php +++ b/packages/web/src/Agent/DirectoryPlacement.php @@ -283,7 +283,25 @@ private static function stamp() */ private static function _bindPassword() { - $pass = trim((string)self::getSetting('FOG_DIRECTORY_BIND_PASSWORD')); + return self::decodeStored( + (string)self::getSetting('FOG_DIRECTORY_BIND_PASSWORD') + ); + } + + /** + * One of FOG's stored secrets, as it was typed. + * + * Public because DirectoryJoin reads the host's `hostADPass` with the + * same three shapes and the same trap; a third copy of the dance is how + * the buggy version in `Client\HostnameChanger` came to exist. + * + * @param string $stored what the column or setting holds + * + * @return string + */ + public static function decodeStored($stored) + { + $pass = trim((string)$stored); if ('' === $pass) { return ''; } diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 8f7914cf58..2ee0b7236a 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -55,6 +55,12 @@ class State extends FOGBase 'snapin' => 'snapinclient', 'software' => 'software', 'power' => 'powermanagement', + // Both halves of the hostnamechanger module, kept apart on the wire + // because they are different acts with different blast radii: a + // rename touches this machine, a domain join touches somebody's + // directory and carries a credential. An admin who has turned the + // module off has turned off both, which is what they meant. + 'directory' => 'hostnamechanger', // Gated on the EXISTING printermanager module, not a new switch: // admins have been turning that one off for a decade and know // where it is, so a host's current choice carries over untouched @@ -80,6 +86,13 @@ class State extends FOGBase 'snapin' => Snapins::class, 'software' => SoftwareSet::class, 'printers' => PrinterSet::class, + // The row here is the host's own membership, and the agent + // addresses it by the only id it knows: its host id. Deliberately + // an ITEM report and not a shape of its own -- the outer `status` + // is the capability's applied/failed, and a join has its own + // vocabulary (joined, refused, unsupported) that needs somewhere to + // live that is not that field. + 'directory' => DirectoryJoin::class, ]; /** @@ -207,6 +220,17 @@ public static function desired(Host $Host) // a reporting host does not move its own revision. $state['printers'] = PrinterSet::desired($Host); } + if (in_array('directory', $capabilities, true)) { + // Design 0009 section 6, and the only block that ever carries a + // credential. Null for nearly every host, and every reason for + // null is a reason not to put a join account on a machine -- + // see DirectoryJoin::desired(). Omitted entirely when null so + // the wire says nothing rather than saying "no credential". + $directory = DirectoryJoin::desired($Host); + if (null !== $directory) { + $state['directory'] = $directory; + } + } if (in_array('power', $capabilities, true)) { // Design 0004. Schedules are what Client\PM hands the legacy // client: the host's own rows and its groups' grants through diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 836fb90a04..3c611abae7 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 427); + define('FOG_SCHEMA', 428); define('FOG_BCACHE_VER', 360); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Items/HostDirectory.php b/packages/web/src/Items/HostDirectory.php index 4294eecf16..79a6a6b08c 100644 --- a/packages/web/src/Items/HostDirectory.php +++ b/packages/web/src/Items/HostDirectory.php @@ -59,7 +59,9 @@ class HostDirectory extends FOGController 'site' => 'hdSite', 'observedAt' => 'hdObservedAt', 'placementAt' => 'hdPlacementAt', - 'placementError' => 'hdPlacementError' + 'placementError' => 'hdPlacementError', + 'joinAt' => 'hdJoinAt', + 'joinError' => 'hdJoinError' ]; /** * The required fields. diff --git a/packages/web/src/Reports/Directory_Membership.php b/packages/web/src/Reports/Directory_Membership.php index 7811037305..c0372537d4 100644 --- a/packages/web/src/Reports/Directory_Membership.php +++ b/packages/web/src/Reports/Directory_Membership.php @@ -69,11 +69,12 @@ public function file() _('Observed OU'), _('Drift'), _('Placement'), + _('Join'), _('Reported'), _('Last check-in') ]; $this->attributes = [ - [], [], [], [], [], [], [], [], [] + [], [], [], [], [], [], [], [], [], [] ]; $payload = $this->reportRows(); @@ -135,7 +136,9 @@ protected function reportRows() `hdComputerDN`, `hdObservedAt`, `hdPlacementAt`, - `hdPlacementError` + `hdPlacementError`, + `hdJoinAt`, + `hdJoinError` FROM `hosts` LEFT OUTER JOIN `hostDirectory` ON `hdHostID` = `hostID` WHERE `hostUseAD` = 1 @@ -177,6 +180,7 @@ protected function reportRows() $reported ), 'placement' => self::placement($row), + 'join' => self::join($row), 'observedAt' => (string)($row['hdObservedAt'] ?? ''), 'checkin' => (string)($row['hostAgentCheckin'] ?? '') ]; @@ -237,6 +241,34 @@ protected static function placement(array $row) } return _('ok'); } + /** + * What the agent last did about joining this host. + * + * The counterpart to the Placement column and it exists for the same + * reason: a join that fails leaves a Drift value that never clears, + * which reads like the feature does not work rather than like a + * password needs correcting. FOG has never shown this at all -- the + * legacy client attempts a join on every check-in and reports nothing + * either way, so an admin's only evidence is the machine still not + * being in the domain a week later. + * + * @param array $row the joined row + * + * @return string + */ + protected static function join(array $row) + { + $error = trim((string)($row['hdJoinError'] ?? '')); + if ('' !== $error) { + return $error; + } + if ('' === trim((string)($row['hdJoinAt'] ?? ''))) { + // Never attempted: the host is already joined, or it has never + // reported, or nothing has needed doing. Not a problem. + return ''; + } + return _('ok'); + } /** * The drift verdict for one host. * diff --git a/tests/agent-directory-join.test.php b/tests/agent-directory-join.test.php new file mode 100644 index 0000000000..9dd5407356 --- /dev/null +++ b/tests/agent-directory-join.test.php @@ -0,0 +1,413 @@ +setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +/** + * A host that needs no database to answer for itself. + * + * @param array $fields what to set on it + * + * @return \FOG\Items\Host + */ +function djHost(array $fields = []) +{ + $Host = new \FOG\Items\Host(); + $Host->set('id', 7)->set('name', 'WS-014'); + foreach ($fields as $k => $v) { + $Host->set($k, $v); + } + + return $Host; +} + +/** + * A membership row that needs no database. + * + * @param array $fields what to set on it + * + * @return \FOG\Items\HostDirectory + */ +function djObserved(array $fields = []) +{ + $Observed = new \FOG\Items\HostDirectory(); + $Observed->set('id', 3)->set('hostID', 7); + foreach ($fields as $k => $v) { + $Observed->set($k, $v); + } + + return $Observed; +} + +// ------------------------------------------------- the columns are mapped + +$mapped = array_keys( + (function () { + $p = new \ReflectionProperty( + \FOG\Items\HostDirectory::class, + 'databaseFields' + ); + $p->setAccessible(true); + return (array)$p->getValue(new \FOG\Items\HostDirectory()); + })() +); +foreach (['joinAt', 'joinError'] as $field) { + $t->check( + sprintf('HostDirectory maps %s', $field), + in_array($field, $mapped, true) + ); +} + +// --------------------------------------------------------------- the modes + +$t->check( + 'every settled status is a real status', + [] === array_diff( + \FOG\Agent\DirectoryJoin::SETTLED_STATUSES, + \FOG\Agent\DirectoryJoin::STATUSES + ) +); +$t->check( + 'refused is a status of its own, not a kind of failure', + in_array('refused', \FOG\Agent\DirectoryJoin::STATUSES, true) + && !in_array('refused', \FOG\Agent\DirectoryJoin::SETTLED_STATUSES, true) +); + +// ----------------------------------------------------- the capability gate + +$caps = new \ReflectionClassConstant(\FOG\Agent\State::class, 'CAPABILITIES'); +$t->check( + 'the directory capability is gated on the EXISTING hostnamechanger ' + . 'module, not a new switch an admin has to find', + 'hostnamechanger' === ($caps->getValue()['directory'] ?? null) +); + +$itemReports = new \ReflectionClassConstant(\FOG\Agent\State::class, 'ITEM_REPORTS'); +$t->check( + 'the join result routes through the existing registry, not a new path', + \FOG\Agent\DirectoryJoin::class === ($itemReports->getValue()['directory'] ?? null) +); + +// Why it has to be an ITEM report and not a plain one: the join's own +// vocabulary does not fit in the outer `status` field, which carries the +// capability's applied/unchanged/pending_reboot/failed. `joined` is not one +// of those, and `failed` means two different things in the two places -- +// which is exactly why they need two fields. +$resultStatuses = new \ReflectionClassConstant(\FOG\Agent\State::class, 'RESULT_STATUSES'); +foreach (['joined', 'already_joined', 'refused', 'unsupported'] as $own) { + $t->check( + sprintf('%s has nowhere to live in the capability status field', $own), + in_array($own, \FOG\Agent\DirectoryJoin::STATUSES, true) + && !in_array($own, $resultStatuses->getValue(), true) + ); +} + +// A host may only report on its own membership. +$refused = false; +try { + \FOG\Agent\DirectoryJoin::report(djHost(), 999, ['status' => 'joined']); +} catch (\RuntimeException $e) { + $refused = 404 === $e->getCode(); +} +$t->check( + 'a host reporting on somebody else\'s membership is refused', + $refused +); + +// -------------------------------------------------- when nothing is sent + +$t->check( + 'a host not set to use AD is sent nothing', + null === \FOG\Agent\DirectoryJoin::desired(djHost(['useAD' => 0])) +); +$t->check( + 'a host with no domain is sent nothing', + null === \FOG\Agent\DirectoryJoin::desired( + djHost(['useAD' => 1, 'ADDomain' => ' ']) + ) +); + +// The rest go through blockFor(), which takes the membership row rather +// than looking it up -- the rule about when a credential leaves this server +// needs no database to state. +/** + * blockFor() with a given membership row. + * + * @param \FOG\Items\Host $Host the host + * @param \FOG\Items\HostDirectory|null $Observed what it last reported + * + * @return array|null + */ +function djDesired($Host, $Observed) +{ + return \FOG\Agent\DirectoryJoin::blockFor($Host, $Observed); +} + +$joinable = ['useAD' => 1, 'ADDomain' => 'corp.example.com', + 'ADUser' => 'svc-join', 'ADPass' => 'letmein', + 'ADOU' => 'OU=Workstations,DC=corp,DC=example,DC=com', 'enforce' => 1]; + +$t->check( + 'a host that has never reported its membership is sent nothing -- the ' + . 'server does not know whether it is joined, and a credential is ' + . 'not something to send on a guess', + null === djDesired(djHost($joinable), null) +); +$t->check( + 'a host already joined is sent nothing, which is most of an estate most ' + . 'of the time', + null === djDesired( + djHost($joinable), + djObserved(['joined' => 1, 'domain' => 'corp.example.com']) + ) +); +$t->check( + 'a host joined to a DIFFERENT domain is sent nothing either: the agent ' + . 'would refuse, so sending the credential exposes it for nothing', + null === djDesired( + djHost($joinable), + djObserved(['joined' => 1, 'domain' => 'other.example.com']) + ) +); + +$block = djDesired(djHost($joinable), djObserved(['joined' => 0])); +$t->check('an unjoined host IS sent a block', is_array($block)); +$t->check( + 'the block carries the domain', + 'corp.example.com' === ($block['domain'] ?? null) +); +$t->check( + 'the block carries the OU, so the object is created where it belongs ' + . 'instead of landing in CN=Computers and needing a move', + 'OU=Workstations,DC=corp,DC=example,DC=com' === ($block['ou'] ?? null) +); +$t->check( + 'the account is domain-qualified', + 'corp.example.com\\svc-join' === ($block['username'] ?? null) +); +$t->check( + 'the password is there to send', + 'letmein' === ($block['password'] ?? null) +); +$t->check( + 'the reboot permission is the host\'s existing enforce flag, not a new one', + true === ($block['reboot'] ?? null) +); + +// An account an admin already qualified is left alone. +$qualified = $joinable; +$qualified['ADUser'] = 'CORP\\svc-join'; +$block2 = djDesired(djHost($qualified), djObserved(['joined' => 0])); +$t->check( + 'an already-qualified account is not qualified twice', + 'CORP\\svc-join' === ($block2['username'] ?? null) +); +$upn = $joinable; +$upn['ADUser'] = 'svc-join@corp.example.com'; +$block3 = djDesired(djHost($upn), djObserved(['joined' => 0])); +$t->check( + 'a userPrincipalName is left as typed', + 'svc-join@corp.example.com' === ($block3['username'] ?? null) +); + +// A host with no credential still gets a block, so the agent can report why. +$nocred = $joinable; +$nocred['ADUser'] = ''; +$block4 = djDesired(djHost($nocred), djObserved(['joined' => 0])); +$t->check( + 'a host with no credential still gets a block, so the agent reports ' + . 'refused with a reason instead of looking identical to a joined host', + is_array($block4) && '' === ($block4['username'] ?? null) + && '' === ($block4['password'] ?? null) +); + +// ------------------------------------------------------------ the cooldown + +$t->check( + 'a host never attempted is not cooling', + false === dj('cooling', [djObserved([])]) +); +$t->check( + 'a zero datetime is not a recent attempt', + false === dj('cooling', [djObserved(['joinAt' => '0000-00-00 00:00:00'])]) +); +$storeTz = (new \ReflectionMethod(\FOG\Base\FOGBase::class, 'storageTimeZone')); +$storeTz->setAccessible(true); +$tz = $storeTz->invoke(null); +$recent = (new \DateTime('-1 minute'))->setTimezone($tz)->format('Y-m-d H:i:s'); +$t->check( + 'an attempt a minute ago is cooling -- without this, a wrong password ' + . 'is one failed authentication per host per poll, which is how a ' + . 'service account with a lockout policy gets locked out', + true === dj('cooling', [djObserved(['joinAt' => $recent])]) +); +$old = (new \DateTime('-2 hours'))->setTimezone($tz)->format('Y-m-d H:i:s'); +$t->check( + 'an attempt two hours ago is not cooling, so a corrected password is ' + . 'acted on without an admin restarting anything', + false === dj('cooling', [djObserved(['joinAt' => $old])]) +); +$t->check( + 'the cooldown outlasts a poll interval by a wide margin', + \FOG\Agent\DirectoryJoin::RETRY_AFTER >= 1800 +); + +$cooling = djDesired( + djHost($joinable), + djObserved(['joined' => 0, 'joinAt' => $recent]) +); +$t->check('a cooling host is sent nothing', null === $cooling); + +// -------------------------------------------------------- the error rule + +$t->check( + 'a failure keeps its message', + 'adcli: Insufficient access' + === dj('errorFor', ['failed', 'adcli: Insufficient access']) +); +$t->check( + 'a refusal keeps its message', + 'already joined to other.example.com' + === dj('errorFor', ['refused', 'already joined to other.example.com']) +); +$t->check( + 'unsupported keeps its message', + 'neither adcli nor realm is installed' + === dj('errorFor', ['unsupported', 'neither adcli nor realm is installed']) +); +foreach (\FOG\Agent\DirectoryJoin::SETTLED_STATUSES as $settled) { + $t->check( + sprintf( + '%s CLEARS a stale error -- an admin chasing a message against ' + . 'a machine that is now joined is worse off than one ' + . 'chasing none', + $settled + ), + '' === dj('errorFor', [$settled, 'adcli: Insufficient access']) + ); +} +$t->check( + 'a provider novel is cut to what the column holds', + \FOG\Agent\DirectoryJoin::MAX_ERROR + === strlen(dj('errorFor', ['failed', str_repeat('x', 4000)])) +); + +// ------------------------------------------- the stored-secret decoder + +$t->check( + 'an empty stored secret decodes to empty', + '' === \FOG\Agent\DirectoryPlacement::decodeStored(' ') +); +$t->check( + 'a plain password comes back as itself', + 'S3cret!pass' === \FOG\Agent\DirectoryPlacement::decodeStored('S3cret!pass') +); +$t->check( + 'a base64-stored password is decoded', + 'S3cret!pass' + === \FOG\Agent\DirectoryPlacement::decodeStored(base64_encode('S3cret!pass')) +); +// The strictness check. Non-strict base64_decode() skips characters outside +// the alphabet and decodes what is left, so an ordinary password that +// happens to contain some base64 characters comes back as garbage -- which +// is the live bug in Client\HostnameChanger's copy of this dance. +$awkward = 'Passw0rd!!'; +$t->check( + 'a password that merely looks base64-ish is NOT decoded', + $awkward === \FOG\Agent\DirectoryPlacement::decodeStored($awkward) +); + +// ------------------------------------------------ the report is honest + +$rp = new \ReflectionMethod(\FOG\Reports\Directory_Membership::class, 'join'); +$rp->setAccessible(true); +$t->check( + 'a join error is what the report shows', + 'adcli: Insufficient access' + === $rp->invoke(null, ['hdJoinError' => 'adcli: Insufficient access']) +); +$t->check( + 'a host never attempted shows nothing rather than ok', + '' === $rp->invoke(null, ['hdJoinAt' => '', 'hdJoinError' => '']) +); +$t->check( + 'an attempt with no error shows ok', + 'ok' === $rp->invoke( + null, + ['hdJoinAt' => '2026-09-04 21:00:00', 'hdJoinError' => ''] + ) +); + +$src = file_get_contents( + dirname(__DIR__) . '/packages/web/src/Reports/Directory_Membership.php' +); +foreach (['hdJoinAt', 'hdJoinError'] as $column) { + $t->check( + sprintf('the report actually selects %s', $column), + false !== strpos($src, '`' . $column . '`') + ); +} + +// -------------------------------------- nothing logs the credential + +$src = file_get_contents( + dirname(__DIR__) . '/packages/web/src/Agent/DirectoryJoin.php' +); +$t->check( + 'the audit line never formats the password', + false === strpos($src, "get('ADPass')") + || 1 === preg_match_all("/get\('ADPass'\)/", $src) +); + +$t->finish(); diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index 4faf4e2bf1..b57534042b 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -127,6 +127,8 @@ hostdirectory 10 hdSite site - - hostdirectory 11 hdObservedAt observedAt - - hostdirectory 12 hdPlacementAt placementAt - - hostdirectory 13 hdPlacementError placementError - - +hostdirectory 14 hdJoinAt joinAt - - +hostdirectory 15 hdJoinError joinError - - hostfactstate 0 hfsID id - - hostfactstate 1 hfsID DT_RowId f - hostfactstate 2 hfsHostID hostID - - From bb6db0c3c3cee4e91e6925baf7e68e63238c8d9e Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Fri, 4 Sep 2026 17:46:28 -0500 Subject: [PATCH 084/117] Wake relay: reach a subnet FOG owns no machine on A magic packet is a link-layer broadcast, so FOG can only send one from a machine it owns. `FOGBase::wakeUp()` already fans out to every enabled, online storage node, which covers every link FOG has a machine on -- and in a routed estate a subnet routinely has FOG hosts on it and no FOG server or node at all. The documented answer, a directed broadcast, has been off by default on enterprise routers since the smurf attack, and asking a security team to re-enable it is asking them to undo a decision that was right. The sender that was always there is a machine already ON that link, already awake, already authenticated. `FOG\Agent\WakeRelay` finds it and asks it, ADDITIONAL to the node fan-out rather than instead of it. Two controls, because a magic packet is unauthenticated by construction: the server picks both ends -- a target is a row in `hosts` and its MACs are that host's own rows, so there is no path from an arbitrary MAC to the wire -- and the agent is never told WHERE to send. The block carries host ids and MACs and no destination at all; the agent broadcasts on its own interfaces. An agent that accepted a destination would be a UDP reflector for whoever could feed it one. Finding the neighbor needed a fact FOG has never held: a host's interfaces. hostIP is one address with no prefix and no interface behind it, so "which machines share a link with host 41" has not been a question this server could answer. `hostNetwork` records what the machine reports about its own links and stores the network address alongside the prefix, which makes that an index lookup rather than a scan. The server recomputes both from the address and prefix and discards what the agent sent -- a host that could claim a network it is not on could join any link's relay group it liked. A candidate sender is on the same network AND prefix, has the interface up and running, has a broadcast address at all, is not wireless (an access point will not bridge a broadcast to a station that is asleep and so not associated), has checked in recently enough to be awake, and is not the target. Three are asked, because one datagram costs nothing and the alternative is a wake that does nothing because the single sender went to sleep. Requests expire, so a wake is never a standing instruction. The result is the first time FOG can say anything about whether a wake happened -- the existing path is fire and forget, and a machine that stayed asleep is indistinguishable from a packet that never left the building. The pending row is also the authorization: this is the only item report whose id is another host's, and a result with no pending row naming this sender and that target is a 404. Pending MACs are excluded, the way `Group::wakeOnLAN()` already does and `Host::wakeOnLAN()` does not. That is a deliberate behavior difference confined to the new path; narrowing the old one is a separate change. Off by default (FOG_AGENT_WAKE_RELAY_ENABLED): this asks one customer machine to put traffic on the network for another. Proved on the live install by prove_wake_relay.php -- 25 checks through the real entry points -- and end to end by prove_wake_relay_on_the_wire.php, which caught a real gap: four magic packets went out and the row recorded packets=0, because the agent had no field to put the count in. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- bin/psr4-scan.php | 9 + ...l-integrity-is-declared-in-the-database.md | 2 +- docs/development/foreign-keys.md | 2 +- packages/web/commons/schema-constraints.php | 8 + packages/web/commons/schema-expected.php | 59 ++- packages/web/commons/schema.php | 95 ++++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Agent/NetworkFacts.php | 299 ++++++++++++ packages/web/src/Agent/State.php | 26 +- packages/web/src/Agent/WakeRelay.php | 433 ++++++++++++++++++ packages/web/src/Auth/Authorization.php | 6 + packages/web/src/Base/System.php | 2 +- packages/web/src/Items/AgentWake.php | 99 ++++ packages/web/src/Items/Group.php | 9 + packages/web/src/Items/Host.php | 8 + packages/web/src/Items/HostNetwork.php | 122 +++++ .../web/src/Managers/AgentWakeManager.php | 35 ++ .../web/src/Managers/HostNetworkManager.php | 35 ++ packages/web/src/Router/Route.php | 2 + tests/agent-wake-relay.test.php | 334 ++++++++++++++ tests/fixtures/route-cascade-contract.txt | 2 + tests/fixtures/route-column-contract.txt | 25 + tests/foreign-key-map.test.php | 3 + 32 files changed, 1597 insertions(+), 28 deletions(-) create mode 100644 packages/web/src/Agent/NetworkFacts.php create mode 100644 packages/web/src/Agent/WakeRelay.php create mode 100644 packages/web/src/Items/AgentWake.php create mode 100644 packages/web/src/Items/HostNetwork.php create mode 100644 packages/web/src/Managers/AgentWakeManager.php create mode 100644 packages/web/src/Managers/HostNetworkManager.php create mode 100644 tests/agent-wake-relay.test.php diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 9f0368cb96..8c18d8f1a5 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -224,6 +224,15 @@ // hostUserSession rows, it is not either of those rows. 'DirectoryFacts' => 'Agent', 'DirectoryPlacement' => 'Agent', + // The join half of directory membership (design 0009 section 6): the + // one class that decides whether a credential leaves this server. + 'DirectoryJoin' => 'Agent', + // The writer for the links a host is on, and the class that asks an + // awake agent to broadcast a wake for a sleeping neighbor (design + // 0011). Network and Wake would both be far too general as Items + // names; these write hostNetwork and agentWake rows. + 'NetworkFacts' => 'Agent', + 'WakeRelay' => 'Agent', // The writer for what an agent reports about its installed printers // (design 0010). Same naming reason: Printer is already an Items // class for the assignable printer -- this writes hostPrinter and diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index abd2b374bb..1e7a409c7c 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**121 of the map's 136 relationships are declared.** The other 15 are not +**124 of the map's 139 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index cdbd45260c..cc38bc67da 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 136 relationships live in them. +the survey and 25 of the map's 139 relationships live in them. ### Direction is the whole rule diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index 8030d396a5..1a4c51ec30 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -354,6 +354,14 @@ ['child' => 'hostDirectory', 'column' => 'hdHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'hostPrinter', 'column' => 'hpHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'hostSpooler', 'column' => 'hspHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'hostNetwork', 'column' => 'hnHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + // Both ends of a wake relay are hosts, and BOTH cascade. A deleted + // target has nothing left to wake; a deleted sender cannot be asked. + // Leaving either behind would leave a row naming a host id that has + // since been reused, which is how an admin ends up reading that a + // machine relayed a wake it has never heard of. + ['child' => 'agentWake', 'column' => 'awTargetID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], + ['child' => 'agentWake', 'column' => 'awSenderID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 14], ['child' => 'ldapUserGrant', 'column' => 'lugTargetID', 'parent' => '(lugTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ['child' => 'oidcUserGrant', 'column' => 'ougTargetID', 'parent' => '(ougTargetType)', 'pcolumn' => '-', 'class' => 'poly', 'action' => 'none'], ]; diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index 5afc0be8cd..d9e13d5960 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -158,6 +158,21 @@ 'atCreated' => 'datetime DEFAULT NULL', ], ], + 'agentWake' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `agentWake` ( `awID` int(11) NOT NULL AUTO_INCREMENT, `awTargetID` int(11) NOT NULL, `awSenderID` int(11) NOT NULL, `awRequestedAt` datetime DEFAULT NULL, `awExpiresAt` datetime DEFAULT NULL, `awStatus` varchar(16) NOT NULL DEFAULT \'pending\', `awPackets` int(11) NOT NULL DEFAULT 0, `awDetail` varchar(255) NOT NULL DEFAULT \'\', `awReportedAt` datetime DEFAULT NULL, `awRequestedBy` varchar(255) NOT NULL DEFAULT \'\', PRIMARY KEY (`awID`), KEY `awSenderStatus` (`awSenderID`,`awStatus`), KEY `awTargetID` (`awTargetID`), KEY `awExpiresAt` (`awExpiresAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'awID' => 'int(11) NOT NULL', + 'awTargetID' => 'int(11) NOT NULL', + 'awSenderID' => 'int(11) NOT NULL', + 'awRequestedAt' => 'datetime DEFAULT NULL', + 'awExpiresAt' => 'datetime DEFAULT NULL', + 'awStatus' => 'varchar(16) NOT NULL DEFAULT \'pending\'', + 'awPackets' => 'int(11) NOT NULL DEFAULT 0', + 'awDetail' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'awReportedAt' => 'datetime DEFAULT NULL', + 'awRequestedBy' => 'varchar(255) NOT NULL DEFAULT \'\'', + ], + ], 'apiTokens' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `apiTokens` ( `atID` int(11) NOT NULL AUTO_INCREMENT, `atUserID` int(11) NOT NULL DEFAULT 0, `atName` varchar(255) NOT NULL DEFAULT \'\', `atHash` char(64) NOT NULL DEFAULT \'\', `atEnabled` tinyint(1) NOT NULL DEFAULT 1, `atCreatedTime` datetime NOT NULL DEFAULT current_timestamp(), `atCreatedBy` varchar(255) NOT NULL DEFAULT \'\', `atLastUsed` datetime DEFAULT NULL, PRIMARY KEY (`atID`), UNIQUE KEY `atHash` (`atHash`), KEY `atUserID` (`atUserID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -406,7 +421,7 @@ 'hdPlacementAt' => 'datetime DEFAULT NULL', 'hdPlacementError' => 'varchar(255) NOT NULL DEFAULT \'\'', 'hdJoinAt' => 'datetime DEFAULT NULL', - 'hdJoinError' => 'varchar(255) NOT NULL DEFAULT \'\'' + 'hdJoinError' => 'varchar(255) NOT NULL DEFAULT \'\'', ], ], 'hostFactState' => [ @@ -432,6 +447,35 @@ 'hmIgnoreImaging' => 'tinyint(1) NOT NULL DEFAULT 0', ], ], + 'hostNetwork' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostNetwork` ( `hnID` int(11) NOT NULL AUTO_INCREMENT, `hnHostID` int(11) NOT NULL, `hnName` varchar(255) NOT NULL DEFAULT \'\', `hnMAC` varchar(17) NOT NULL DEFAULT \'\', `hnIPv4` varchar(15) NOT NULL DEFAULT \'\', `hnPrefix` tinyint(3) unsigned NOT NULL DEFAULT 0, `hnNetwork` varchar(15) NOT NULL DEFAULT \'\', `hnBroadcast` varchar(15) NOT NULL DEFAULT \'\', `hnUp` tinyint(1) NOT NULL DEFAULT 0, `hnWireless` tinyint(1) NOT NULL DEFAULT 0, `hnObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hnID`), UNIQUE KEY `hnHostAddress` (`hnHostID`,`hnName`,`hnIPv4`), KEY `hnLink` (`hnNetwork`,`hnPrefix`), KEY `hnMAC` (`hnMAC`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hnID' => 'int(11) NOT NULL', + 'hnHostID' => 'int(11) NOT NULL', + 'hnName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hnMAC' => 'varchar(17) NOT NULL DEFAULT \'\'', + 'hnIPv4' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnPrefix' => 'tinyint(3) unsigned NOT NULL DEFAULT 0', + 'hnNetwork' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnBroadcast' => 'varchar(15) NOT NULL DEFAULT \'\'', + 'hnUp' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hnWireless' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hnObservedAt' => 'datetime DEFAULT NULL', + ], + ], + 'hostPrinter' => [ + 'create' => 'CREATE TABLE IF NOT EXISTS `hostPrinter` ( `hpID` int(11) NOT NULL AUTO_INCREMENT, `hpHostID` int(11) NOT NULL, `hpName` varchar(255) NOT NULL DEFAULT \'\', `hpURI` varchar(1024) NOT NULL DEFAULT \'\', `hpDriver` varchar(255) NOT NULL DEFAULT \'\', `hpDefault` tinyint(1) NOT NULL DEFAULT 0, `hpShared` tinyint(1) NOT NULL DEFAULT 0, `hpObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hpID`), UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), KEY `hpName` (`hpName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', + 'columns' => [ + 'hpID' => 'int(11) NOT NULL', + 'hpHostID' => 'int(11) NOT NULL', + 'hpName' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', + 'hpDriver' => 'varchar(255) NOT NULL DEFAULT \'\'', + 'hpDefault' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpShared' => 'tinyint(1) NOT NULL DEFAULT 0', + 'hpObservedAt' => 'datetime DEFAULT NULL', + ], + ], 'hosts' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hosts` ( `hostID` int(11) NOT NULL AUTO_INCREMENT, `hostName` varchar(16) NOT NULL, `hostDesc` longtext NOT NULL DEFAULT \'\', `hostIP` varchar(25) NOT NULL DEFAULT \'\', `hostImage` int(11) DEFAULT NULL, `hostBuilding` int(11) NOT NULL DEFAULT 0, `hostCreateDate` timestamp NOT NULL DEFAULT current_timestamp(), `hostLastDeploy` datetime DEFAULT NULL, `hostCreateBy` varchar(50) NOT NULL DEFAULT \'\', `hostUseAD` char(1) NOT NULL DEFAULT \'\', `hostADDomain` varchar(250) NOT NULL DEFAULT \'\', `hostADOU` longtext NOT NULL DEFAULT \'\', `hostADUser` varchar(250) NOT NULL DEFAULT \'\', `hostADPass` varchar(250) NOT NULL DEFAULT \'\', `hostADPassLegacy` longtext NOT NULL DEFAULT \'\', `hostProductKey` longtext DEFAULT NULL, `hostPrinterLevel` varchar(2) NOT NULL DEFAULT \'\', `hostKernelArgs` varchar(250) NOT NULL DEFAULT \'\', `hostKernel` varchar(250) NOT NULL DEFAULT \'\', `hostDevice` varchar(250) NOT NULL DEFAULT \'\', `hostInit` longtext DEFAULT NULL, `hostPending` tinyint(1) NOT NULL DEFAULT 0, `hostPubKey` longtext NOT NULL DEFAULT \'\', `hostSecToken` longtext NOT NULL DEFAULT \'\', `hostSecTime` timestamp NULL DEFAULT NULL, `hostPingCode` varchar(20) DEFAULT NULL, `hostExitBios` longtext DEFAULT NULL, `hostExitEfi` longtext DEFAULT NULL, `hostEnforce` tinyint(1) NOT NULL DEFAULT 1, `hostInfoKey` varchar(255) DEFAULT NULL, `hostInfoLock` tinyint(1) DEFAULT 0, `hostSecTokenPrev` longtext NOT NULL DEFAULT \'\', `hostLastPing` datetime DEFAULT NULL, `hostLastCheckin` datetime DEFAULT NULL, `hostPingMethod` varchar(10) DEFAULT NULL, `hostArchID` mediumint(9) DEFAULT NULL, `hostSbState` varchar(16) DEFAULT NULL, `hostSbStateTime` datetime DEFAULT NULL, `hostSbEnrolled` datetime DEFAULT NULL, `hostSbEnrollCert` varchar(95) DEFAULT NULL, `hostSbEnrollVia` varchar(16) DEFAULT NULL, `hostAgentFingerprint` varchar(64) NOT NULL DEFAULT \'\', `hostAgentNotAfter` datetime DEFAULT NULL, `hostAgentVersion` varchar(50) NOT NULL DEFAULT \'\', `hostAgentCheckin` datetime DEFAULT NULL, PRIMARY KEY (`hostID`), UNIQUE KEY `hostName` (`hostName`), KEY `new_index` (`hostName`), KEY `new_index1` (`hostIP`), KEY `new_index4` (`hostUseAD`), KEY `fk_hosts_hostImage` (`hostImage`), KEY `fk_hosts_hostArchID` (`hostArchID`), KEY `hostAgentFingerprint` (`hostAgentFingerprint`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ @@ -482,19 +526,6 @@ 'hostAgentCheckin' => 'datetime DEFAULT NULL', ], ], - 'hostPrinter' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hostPrinter` ( `hpID` int(11) NOT NULL AUTO_INCREMENT, `hpHostID` int(11) NOT NULL, `hpName` varchar(255) NOT NULL DEFAULT \'\', `hpURI` varchar(1024) NOT NULL DEFAULT \'\', `hpDriver` varchar(255) NOT NULL DEFAULT \'\', `hpDefault` tinyint(1) NOT NULL DEFAULT 0, `hpShared` tinyint(1) NOT NULL DEFAULT 0, `hpObservedAt` datetime DEFAULT NULL, PRIMARY KEY (`hpID`), UNIQUE KEY `hpHostName` (`hpHostID`,`hpName`), KEY `hpName` (`hpName`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'hpID' => 'int(11) NOT NULL', - 'hpHostID' => 'int(11) NOT NULL', - 'hpName' => 'varchar(255) NOT NULL DEFAULT \'\'', - 'hpURI' => 'varchar(1024) NOT NULL DEFAULT \'\'', - 'hpDriver' => 'varchar(255) NOT NULL DEFAULT \'\'', - 'hpDefault' => 'tinyint(1) NOT NULL DEFAULT 0', - 'hpShared' => 'tinyint(1) NOT NULL DEFAULT 0', - 'hpObservedAt' => 'datetime DEFAULT NULL', - ], - ], 'hostScreenSettings' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hostScreenSettings` ( `hssID` int(11) NOT NULL AUTO_INCREMENT, `hssHostID` int(11) NOT NULL, `hssWidth` int(11) NOT NULL DEFAULT 0, `hssHeight` int(11) NOT NULL DEFAULT 0, `hssRefresh` int(11) NOT NULL DEFAULT 0, `hssOrientation` int(11) NOT NULL DEFAULT 0, `hssOther1` int(11) NOT NULL DEFAULT 0, `hssOther2` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`hssID`), UNIQUE KEY `hssHostID` (`hssHostID`), KEY `new_index` (`hssHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 2f4f783d2c..0a4d9ab32e 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -11384,3 +11384,98 @@ function () { . "ADD COLUMN `hdJoinAt` datetime DEFAULT NULL, " . "ADD COLUMN `hdJoinError` varchar(255) NOT NULL DEFAULT ''", ]; + +// 429 +$this->schema[] = [ + // Design 0011 section 3: which links a host is actually on. + // + // FOG has never recorded a host's interfaces. `hosts.hostIP` is + // whatever the host last resolved to -- one address, no prefix, no + // notion of which of several interfaces it came from -- so "which + // machines share a link with host 41" has not been a question this + // server could answer, and that question is the entire basis of the + // wake relay: a magic packet is a link-layer broadcast, and FOG can + // only send one from a machine it owns. + // + // hnNetwork is the address masked to hnPrefix, stored rather than + // computed. Two hosts are on the same link when both columns match, + // which is an index lookup; the honest alternative, + // `INET_ATON(hnIPv4) & mask`, is a full scan on every wake. + // + // One row per host per interface ADDRESS, not per interface: an + // interface with two addresses is on two links and can broadcast on + // both. Replaced in place, not a history -- this is current state. + // + // hnObservedAt is when the interfaces were last REPORTED, not when + // they were last confirmed. The agent hash-gates the block, so an + // unchanged set is never sent; "is this still true" is answered by + // the host's own hostAgentCheckin, which is also what says whether + // the machine is awake enough to relay anything. + "CREATE TABLE IF NOT EXISTS `hostNetwork` ( " + . "`hnID` int(11) NOT NULL AUTO_INCREMENT, " + . "`hnHostID` int(11) NOT NULL, " + . "`hnName` varchar(255) NOT NULL DEFAULT '', " + . "`hnMAC` varchar(17) NOT NULL DEFAULT '', " + . "`hnIPv4` varchar(15) NOT NULL DEFAULT '', " + . "`hnPrefix` tinyint(3) unsigned NOT NULL DEFAULT 0, " + . "`hnNetwork` varchar(15) NOT NULL DEFAULT '', " + . "`hnBroadcast` varchar(15) NOT NULL DEFAULT '', " + . "`hnUp` tinyint(1) NOT NULL DEFAULT 0, " + . "`hnWireless` tinyint(1) NOT NULL DEFAULT 0, " + . "`hnObservedAt` datetime DEFAULT NULL, " + . "PRIMARY KEY (`hnID`), " + . "UNIQUE KEY `hnHostAddress` (`hnHostID`,`hnName`,`hnIPv4`), " + . "KEY `hnLink` (`hnNetwork`,`hnPrefix`), " + . "KEY `hnMAC` (`hnMAC`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", +]; + +// 430 +$this->schema[] = [ + // Design 0011: one row per (machine to wake, machine asked to send it). + // + // A wake ordered now cannot be relayed now -- the neighboring agent + // finds out when it next polls -- so the request has to be written + // down somewhere, and FOG has nowhere. That is what this table is. + // + // Fanning out to SEVERAL senders is deliberate: a magic packet is one + // UDP datagram, sending three costs nothing, and the alternative is a + // wake that silently does nothing because the single chosen sender + // went to sleep between the poll and the send. + // + // It is also the first time FOG can say anything at all about whether + // a wake happened. The existing path is fire and forget: a machine + // that stays asleep is indistinguishable from a packet that never + // left the building. Here "three machines were asked and all three + // said they sent it" is a row an admin can read. + // + // awExpiresAt is what keeps a wake from being a standing instruction. + // A machine that comes back a week later must not be told to broadcast + // for a wake somebody ordered last Tuesday. + "CREATE TABLE IF NOT EXISTS `agentWake` ( " + . "`awID` int(11) NOT NULL AUTO_INCREMENT, " + . "`awTargetID` int(11) NOT NULL, " + . "`awSenderID` int(11) NOT NULL, " + . "`awRequestedAt` datetime DEFAULT NULL, " + . "`awExpiresAt` datetime DEFAULT NULL, " + . "`awStatus` varchar(16) NOT NULL DEFAULT 'pending', " + . "`awPackets` int(11) NOT NULL DEFAULT 0, " + . "`awDetail` varchar(255) NOT NULL DEFAULT '', " + . "`awReportedAt` datetime DEFAULT NULL, " + . "`awRequestedBy` varchar(255) NOT NULL DEFAULT '', " + . "PRIMARY KEY (`awID`), " + . "KEY `awSenderStatus` (`awSenderID`,`awStatus`), " + . "KEY `awTargetID` (`awTargetID`), " + . "KEY `awExpiresAt` (`awExpiresAt`) " + . ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC", + // Off by default. This asks one customer machine to put traffic on the + // network on behalf of another, which is a thing an estate owner opts + // into rather than discovers after an upgrade. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_AGENT_WAKE_RELAY_ENABLED','This setting defines if FOG may ask " + . "an enrolled agent to send a Wake-on-LAN packet for another FOG host " + . "on the same subnet. This reaches subnets that have no FOG server or " + . "storage node on them, which cannot be woken otherwise. Off by " + . "default. (Valid values: 0 or 1).','0','FOG Agent')", +]; diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 82cbde9bba..e83dbac6e4 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10389,7 +10389,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 61cd4ac03d..6929031908 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10398,7 +10398,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 457a3600df..080497fce7 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10557,7 +10557,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c0d258a24b..3b885aa4d3 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10390,7 +10390,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da4a4e9e3f..bf655dd2e9 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10382,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 74e587b351..6fe3a171cf 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10105,7 +10105,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 91a800020b..cef3891df0 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10062,7 +10062,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index d1eeba7d62..36890578dc 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8906,7 +8906,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 15d3a42fe7..baa6f23ea8 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 2c9e059e4e..bcc561b1b1 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Agent/NetworkFacts.php b/packages/web/src/Agent/NetworkFacts.php new file mode 100644 index 0000000000..49856a615f --- /dev/null +++ b/packages/web/src/Agent/NetworkFacts.php @@ -0,0 +1,299 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Base\FOGBase; +use FOG\Items\Host; + +/** + * Reconciles a reported network block into `hostNetwork` (design 0011 + * section 3). + * + * A fact report like InventoryFacts, registered the same way: an entry in + * State::FACT_REPORTS and a block in the poll, never a route of its own + * (the route rule, protocol-v1.md). + * + * The contrast to draw is with `hosts.hostIP`, which this does not replace. + * hostIP is one address with no prefix and no interface behind it, resolved + * whenever FOG last looked; these rows are the machine's own account of its + * links. The difference matters because a prefix is what turns an address + * into a LINK, and "which awake machine is on the same link as this + * sleeping one" is the question the wake relay is built out of. + * + * There is deliberately no audit line here. Interfaces move whenever a + * laptop changes desk, a VPN comes up or a container engine starts, and an + * audit entry per event would bury the results that matter under noise + * nobody asked for. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class NetworkFacts extends FOGBase +{ + /** + * Most interface addresses accepted from one host. + * + * A machine running containers legitimately carries dozens, so this is + * generous rather than tight. It is here because the list is input from + * an enrolled but otherwise untrusted host: a machine claiming a + * million interfaces must fail this check rather than the database. + */ + const MAX_INTERFACES = 128; + + /** + * Column widths, so an overlong value is truncated here rather than + * failing the insert under strict mode and costing the host its poll. + */ + const WIDTHS = [ + 'name' => 255, + 'mac' => 17, + 'ipv4' => 15, + 'network' => 15, + 'broadcast' => 15 + ]; + + /** + * Records the host's current interfaces. + * + * The list is complete by contract: any address currently recorded for + * this host and absent from it is gone. That is why the agent sends no + * block at all when it could not read its interfaces -- an empty list + * here means "this machine is on no link", and would clear every row it + * has (design 0006 section 6). + * + * @param Host $Host the host the certificate bound + * @param array $block the reported network block + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return void + */ + public static function report(Host $Host, array $block) + { + $list = $block['interfaces'] ?? []; + if (!is_array($list)) { + $list = []; + } + if (count($list) > self::MAX_INTERFACES) { + throw new \RuntimeException('interface list too large', 413); + } + + $hostID = (int)$Host->get('id'); + $incoming = self::clean($list); + $now = self::niceDate()->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s'); + + // Replace the set, in one transaction so nothing observes the + // intermediate empty state -- which matters more here than it does + // for printers, because a wake relay running against the empty + // moment would conclude the estate had no machine on any link. + self::$DB->query('START TRANSACTION'); + try { + self::$DB->query( + 'DELETE FROM `hostNetwork` WHERE `hnHostID`=:host', + [], + [':host' => $hostID] + ); + self::insert($hostID, $incoming, $now); + self::$DB->query('COMMIT'); + } catch (\Exception $e) { + self::$DB->query('ROLLBACK'); + throw $e; + } + } + + /** + * Normalizes the reported list, keyed by interface name and address. + * + * Keying deduplicates: a host reporting the same address twice would + * otherwise hit the unique index mid-insert and roll back the whole + * poll. + * + * Every address is validated here rather than trusted. The agent + * computes the network and the broadcast itself, and this is the class + * that decides whether to believe it -- so both are RECOMPUTED from the + * address and prefix, and the reported values are discarded. A host + * that claimed a network address it is not on would otherwise be a host + * that could join any link's relay group it liked. + * + * @param array $list the reported interfaces + * + * @return array key => normalized row + */ + private static function clean(array $list) + { + $out = []; + foreach ($list as $entry) { + if (!is_array($entry)) { + continue; + } + $row = []; + foreach (self::WIDTHS as $field => $width) { + $row[$field] = substr( + trim((string)($entry[$field] ?? '')), + 0, + $width + ); + } + $prefix = (int)($entry['prefix'] ?? 0); + if ('' === $row['name'] || $prefix < 0 || $prefix > 32) { + continue; + } + $long = self::ipToLong($row['ipv4']); + if (null === $long) { + // Not an IPv4 address. Dropped rather than stored: a row + // with no address is on no link, so nothing reads it. + continue; + } + $row['prefix'] = $prefix; + $row['network'] = self::networkFor($long, $prefix); + $row['broadcast'] = self::broadcastFor($long, $prefix); + $row['mac'] = strtolower($row['mac']); + $row['up'] = !empty($entry['up']) ? 1 : 0; + $row['wireless'] = !empty($entry['wireless']) ? 1 : 0; + $out[$row['name'] . '|' . $row['ipv4']] = $row; + } + + return $out; + } + + /** + * An IPv4 address as an unsigned 32-bit integer, or null. + * + * ip2long() alone is not the check: it accepts shortened forms like + * `10.1` that no interface reports, so the round trip through long2ip + * is what pins the value to the dotted quad the agent sent. + * + * @param string $ip the address + * + * @return int|null + */ + protected static function ipToLong($ip) + { + $long = ip2long($ip); + if (false === $long || long2ip($long) !== $ip) { + return null; + } + + return $long; + } + + /** + * The network address for an address and prefix. + * + * @param int $long the address + * @param int $prefix the prefix length + * + * @return string + */ + protected static function networkFor($long, $prefix) + { + return long2ip($long & self::mask($prefix)); + } + + /** + * The broadcast address, empty where the link has none. + * + * A /31 is a point-to-point pair (RFC 3021) and a /32 is a host route; + * neither has a broadcast address, and the all-ones address on a /31 + * names the peer rather than the link. + * + * @param int $long the address + * @param int $prefix the prefix length + * + * @return string + */ + protected static function broadcastFor($long, $prefix) + { + if ($prefix >= 31) { + return ''; + } + + return long2ip($long | (~self::mask($prefix) & 0xFFFFFFFF)); + } + + /** + * The netmask for a prefix length, as an integer. + * + * A /0 is spelled out rather than shifted: `-1 << 32` is undefined + * across platforms and PHP gives back -1, which would make every host + * in the estate share one link. + * + * @param int $prefix the prefix length + * + * @return int + */ + protected static function mask($prefix) + { + if ($prefix <= 0) { + return 0; + } + + return (-1 << (32 - $prefix)) & 0xFFFFFFFF; + } + + /** + * Inserts the reported interfaces. + * + * One statement rather than a row at a time, for PrinterFacts' reason: + * a container host with fifty interfaces would otherwise cost fifty + * round trips on every poll where anything moved. + * + * @param int $hostID the host + * @param array $incoming key => normalized row + * @param string $now the timestamp for this reconcile + * + * @return void + */ + private static function insert($hostID, array $incoming, $now) + { + if (empty($incoming)) { + return; + } + $values = []; + $binds = []; + $i = 0; + foreach ($incoming as $row) { + // A distinct placeholder name per value rather than reusing one + // for the host id and the timestamp: a real prepared statement + // binds each name once, and a driver that is not emulating them + // rejects the repeat with a bound-parameter count error. + $p = ':r' . $i++ . '_'; + $values[] = '(' . $p . 'h,' . $p . 'n,' . $p . 'm,' . $p . 'i,' + . $p . 'p,' . $p . 'w,' . $p . 'b,' . $p . 'u,' . $p . 'l,' + . $p . 'o)'; + $binds[$p . 'h'] = (int)$hostID; + $binds[$p . 'n'] = $row['name']; + $binds[$p . 'm'] = $row['mac']; + $binds[$p . 'i'] = $row['ipv4']; + $binds[$p . 'p'] = $row['prefix']; + $binds[$p . 'w'] = $row['network']; + $binds[$p . 'b'] = $row['broadcast']; + $binds[$p . 'u'] = $row['up']; + $binds[$p . 'l'] = $row['wireless']; + $binds[$p . 'o'] = $now; + } + self::$DB->query( + 'INSERT INTO `hostNetwork` ' + . '(`hnHostID`,`hnName`,`hnMAC`,`hnIPv4`,`hnPrefix`,`hnNetwork`,' + . '`hnBroadcast`,`hnUp`,`hnWireless`,`hnObservedAt`) VALUES ' + . implode(',', $values), + [], + $binds + ); + } +} diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 2ee0b7236a..5935568264 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -65,7 +65,12 @@ class State extends FOGBase // admins have been turning that one off for a decade and know // where it is, so a host's current choice carries over untouched // (design 0010 section 5). - 'printers' => 'printermanager' + 'printers' => 'printermanager', + // Gated on the EXISTING powermanagement module, for the printers + // reason: that is the switch an admin already turns off to stop + // FOG touching a machine's power, and relaying a wake is FOG using + // this machine to touch another one's (design 0011 section 4). + 'wake' => 'powermanagement' ]; /** @@ -93,6 +98,12 @@ class State extends FOGBase // vocabulary (joined, refused, unsupported) that needs somewhere to // live that is not that field. 'directory' => DirectoryJoin::class, + // The row here is another host's pending wake, which is the only + // item report whose id is NOT the reporting host's own. What makes + // that safe is the pending row itself: a host may only report on a + // wake it was actually asked to send, so there is no id it can + // name that it was not already handed. + 'wake' => WakeRelay::class, ]; /** @@ -125,6 +136,7 @@ class State extends FOGBase 'software' => SoftwareFacts::class, 'directory' => DirectoryFacts::class, 'printers' => PrinterFacts::class, + 'network' => NetworkFacts::class, ]; /** @@ -231,6 +243,18 @@ public static function desired(Host $Host) $state['directory'] = $directory; } } + if (in_array('wake', $capabilities, true)) { + // Design 0011: FOG hosts on this machine's own links that are + // waiting to be woken. Null for essentially every host on + // essentially every poll -- a wake is rare and pending for + // minutes -- so the block is omitted entirely rather than + // sent empty. There is no destination in it: the agent + // broadcasts on its own interfaces, so it cannot be aimed. + $wake = WakeRelay::desired($Host); + if (null !== $wake) { + $state['wake'] = $wake; + } + } if (in_array('power', $capabilities, true)) { // Design 0004. Schedules are what Client\PM hands the legacy // client: the host's own rows and its groups' grants through diff --git a/packages/web/src/Agent/WakeRelay.php b/packages/web/src/Agent/WakeRelay.php new file mode 100644 index 0000000000..aa650d390f --- /dev/null +++ b/packages/web/src/Agent/WakeRelay.php @@ -0,0 +1,433 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Items\Host; +use FOG\Router\Route; + +/** + * Asks an already-awake agent to broadcast a magic packet for a sleeping + * neighbor (design 0011). + * + * A magic packet is a link-layer broadcast, so FOG can only send one from a + * machine it owns. `FOGBase::wakeUp()` already fans out to every enabled, + * online storage node, which covers every link FOG has a machine on -- and + * in a routed estate a subnet routinely has FOG hosts on it and no FOG + * server or storage node at all. The documented answer, a directed + * broadcast, has been off by default on enterprise routers since the smurf + * attack, and asking a security team to re-enable it is asking them to undo + * a decision that was right. + * + * The sender that was always there is a machine already ON that link, + * already awake, already authenticated to FOG. That is what this class + * finds and asks. + * + * The security shape is the whole design, so both halves are stated here + * rather than only in the document: + * + * - THE SERVER PICKS BOTH ENDS. An agent never chooses a target, and the + * target is always a row in `hosts` whose MACs are that host's own + * `hostMAC` rows. There is no path from an arbitrary MAC to the wire. + * - THE AGENT IS NEVER TOLD WHERE TO SEND. The block carries host ids and + * MACs and no destination at all; the agent broadcasts on its own + * interfaces. An agent that accepted a destination would be a UDP + * reflector for whoever could feed it one. + * + * This is ADDITIONAL. The node fan-out still runs first and unchanged, and + * an estate with a storage node on the link never needs any of this. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class WakeRelay extends FOGBase +{ + /** + * The setting that turns the relay on for the whole install. + * + * Off by default. This asks one customer machine to put traffic on the + * network on behalf of another, which is a thing an estate owner opts + * into rather than discovers after an upgrade. + */ + const RELAY_SETTING = 'FOG_AGENT_WAKE_RELAY_ENABLED'; + + /** + * How many neighbors are asked for one wake. + * + * More than one on purpose. A magic packet is a single UDP datagram, so + * asking three costs nothing measurable, and the alternative is a wake + * that silently does nothing because the one chosen sender went to + * sleep between the poll that told it and the moment it would have + * sent. + */ + const MAX_SENDERS = 3; + + /** + * Seconds a request stays askable. + * + * This is what keeps a wake from becoming a standing instruction. A + * laptop that comes back next Tuesday must not be handed a wake + * somebody ordered last week, by which time the machine is either + * already awake or deliberately off. + */ + const TTL = 600; + + /** + * Seconds since a host's last check-in for it to count as awake. + * + * A sender that last polled an hour ago is a sender that is asleep, and + * asking it is how a wake gets recorded as pending forever. Three poll + * intervals at the default five minutes: long enough to survive one + * missed poll, short enough to mean something. + */ + const AWAKE_WITHIN = 900; + + /** + * Most targets in one poll answer, mirroring the agent's own constant. + * + * The agent enforces its own ceiling regardless of what arrives, which + * is the half that matters; this one keeps the server from composing a + * block it knows will be truncated. + */ + const MAX_TARGETS = 32; + + /** + * What an agent may report for one relay. + */ + const STATUS_SENT = 'sent'; + const STATUSES = ['sent', 'failed']; + + /** + * The state of a request nobody got to in time. + */ + const STATUS_PENDING = 'pending'; + const STATUS_EXPIRED = 'expired'; + + /** + * Longest detail kept: the column is a varchar(255) because this is a + * line an admin reads in a report, not a log. + */ + const MAX_DETAIL = 255; + + /** + * Asks this host's awake neighbors to wake it. + * + * Called alongside the existing node fan-out, never instead of it. A + * return of zero is the normal answer in an estate that does not need + * this: the relay is off, or FOG already owns a machine on the link. + * + * @param Host $Target the host to wake + * @param string $by who asked, for the record + * + * @return int how many neighbors were asked + */ + public static function request(Host $Target, $by = '') + { + if (!self::enabled() || !$Target->isValid()) { + return 0; + } + $targetID = (int)$Target->get('id'); + $senders = self::senders($targetID); + if (empty($senders)) { + return 0; + } + + $now = self::niceDate(); + $requestedAt = self::stamp($now); + $expiresAt = self::stamp( + (clone $now)->modify('+' . self::TTL . ' seconds') + ); + foreach ($senders as $senderID) { + $Wake = new \FOG\Items\AgentWake(); + $Wake + ->set('targetID', $targetID) + ->set('senderID', (int)$senderID) + ->set('requestedAt', $requestedAt) + ->set('expiresAt', $expiresAt) + ->set('status', self::STATUS_PENDING) + ->set('requestedBy', substr(trim((string)$by), 0, 255)) + ->save(); + } + + Audit::record( + [ + 'type' => 'agent.wake', + 'subjectType' => 'host', + 'subjectID' => $targetID, + 'subjectLabel' => (string)$Target->get('name'), + 'renderable' => 1, + 'affectedCount' => count($senders), + 'text' => substr( + sprintf( + 'asked %d neighboring agent(s) to broadcast a wake', + count($senders) + ), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + + return count($senders); + } + + /** + * The wake block for a host, or null when it has nothing to relay. + * + * Null is the answer essentially always: a wake is a rare event and it + * is pending for only a few minutes. + * + * @param Host $Host the principal + * + * @return array|null + */ + public static function desired(Host $Host) + { + if (!self::enabled()) { + return null; + } + self::expire(); + + $rows = self::$DB->query( + 'SELECT `awTargetID` FROM `agentWake` ' + . 'WHERE `awSenderID`=:sender AND `awStatus`=:pending ' + . 'ORDER BY `awRequestedAt` ASC LIMIT ' . (int)self::MAX_TARGETS, + [], + [ + ':sender' => (int)$Host->get('id'), + ':pending' => self::STATUS_PENDING + ] + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + + $targets = []; + foreach ((array)$rows as $row) { + $targetID = (int)($row['awTargetID'] ?? 0); + $macs = self::macsFor($targetID); + if (empty($macs)) { + // Nothing to send. Left pending so it expires on its own + // rather than being reported as a failure by a machine + // that never had anything to fail at. + continue; + } + $targets[] = ['id' => $targetID, 'macs' => $macs]; + } + if (empty($targets)) { + return null; + } + + return ['targets' => $targets]; + } + + /** + * Records what an agent did about one relay. + * + * The authorization is the pending row itself. A host may only report + * on a wake it was actually ASKED to send, which is the check that + * makes the item id safe to be another host's: without it any enrolled + * agent could write a result against any host in the estate. + * + * @param Host $Host the host the certificate bound + * @param int $targetID the host it says it woke + * @param array $body the reported result + * + * @throws \RuntimeException with an HTTP code when refused + * + * @return string the status recorded + */ + public static function report(Host $Host, $targetID, array $body) + { + $status = (string)($body['status'] ?? ''); + if (!in_array($status, self::STATUSES, true)) { + throw new \RuntimeException('unknown status', 400); + } + + $ids = Route::getIds( + 'agentwake', + [ + 'senderID' => (int)$Host->get('id'), + 'targetID' => (int)$targetID, + 'status' => self::STATUS_PENDING + ], + 'id' + ); + $id = (int)(array_shift($ids) ?: 0); + if ($id < 1) { + throw new \RuntimeException('no wake was requested of this host', 404); + } + + $Wake = new \FOG\Items\AgentWake($id); + $Wake + ->set('status', $status) + // The packet count, because "sent" with a count of zero would + // be a lie -- and FOG's existing wake path cannot tell the + // difference at all. + ->set('packets', max(0, (int)($body['packets'] ?? 0))) + ->set('detail', substr( + trim((string)($body['details'] ?? '')), + 0, + self::MAX_DETAIL + )) + ->set('reportedAt', self::stamp(self::niceDate())) + ->save(); + + return $status; + } + + /** + * The hosts that could broadcast for this one. + * + * The query is the design in one place. A candidate has to be: + * + * 1. on the same LINK -- the same network address AND the same prefix, + * which is what makes two addresses neighbors rather than merely + * similar, + * 2. able to broadcast there: the interface up, the link carrying a + * broadcast address at all, and not wireless (an access point will + * not bridge a broadcast to a station that is asleep and therefore + * not associated, so a wireless relay sends into a link the target + * has already left), + * 3. awake, judged by its own agent check-in, and + * 4. not the target, which is asleep and is the reason we are here. + * + * Ordered by the most recent check-in, because the machine that spoke + * most recently is the one most likely to still be listening. + * + * @param int $targetID the host to wake + * + * @return int[] host ids + */ + protected static function senders($targetID) + { + $rows = self::$DB->query( + 'SELECT DISTINCT `mine`.`hnHostID` AS `hostID` ' + . 'FROM `hostNetwork` AS `theirs` ' + . 'INNER JOIN `hostNetwork` AS `mine` ' + . 'ON `mine`.`hnNetwork` = `theirs`.`hnNetwork` ' + . 'AND `mine`.`hnPrefix` = `theirs`.`hnPrefix` ' + . 'INNER JOIN `hosts` ON `hostID` = `mine`.`hnHostID` ' + . 'WHERE `theirs`.`hnHostID` = :target ' + . 'AND `mine`.`hnHostID` <> :target2 ' + . 'AND `mine`.`hnUp` = 1 ' + . 'AND `mine`.`hnWireless` = 0 ' + . "AND `mine`.`hnBroadcast` <> '' " + . 'AND `hostAgentCheckin` >= :fresh ' + . 'ORDER BY `hostAgentCheckin` DESC ' + . 'LIMIT ' . (int)self::MAX_SENDERS, + [], + [ + ':target' => (int)$targetID, + ':target2' => (int)$targetID, + ':fresh' => self::stamp( + self::niceDate() + ->modify('-' . self::AWAKE_WITHIN . ' seconds') + ) + ] + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + + $out = []; + foreach ((array)$rows as $row) { + $out[] = (int)$row['hostID']; + } + + return $out; + } + + /** + * The target's MAC addresses. + * + * PENDING MACs are excluded, the way `Group::wakeOnLAN()` already does + * and `Host::wakeOnLAN()` does not. A pending MAC is one FOG has seen + * and nobody has accepted, and asking the fleet to broadcast at it is + * exactly the wrong default. This is a deliberate behavior difference + * from the existing path, and it is confined to the new one -- + * narrowing `Host::wakeOnLAN()` is a separate change with its own blast + * radius. + * + * @param int $targetID the host to wake + * + * @return string[] + */ + protected static function macsFor($targetID) + { + $macs = Route::getIds( + 'macaddressassociation', + ['hostID' => (int)$targetID, 'pending' => [0, '']], + 'mac' + ); + $out = []; + foreach ((array)$macs as $mac) { + $mac = trim((string)$mac); + if ('' !== $mac) { + $out[] = $mac; + } + } + + return array_values(array_unique($out)); + } + + /** + * Ages out requests nobody got to. + * + * Run on the read rather than on a cron: the rows only matter when a + * poll is composing a block, and a request that expired unnoticed is + * one nobody was going to act on anyway. + * + * @return void + */ + protected static function expire() + { + self::$DB->query( + 'UPDATE `agentWake` SET `awStatus`=:expired ' + . 'WHERE `awStatus`=:pending AND `awExpiresAt` < :now', + [], + [ + ':expired' => self::STATUS_EXPIRED, + ':pending' => self::STATUS_PENDING, + ':now' => self::stamp(self::niceDate()) + ] + ); + } + + /** + * Whether the relay is on for this install. + * + * @return bool + */ + protected static function enabled() + { + return (bool)self::getSetting(self::RELAY_SETTING); + } + + /** + * A moment in storage time. + * + * @param \DateTime $at the moment + * + * @return string + */ + protected static function stamp($at) + { + // Cloned, because setTimezone() and modify() both mutate in place: + // stamping a moment must not move the caller's copy of it. + $when = clone $at; + + return $when->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s'); + } +} diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 02a99d5fd0..5ffd73d8d1 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -440,7 +440,13 @@ class Authorization extends FOGBase 'hostdirectory' => 'host', 'hostprinter' => 'host', 'hostspooler' => 'host', + 'hostnetwork' => 'host', 'hostusersession' => 'host', + // A wake relay names two hosts and belongs to neither more than + // the other. Gated on the host node all the same: seeing that a + // machine was asked to wake another is seeing host detail, and + // ordering the wake goes through the host's own page. + 'agentwake' => 'host', 'hostfactstate' => 'host', 'software' => 'software', 'softwareassociation' => 'software', diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 3c611abae7..799288156b 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 428); + define('FOG_SCHEMA', 430); define('FOG_BCACHE_VER', 360); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Items/AgentWake.php b/packages/web/src/Items/AgentWake.php new file mode 100644 index 0000000000..c21bed0b22 --- /dev/null +++ b/packages/web/src/Items/AgentWake.php @@ -0,0 +1,99 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * A pending or finished wake relay. + * + * One row per (machine to wake, machine asked to send it). A wake ordered + * now cannot be relayed now -- the neighboring agent finds out when it + * next polls -- so the request has to be written down, and FOG has had + * nowhere to write it. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentWake extends FOGController +{ + /** + * The agentWake table. + * + * @var string + */ + protected $databaseTable = 'agentWake'; + /** + * The agentWake fields and common names. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'awID', + 'targetID' => 'awTargetID', + 'senderID' => 'awSenderID', + 'requestedAt' => 'awRequestedAt', + 'expiresAt' => 'awExpiresAt', + 'status' => 'awStatus', + 'packets' => 'awPackets', + 'detail' => 'awDetail', + 'reportedAt' => 'awReportedAt', + 'requestedBy' => 'awRequestedBy' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'targetID', + 'senderID' + ]; + /** + * Additional fields. + * + * @var array + */ + protected $additionalFields = [ + 'target', + 'sender' + ]; + /** + * The host being woken. + * + * @return object + */ + public function getTarget() + { + if (!array_key_exists('target', $this->data)) { + $this->set('target', new Host($this->get('targetID'))); + } + return $this->get('target'); + } + /** + * The host asked to send the packet. + * + * @return object + */ + public function getSender() + { + if (!array_key_exists('sender', $this->data)) { + $this->set('sender', new Host($this->get('senderID'))); + } + return $this->get('sender'); + } +} diff --git a/packages/web/src/Items/Group.php b/packages/web/src/Items/Group.php index 9fdd4e5b16..ca073f35d7 100644 --- a/packages/web/src/Items/Group.php +++ b/packages/web/src/Items/Group.php @@ -13,6 +13,7 @@ namespace FOG\Items; +use FOG\Agent\WakeRelay; use FOG\Assign\Resolver; use FOG\Base\FOGController; use FOG\Boot\SecureBootState; @@ -1158,6 +1159,14 @@ public function wakeOnLAN() ); self::wakeUp($hostMACs); } + // Design 0011, additional to the fan-out above. Per host rather + // than per MAC, because a relay request names the machine being + // woken so its result has a row to land on -- and because the + // neighbor that can reach one host on a link is not necessarily + // the one that can reach another. + foreach ((array)$this->get('hosts') as $hostID) { + WakeRelay::request(new Host($hostID), 'group wake'); + } } /** * Create snapin tasks for hosts. diff --git a/packages/web/src/Items/Host.php b/packages/web/src/Items/Host.php index 946b778018..a960e9f06d 100644 --- a/packages/web/src/Items/Host.php +++ b/packages/web/src/Items/Host.php @@ -13,6 +13,7 @@ namespace FOG\Items; +use FOG\Agent\WakeRelay; use FOG\Assign\Resolver; use FOG\Base\FOGController; use FOG\Boot\SecureBootState; @@ -1696,6 +1697,13 @@ public function createImagePackage( public function wakeOnLAN() { self::wakeUp($this->getMyMacs()); + // Design 0011, and ADDITIONAL to the line above rather than a + // replacement for it. wakeUp() reaches every link FOG owns a + // machine on; this reaches the links it does not, by asking an + // agent already awake there. Off by default and a no-op when the + // host has no awake neighbor, which is why it needs no branch + // here. + WakeRelay::request($this, 'host wake'); return $this; } /** diff --git a/packages/web/src/Items/HostNetwork.php b/packages/web/src/Items/HostNetwork.php new file mode 100644 index 0000000000..f38bf880cc --- /dev/null +++ b/packages/web/src/Items/HostNetwork.php @@ -0,0 +1,122 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Items; + +use FOG\Base\FOGController; + +/** + * One address on one of a host's interfaces. + * + * The contrast to draw is with `hosts.hostIP`, which this does not replace: + * hostIP is one address with no prefix and no interface behind it, resolved + * whenever FOG last looked. These rows are what the machine reported about + * its own links, prefix and all, which is the difference between knowing + * where a host answered from and knowing which link it is ON. + * + * One row per host per interface ADDRESS. An interface with two addresses + * is on two links and can broadcast on both, so it is two rows. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostNetwork extends FOGController +{ + /** + * The hostNetwork table. + * + * @var string + */ + protected $databaseTable = 'hostNetwork'; + /** + * The hostNetwork fields and common names. + * + * @var array + */ + protected $databaseFields = [ + 'id' => 'hnID', + 'hostID' => 'hnHostID', + 'name' => 'hnName', + 'mac' => 'hnMAC', + 'ipv4' => 'hnIPv4', + 'prefix' => 'hnPrefix', + 'network' => 'hnNetwork', + 'broadcast' => 'hnBroadcast', + 'up' => 'hnUp', + 'wireless' => 'hnWireless', + 'observedAt' => 'hnObservedAt' + ]; + /** + * The required fields. + * + * @var array + */ + protected $databaseFieldsRequired = [ + 'hostID' + ]; + /** + * Additional fields. + * + * @var array + */ + protected $additionalFields = [ + 'host' + ]; + /** + * Return the associated host object. + * + * @return object + */ + public function getHost() + { + if (!array_key_exists('host', $this->data)) { + $this->set('host', new Host($this->get('hostID'))); + } + return $this->get('host'); + } + /** + * The link in CIDR notation, for a report an admin reads. + * + * @return string + */ + public function link() + { + $network = trim((string)$this->get('network')); + if ('' === $network) { + return ''; + } + return $network . '/' . (int)$this->get('prefix'); + } + /** + * Whether this row can carry a broadcast for a neighbor. + * + * Three things have to be true and each has bitten a real WoL + * deployment: the interface has to be up (a configured NIC with the + * cable out sends nothing), the link has to HAVE a broadcast address + * (a /31 point-to-point pair and a /32 host route do not), and it must + * not be wireless -- an access point will not bridge a broadcast to a + * station that is asleep and therefore not associated, so a wireless + * relay is a packet sent into a link the target has already left. + * + * @return bool + */ + public function canRelay() + { + return (bool)$this->get('up') + && '' !== trim((string)$this->get('broadcast')) + && !(bool)$this->get('wireless'); + } +} diff --git a/packages/web/src/Managers/AgentWakeManager.php b/packages/web/src/Managers/AgentWakeManager.php new file mode 100644 index 0000000000..2b4b6798dc --- /dev/null +++ b/packages/web/src/Managers/AgentWakeManager.php @@ -0,0 +1,35 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * The agentWake manager. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentWakeManager extends FOGManagerController +{ + /** + * The base table name. + * + * @var string + */ + public $tablename = 'agentWake'; +} diff --git a/packages/web/src/Managers/HostNetworkManager.php b/packages/web/src/Managers/HostNetworkManager.php new file mode 100644 index 0000000000..af55e82946 --- /dev/null +++ b/packages/web/src/Managers/HostNetworkManager.php @@ -0,0 +1,35 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Managers; + +use FOG\Base\FOGManagerController; + +/** + * The hostNetwork manager. + * + * @category WakeRelay + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class HostNetworkManager extends FOGManagerController +{ + /** + * The base table name. + * + * @var string + */ + public $tablename = 'hostNetwork'; +} diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index a330a5e539..f5f8cd9cff 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -648,6 +648,7 @@ class Route extends FOGBase * @var array */ public static $validClasses = [ + 'agentwake', 'architecture', 'filedeletequeue', 'group', @@ -660,6 +661,7 @@ class Route extends FOGBase 'hostscreensetting', 'hostsoftware', 'hostdirectory', + 'hostnetwork', 'hostprinter', 'hostspooler', 'hostusersession', diff --git a/tests/agent-wake-relay.test.php b/tests/agent-wake-relay.test.php new file mode 100644 index 0000000000..97ff12fa98 --- /dev/null +++ b/tests/agent-wake-relay.test.php @@ -0,0 +1,334 @@ +setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +/** + * Call a protected static on NetworkFacts. + * + * @param string $name the method + * @param array $args the arguments + * + * @return mixed + */ +function nf($name, array $args = []) +{ + $m = new \ReflectionMethod(\FOG\Agent\NetworkFacts::class, $name); + $m->setAccessible(true); + + return $m->invokeArgs(null, $args); +} + +// ------------------------------------------------- the columns are mapped + +foreach ([ + [\FOG\Items\HostNetwork::class, ['hostID', 'name', 'mac', 'ipv4', 'prefix', + 'network', 'broadcast', 'up', 'wireless', 'observedAt']], + [\FOG\Items\AgentWake::class, ['targetID', 'senderID', 'requestedAt', + 'expiresAt', 'status', 'packets', 'detail', 'reportedAt', + 'requestedBy']] +] as [$class, $fields]) { + $p = new \ReflectionProperty($class, 'databaseFields'); + $p->setAccessible(true); + $mapped = array_keys((array)$p->getValue(new $class())); + foreach ($fields as $field) { + $t->check( + sprintf('%s maps %s', basename(str_replace('\\', '/', $class)), $field), + in_array($field, $mapped, true) + ); + } +} + +// ------------------------------------------------ the link math is the DB's + +// The agent computes these too, and this side recomputes them anyway. A +// host that could claim a network address it is not on would be a host that +// could join any link's relay group it liked. +foreach ([ + ['10.255.20.7', 24, '10.255.20.0', '10.255.20.255'], + ['192.168.1.66', 26, '192.168.1.64', '192.168.1.127'], + ['172.16.4.9', 16, '172.16.0.0', '172.16.255.255'], + ['10.0.0.5', 8, '10.0.0.0', '10.255.255.255'], +] as [$ip, $prefix, $network, $broadcast]) { + $long = nf('ipToLong', [$ip]); + $t->check( + sprintf('%s/%d is on %s', $ip, $prefix, $network), + $network === nf('networkFor', [$long, $prefix]), + (string)nf('networkFor', [$long, $prefix]) + ); + $t->check( + sprintf('%s/%d broadcasts to %s', $ip, $prefix, $broadcast), + $broadcast === nf('broadcastFor', [$long, $prefix]), + (string)nf('broadcastFor', [$long, $prefix]) + ); +} + +// A /31 is a point-to-point pair (RFC 3021) and a /32 is a host route. +// Sending the all-ones address on a /31 names the PEER, not the link. +foreach ([31, 32] as $prefix) { + $t->check( + sprintf('a /%d has no broadcast address', $prefix), + '' === nf('broadcastFor', [nf('ipToLong', ['10.0.0.1']), $prefix]) + ); +} + +// A /0 is spelled out rather than shifted: `-1 << 32` is undefined across +// platforms and PHP hands back -1, which would put every host in the estate +// on one link. +$t->check( + 'a /0 masks nothing rather than everything', + '0.0.0.0' === nf('networkFor', [nf('ipToLong', ['10.255.20.7']), 0]), + (string)nf('networkFor', [nf('ipToLong', ['10.255.20.7']), 0]) +); + +// ip2long alone accepts shortened forms no interface reports. +foreach (['10.1', '10.255.20', 'not-an-address', '', '999.1.1.1'] as $bad) { + $t->check( + sprintf('%s is not an address', '' === $bad ? '(empty)' : $bad), + null === nf('ipToLong', [$bad]) + ); +} + +// -------------------------------------------- a reported block is cleaned + +$clean = (new \ReflectionMethod( + \FOG\Agent\NetworkFacts::class, + 'clean' +)); +$clean->setAccessible(true); + +$rows = $clean->invoke(null, [ + // The address is believed; the network and broadcast are NOT. This host + // claims to be on a link it is not on. + ['name' => 'eno1', 'mac' => 'AA:BB:CC:DD:EE:FF', 'ipv4' => '10.255.20.7', + 'prefix' => 24, 'network' => '10.9.9.0', 'broadcast' => '10.9.9.255', + 'up' => true, 'wireless' => false], + // Same interface and address twice, as a host with a duplicated row + // would send it: the unique index would otherwise roll back the whole + // poll mid-insert. + ['name' => 'eno1', 'mac' => 'AA:BB:CC:DD:EE:FF', 'ipv4' => '10.255.20.7', + 'prefix' => 24, 'network' => '10.9.9.0', 'broadcast' => '10.9.9.255', + 'up' => true, 'wireless' => false], + // No name, no address, an impossible prefix: each dropped. + ['name' => '', 'ipv4' => '10.0.0.1', 'prefix' => 24], + ['name' => 'eno2', 'ipv4' => 'nonsense', 'prefix' => 24], + ['name' => 'eno3', 'ipv4' => '10.0.0.1', 'prefix' => 99], + ['name' => 'eno4', 'ipv4' => '10.0.0.1', 'prefix' => -1], + 'not even an array' +]); + +$t->check('one row survives the cleaning', 1 === count($rows), (string)count($rows)); +$row = array_shift($rows); +$t->check( + 'the network is RECOMPUTED, not taken from the host', + '10.255.20.0' === $row['network'], + (string)$row['network'] +); +$t->check( + 'and so is the broadcast, so a host cannot name a link it is not on', + '10.255.20.255' === $row['broadcast'], + (string)$row['broadcast'] +); +$t->check( + 'the MAC is folded to lower case, the way hostMAC stores it', + 'aa:bb:cc:dd:ee:ff' === $row['mac'], + (string)$row['mac'] +); +$t->check('a reported flag survives', 1 === $row['up']); + +// ------------------------------------------------------ the statuses + +$t->check( + 'sent is a status an agent may report', + in_array(\FOG\Agent\WakeRelay::STATUS_SENT, \FOG\Agent\WakeRelay::STATUSES, true) +); +// The server's own bookkeeping words are NOT things an agent may claim. +// Without this an agent could report itself `expired` and take a request +// off the board that nobody sent. +foreach ([ + \FOG\Agent\WakeRelay::STATUS_PENDING, + \FOG\Agent\WakeRelay::STATUS_EXPIRED +] as $internal) { + $t->check( + sprintf('an agent may not report %s', $internal), + !in_array($internal, \FOG\Agent\WakeRelay::STATUSES, true) + ); +} + +// ------------------------------------------------------- the block shape + +// The one that matters. Whatever else changes, an agent must never be told +// an address. +$forbidden = ['ip', 'address', 'addr', 'broadcast', 'destination', 'dst', + 'host', 'target_ip', 'port']; +$block = ['targets' => [['id' => 41, 'macs' => ['00:11:22:33:44:55']]]]; +$t->check( + 'the wake block names no destination at all', + [] === array_intersect($forbidden, array_keys($block['targets'][0])) +); +$t->check( + 'a target is a host id and its MACs, and nothing else', + ['id', 'macs'] === array_keys($block['targets'][0]) +); + +// ------------------------------------------------------ the registration + +$t->check( + 'wake is gated on the EXISTING powermanagement module, not a new switch', + 'powermanagement' === (\FOG\Agent\State::CAPABILITIES['wake'] ?? null), + (string)(\FOG\Agent\State::CAPABILITIES['wake'] ?? '') +); +$t->check( + 'a wake result rides the item half of the result route', + \FOG\Agent\WakeRelay::class === (\FOG\Agent\State::ITEM_REPORTS['wake'] ?? null) +); +$t->check( + 'the interfaces are a fact kind, not a route of their own', + \FOG\Agent\NetworkFacts::class === (\FOG\Agent\State::FACT_REPORTS['network'] ?? null) +); + +// ------------------------------------------------------- the relay is off + +$t->check( + 'the relay setting is off in a fresh schema step', + false !== strpos( + file_get_contents(__DIR__ . '/../packages/web/commons/schema.php'), + "'FOG_AGENT_WAKE_RELAY_ENABLED','This setting defines if FOG may ask " + ) +); + +// -------------------------------------------------- the ceilings are ours + +$t->check( + 'the target ceiling is a constant here, not a number an agent is told', + is_int(\FOG\Agent\WakeRelay::MAX_TARGETS) + && \FOG\Agent\WakeRelay::MAX_TARGETS > 0 +); +$t->check( + 'more than one neighbor is asked, so one going to sleep is not fatal', + \FOG\Agent\WakeRelay::MAX_SENDERS > 1, + (string)\FOG\Agent\WakeRelay::MAX_SENDERS +); +$t->check( + 'a request expires, so a wake is never a standing instruction', + \FOG\Agent\WakeRelay::TTL > 0 && \FOG\Agent\WakeRelay::TTL <= 3600, + (string)\FOG\Agent\WakeRelay::TTL +); +$t->check( + 'a sender has to have checked in recently enough to be awake', + \FOG\Agent\WakeRelay::AWAKE_WITHIN > 0 + && \FOG\Agent\WakeRelay::AWAKE_WITHIN <= 3600, + (string)\FOG\Agent\WakeRelay::AWAKE_WITHIN +); + +// ------------------------------------------- the sender query is the design + +$sql = (function () { + $file = file_get_contents( + __DIR__ . '/../packages/web/src/Agent/WakeRelay.php' + ); + $start = strpos($file, 'protected static function senders'); + return substr($file, $start, strpos($file, 'protected static function macsFor') - $start); +})(); + +foreach ([ + ['the same network', '`mine`.`hnNetwork` = `theirs`.`hnNetwork`'], + ['AND the same prefix -- a /16 and a /24 are not one link', + '`mine`.`hnPrefix` = `theirs`.`hnPrefix`'], + ['the interface is up', '`mine`.`hnUp` = 1'], + ['the link has a broadcast address at all', "`mine`.`hnBroadcast` <> ''"], + ['not wireless -- an AP will not bridge a broadcast to a sleeping station', + '`mine`.`hnWireless` = 0'], + ['the sender has checked in recently enough to be awake', + '`hostAgentCheckin` >= :fresh'], + ['and it is not the sleeping machine itself', + '`mine`.`hnHostID` <> :target2'] +] as [$what, $needle]) { + $t->check( + 'a candidate sender is chosen by: ' . $what, + false !== strpos($sql, $needle) + ); +} + +// ------------------------------------------ pending MACs stay off the wire + +$macs = (function () { + $file = file_get_contents( + __DIR__ . '/../packages/web/src/Agent/WakeRelay.php' + ); + $start = strpos($file, 'protected static function macsFor'); + return substr($file, $start, 1400); +})(); +$t->check( + 'a pending MAC is never broadcast at -- Group::wakeOnLAN already ' + . 'filters these and Host::wakeOnLAN does not', + false !== strpos($macs, "'pending' => [0, '']") +); + +// --------------------------------- the existing path is not replaced by it + +$host = file_get_contents( + __DIR__ . '/../packages/web/src/Items/Host.php' +); +$t->check( + 'Host::wakeOnLAN still fans out to the storage nodes first', + 1 === preg_match( + '/function wakeOnLAN\(\)\s*\{\s*self::wakeUp\(\$this->getMyMacs\(\)\);/', + $host + ) +); +$t->check( + 'and the relay is additional to it', + false !== strpos($host, 'WakeRelay::request($this,') +); + +exit($t->finish()); diff --git a/tests/fixtures/route-cascade-contract.txt b/tests/fixtures/route-cascade-contract.txt index 3aac5db21c..3429706408 100644 --- a/tests/fixtures/route-cascade-contract.txt +++ b/tests/fixtures/route-cascade-contract.txt @@ -1,3 +1,4 @@ +agentwake (nothing) architecture (nothing) filedeletequeue (nothing) group groupassociation groupID @@ -24,6 +25,7 @@ host task hostID hostautologout (nothing) hostdirectory (nothing) hostfactstate (nothing) +hostnetwork (nothing) hostprinter (nothing) hostscreensetting (nothing) hostsoftware (nothing) diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index b57534042b..022978d5f5 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -1,3 +1,14 @@ +agentwake 0 awID id - - +agentwake 1 awID DT_RowId f - +agentwake 2 awTargetID targetID - - +agentwake 3 awSenderID senderID - - +agentwake 4 awRequestedAt requestedAt - - +agentwake 5 awExpiresAt expiresAt - - +agentwake 6 awStatus status - - +agentwake 7 awPackets packets - - +agentwake 8 awDetail detail - - +agentwake 9 awReportedAt reportedAt - - +agentwake 10 awRequestedBy requestedBy - - architecture 0 archID id - - architecture 1 archID DT_RowId f - architecture 2 archName name - - @@ -136,6 +147,20 @@ hostfactstate 3 hfsHostID hostLink f:classname host hostfactstate 4 hfsKind kind - - hostfactstate 5 hfsHash hash - - hostfactstate 6 hfsUpdated updated - - +hostnetwork 0 hnID id - - +hostnetwork 1 hnID DT_RowId f - +hostnetwork 2 hnHostID hostID - - +hostnetwork 3 hnHostID hostLink f:classname host +hostnetwork 4 hnName name - - +hostnetwork 5 hnName mainlink f:classname,tmpcolumns - +hostnetwork 6 hnMAC mac - - +hostnetwork 7 hnIPv4 ipv4 - - +hostnetwork 8 hnPrefix prefix - - +hostnetwork 9 hnNetwork network - - +hostnetwork 10 hnBroadcast broadcast - - +hostnetwork 11 hnUp up - - +hostnetwork 12 hnWireless wireless - - +hostnetwork 13 hnObservedAt observedAt - - hostprinter 0 hpID id - - hostprinter 1 hpID DT_RowId f - hostprinter 2 hpHostID hostID - - diff --git a/tests/foreign-key-map.test.php b/tests/foreign-key-map.test.php index 4523b9bcbe..4525c3ea5e 100644 --- a/tests/foreign-key-map.test.php +++ b/tests/foreign-key-map.test.php @@ -371,6 +371,9 @@ 'hostDirectory.hdHostID', 'hostPrinter.hpHostID', 'hostSpooler.hspHostID', + 'hostNetwork.hnHostID', + 'agentWake.awTargetID', + 'agentWake.awSenderID', 'hostFactState.hfsHostID', // Plugin groups, named for the plugin rather than numbered. Each // lands in that plugin's own repo, in an appended step of its From 0ef4a93913ff6ec8c7078f79da8fe29ac33349d8 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Fri, 4 Sep 2026 22:47:49 +0000 Subject: [PATCH 085/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index e83dbac6e4..82cbde9bba 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10389,6 +10389,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 6929031908..61cd4ac03d 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10398,6 +10398,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 080497fce7..457a3600df 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10557,6 +10557,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 3b885aa4d3..c0d258a24b 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10390,6 +10390,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index bf655dd2e9..da4a4e9e3f 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 6fe3a171cf..74e587b351 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10105,6 +10105,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index cef3891df0..91a800020b 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10062,6 +10062,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 36890578dc..d1eeba7d62 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8906,6 +8906,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index baa6f23ea8..15d3a42fe7 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index bcc561b1b1..2c9e059e4e 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 6086662ee2f98ca934c7cb0c356c6162acfbce6d Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 05:53:14 -0500 Subject: [PATCH 086/117] Agent: report Secure Boot posture as a fact hosts.hostSbState is written by iPXE on every PXE boot, which is the right place for it -- iPXE runs whenever a machine netboots, where FOS runs only when someone schedules a task. But a machine that boots its own disk never netboots, so its value is frozen at whatever it said the last time anybody imaged it, and the staleness runs in the dangerous direction: `disabled` is the value that makes a host look like a valid enrollment target, and it is exactly what a machine leaves behind on the last netboot before it starts enforcing. Measured on host 105 telliottwin11, which the ledger called `disabled` while Confirm-SecureBootUEFI answered True. A second reporter for the same column, not a second vocabulary. The agent sends the same three raw values iPXE sends -- platform, the SecureBoot byte, the SetupMode byte -- and SecureBootFacts maps them with SecureBootState::fromBootRequest(), the very call the boot path uses. Letting the agent send a computed name would put the six-way mapping in two codebases in two languages, which is the drift the vocabulary was copied verbatim from FOS's sbState() to avoid. A registry entry and a block in the poll, never a route of its own (the route rule). facts() dispatches FACT_REPORTS generically and answers want_secureboot on its own, and hfsKind is varchar(16), so nothing else had to change. Still advisory (ADR 0029). An agent report arrives over an enrolled mTLS channel, so the server knows whose certificate asserted it -- that is attribution, not trust. A compromised OS can lie about its own firmware. Two things the tests caught before this shipped: - HostManager::update() takes item PROPERTY names. perform_update() looks each key up in Host::$databaseFields, so 'hostSbState' resolved to nothing and built an UPDATE with an empty column. It wants 'sbstate'. - The first version of the test read the SQL for the string "hostSbState" and passed against exactly that bug, because the bind placeholder is named :update_ from the property name. It now reads the column, and "writes nothing" means no UPDATE at all rather than no RECOGNIZED state -- the earlier form let an UNKNOWN write through, which is the one write the guard exists to prevent. Five mutants killed: the column name, the UNKNOWN guard, collapsing an absent key to '', ignoring setup_mode, and a client-supplied timestamp. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- packages/web/src/Agent/SecureBootFacts.php | 152 +++++++++++++ packages/web/src/Agent/State.php | 6 + tests/agent-secureboot-facts.test.php | 248 +++++++++++++++++++++ 3 files changed, 406 insertions(+) create mode 100644 packages/web/src/Agent/SecureBootFacts.php create mode 100644 tests/agent-secureboot-facts.test.php diff --git a/packages/web/src/Agent/SecureBootFacts.php b/packages/web/src/Agent/SecureBootFacts.php new file mode 100644 index 0000000000..a7ced62226 --- /dev/null +++ b/packages/web/src/Agent/SecureBootFacts.php @@ -0,0 +1,152 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Agent; + +use FOG\Audit\Audit; +use FOG\Base\FOGBase; +use FOG\Boot\SecureBootState; +use FOG\Items\Host; +use FOG\Managers\HostManager; + +/** + * Writes a reported Secure Boot posture onto hosts.hostSbState (design + * 0012). + * + * A fact report like InventoryFacts, and registered the same way: an entry + * in State::FACT_REPORTS and a block in the poll, never a route of its own + * (the route rule, protocol-v1.md). + * + * WHY A SECOND REPORTER AT ALL. hostSbState is written by iPXE on every PXE + * boot, which is the right place for it: iPXE runs whenever the machine + * netboots, where FOS runs only when someone schedules a task. But a machine + * that boots from its own disk never netboots, so for that machine the value + * is frozen at whatever it said the last time anybody imaged it -- and the + * staleness runs in the dangerous direction. `disabled` is the value that + * makes a host look like a valid enrollment target, and it is exactly what a + * machine leaves behind on the last netboot before it starts enforcing. + * + * OBSERVATIONS, NOT A VERDICT. The agent sends the same three raw values + * iPXE sends -- platform, the SecureBoot byte, the SetupMode byte -- and + * this class maps them with SecureBootState::fromBootRequest(), the very + * call the boot path uses. The six state names were copied verbatim from + * FOS's own sbState() so the reporters could not drift into two + * vocabularies for one fact; letting the agent send a computed name would + * reintroduce that drift with a third implementation, in Go. + * + * STILL ADVISORY (ADR 0029). An agent report arrives over an enrolled mTLS + * channel, so unlike an anonymous boot request the server knows whose + * certificate asserted it. That is attribution, not trust: a compromised + * operating system can lie about its own firmware. Nothing may read this + * column as a security control -- it is for targeting, filtering and + * display, exactly as before. + * + * @category SecureBoot + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class SecureBootFacts extends FOGBase +{ + /** + * Records a reported Secure Boot posture on the host row. + * + * Reached only when the server's hash for the block moved, so every + * call here is a real change in what the machine says about itself. + * + * @param Host $Host the host the certificate bound + * @param array $block the reported observations + * + * @return void + */ + public static function report(Host $Host, array $block) + { + // null and '' are DIFFERENT inputs to fromBootRequest(): '' means + // the machine looked and found nothing readable, null means it did + // not answer at all. Casting a missing key to '' would turn a + // malformed block into "UEFI, state unreadable", which asserts an + // observation about a machine that made none. + $state = SecureBootState::fromBootRequest( + (string)($block['platform'] ?? ''), + array_key_exists('secure_boot', $block) + ? (string)$block['secure_boot'] : null, + array_key_exists('setup_mode', $block) + ? (string)$block['setup_mode'] : null + ); + if (!SecureBootState::isKnown($state) + || SecureBootState::UNKNOWN === $state + ) { + // fromBootRequest() answers UNKNOWN when it was given nothing + // at all. Writing that would erase a real observation and + // replace it with "nobody has ever said", which is worse than + // the stale value it overwrote -- and the agent does not send + // the block at all when it has nothing to say, so arriving + // here means a malformed report rather than an honest silence. + return; + } + + $hostID = (int)$Host->get('id'); + $previous = (string)$Host->get('sbstate'); + + // The manager rather than Host::save(), as agentPoll and + // Enrollment::renew do: a save rewrites the MAC association, and a + // fact report is a routine call on every poll where the posture + // moved. + // + // The time is the SERVER's, on storageTimeZone() like every other + // datetime FOG writes. A client-supplied timestamp on a + // client-supplied observation is two lies for the price of one, and + // the question the column answers -- how stale is this -- is only + // meaningful in the server's own time base. + (new HostManager())->update( + ['id' => $hostID], + '', + [ + // PROPERTY names, not column names: update() looks each one + // up in Host::$databaseFields, so 'hostSbState' resolves to + // nothing and builds an UPDATE with an empty column. + 'sbstate' => $state, + 'sbstatetime' => self::niceDate() + ->setTimezone(self::storageTimeZone()) + ->format('Y-m-d H:i:s') + ] + ); + + // Renderable, and naming both ends of the move: "enforcing" alone + // does not tell an admin that this host just stopped being a valid + // enrollment target, which is the thing worth seeing in a list. + Audit::record( + [ + 'type' => 'agent.secureboot', + 'subjectType' => 'host', + 'subjectID' => $hostID, + 'subjectLabel' => (string)$Host->get('name'), + 'renderable' => 1, + 'affectedCount' => 1, + 'text' => substr( + sprintf( + 'agent reported Secure Boot %s%s', + SecureBootState::label($state), + '' === $previous || $previous === $state + ? '' + : ' (was ' . SecureBootState::label($previous) . ')' + ), + 0, + Audit::MAX_DETAIL + ), + 'authSource' => Principal::AUTH_SOURCE + ] + ); + } +} diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 5935568264..1fadd22588 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -137,6 +137,12 @@ class State extends FOGBase 'directory' => DirectoryFacts::class, 'printers' => PrinterFacts::class, 'network' => NetworkFacts::class, + // The second reporter for hosts.hostSbState (design 0012). iPXE + // writes the same column on every netboot; this one speaks on + // every poll, which is what a machine that boots its own disk + // actually does. Both go through + // SecureBootState::fromBootRequest() so there is one mapping. + 'secureboot' => SecureBootFacts::class, ]; /** diff --git a/tests/agent-secureboot-facts.test.php b/tests/agent-secureboot-facts.test.php new file mode 100644 index 0000000000..3d3787c051 --- /dev/null +++ b/tests/agent-secureboot-facts.test.php @@ -0,0 +1,248 @@ +set('id', $id)->set('name', 'lab-01')->set('sbstate', $state); + + return $Host; +} + +/** + * Run report() against the fake connection and return the UPDATE binds. + * + * @param FogFakeDb $db the fake connection + * @param array $block the reported block + * @param string $prior the posture already stored + * + * @return array [statements, binds] + */ +function sbReport($db, array $block, $prior = '') +{ + $db->log = []; + $binds = []; + $db->responder = function ($sql, $params) use (&$binds) { + $binds[] = [$sql, $params]; + return null; + }; + \FOG\Agent\SecureBootFacts::report(sbHost(7, $prior), $block); + $db->responder = null; + + return [$db->log, $binds]; +} + +/** + * The value an UPDATE actually bound to the hostSbState COLUMN. + * + * Deliberately reads the column and not the placeholder. perform_update() + * names the bind :update_ from the PROPERTY name and puts the column + * from Host::$databaseFields into the SQL, so a report passing 'hostSbState' + * still produces the string "hostSbState" in the statement -- as the + * placeholder -- while the column collapses to `hosts`.``. Matching the + * placeholder made this whole file pass against exactly that bug. + * + * @param array $binds the recorded statements + * + * @return string the bound value, or '' when no such UPDATE was issued + */ +function sbWritten(array $binds) +{ + foreach ($binds as list($sql, $params)) { + if (false === stripos($sql, 'UPDATE') + || !preg_match('/`hosts`\.`hostSbState`\s*=\s*:(\w+)/i', $sql, $m) + ) { + continue; + } + foreach ((array)$params as $k => $v) { + if (ltrim((string)$k, ':') === $m[1]) { + return (string)$v; + } + } + } + + return ''; +} + +/** + * How many UPDATEs against hosts the report issued. + * + * "Writes nothing" has to mean no statement at all. Asking only whether a + * RECOGNIZED state was written lets an UNKNOWN through, which is the one + * write the guard exists to prevent. + * + * @param array $binds the recorded statements + * + * @return int + */ +function sbUpdates(array $binds) +{ + $n = 0; + foreach ($binds as list($sql,)) { + if (false !== stripos($sql, 'UPDATE') && false !== stripos($sql, '`hosts`')) { + $n++; + } + } + + return $n; +} + +// ------------------------------------------------- the mapping is the server's + +$cases = [ + 'enforcing' => ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '00'], + 'disabled' => ['platform' => 'efi', 'secure_boot' => '00', 'setup_mode' => '00'], + // Setup Mode wins over the SecureBoot byte: db is writable there + // whatever SecureBoot says, and that is the difference between an + // enrollment nobody has to attend and one that needs a human. + 'setup' => ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '01'], + 'nonefi' => ['platform' => 'bios', 'secure_boot' => '', 'setup_mode' => ''], + 'noefivars' => ['platform' => 'efi', 'secure_boot' => '', 'setup_mode' => ''], +]; +foreach ($cases as $want => $block) { + list(, $binds) = sbReport($db, $block); + $t->check( + 'a ' . $want . ' machine stores ' . $want, + sbWritten($binds) === $want + ); +} + +// The measured lab case, both platforms, design 0012's status line. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '00'], + 'disabled' +); +$t->check( + 'telliottwin11 reporting {efi 01 00} replaces the stale disabled', + 'enforcing' === sbWritten($binds) +); + +// ------------------------------------------------------ a malformed block + +// Nothing at all. fromBootRequest answers UNKNOWN, and storing that would +// erase a real observation in favor of "nobody has ever said". +list(, $binds) = sbReport($db, [], 'enforcing'); +$t->check('an empty block writes no UPDATE at all', 0 === sbUpdates($binds)); + +// The keys are absent rather than empty, which is a different input: were +// they cast to '' this would store noefivars and assert that an enforcing +// machine's firmware had become unreadable. +list(, $binds) = sbReport($db, ['platform' => 'efi'], 'enforcing'); +$t->check( + 'a block with no observations in it writes nothing, not noefivars', + 0 === sbUpdates($binds) && '' === sbWritten($binds) +); + +// The same shape WITH the keys present is a real answer and must still +// store -- otherwise the guard above would swallow honest reports too. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => '', 'setup_mode' => ''], + 'enforcing' +); +$t->check( + 'a machine that looked and could not read the variables still reports', + 'noefivars' === sbWritten($binds) +); + +// A host cannot invent a state: the block carries bytes, and any byte that +// is not 00 or 01 lands in noefivars rather than passing through. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => 'yes', 'setup_mode' => 'no'] +); +$t->check( + 'an unrecognized byte does not become a state of its own', + 'noefivars' === sbWritten($binds) +); + +// A host cannot claim a platform either. Anything but efi is nonefi, so +// there is no way to reach a state by naming one. +list(, $binds) = sbReport( + $db, + ['platform' => 'enforcing', 'secure_boot' => '01', 'setup_mode' => '00'] +); +$t->check( + 'a made-up platform is nonefi, not the string the host sent', + 'nonefi' === sbWritten($binds) +); + +// The time is stamped here, in the server's own base. The column answers +// "how stale is this", so a client-supplied timestamp would make the one +// question it exists for unanswerable. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '00'] +); +$stamped = ''; +foreach ($binds as list($sql, $params)) { + if (preg_match('/`hosts`\.`hostSbStateTime`\s*=\s*:(\w+)/i', $sql, $m)) { + foreach ((array)$params as $k => $v) { + if (ltrim((string)$k, ':') === $m[1]) { + $stamped = (string)$v; + } + } + } +} +$t->check( + 'the state is stamped with a server-side datetime', + 1 === preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/', $stamped) +); +// Parsed in the STORAGE timezone, which is the one it was formatted in. +// strtotime() would read it in PHP's default zone instead and the check +// would drift by the offset between the two. +$parsed = '' === $stamped ? false : \DateTime::createFromFormat( + 'Y-m-d H:i:s', + $stamped, + \FOG\Base\FOGBase::storageTimeZone() +); +$t->check( + 'the stamp is now, not a value the host could have chosen', + false !== $parsed && abs(time() - $parsed->getTimestamp()) < 120 +); + +$t->finish(); From 9499c708c48ac3bb6f31905c511eebda75607ae4 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 06:15:48 -0500 Subject: [PATCH 087/117] Host page: show the agent's check-in, not the legacy client's The host form showed "Last Client Check-In" -- hostLastCheckin, written by the legacy FOG Client. fog-agent writes a different column, hostAgentCheckin, on every poll in Route::agentPoll(), and nothing rendered it anywhere. So a host running only the agent showed "Never", or a real date from months ago, for a machine that had polled a minute earlier. Two clients, two columns, one of them displayed. The agent replaces the legacy client, so the form now shows the agent's clock and the legacy field is gone from it. hostLastCheckin itself is untouched: the legacy client still writes it and the host list still carries a column for it, which is what a site running both during a migration needs. agentCheckin joins host's serverOwnedFields. It belongs there for the reason lastcheckin does -- a caller writing it asserts an event that did not happen -- and for one more that lastcheckin does not have: WakeRelay picks which hosts are fresh enough to relay a wake by this column, so a host able to write its own heartbeat could nominate itself as a relay for a subnet it is not on. The existing test checked a subset of the list by hand, so the new entry was not covered until it got its own assertion; removing it from the list now turns that assertion red. Two test gates this tripped, both fixed rather than worked around: - psr4-layout: SecureBootFacts had no home in bin/psr4-scan.php's TABLE. Added with the reason it is not called SecureBootState -- that name is the Boot class holding the six state names, and this only reports into that vocabulary. - utc-storage-boundary: it counts dateOrNever() calls that name their table and column, and its "hinted" pattern accepted only a lowercase property name. 'agentCheckin' is camelCase in Host::$databaseFields, so a correctly hinted call was reported as unhinted. Widened to [A-Za-z]; dropping the hint from the new call still turns it red. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- bin/psr4-scan.php | 5 +++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../en_US.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 9 +++-- .../web/management/languages/messages.pot | 7 ++-- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 7 ++-- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 7 ++-- packages/web/src/Pages/HostManagement.php | 33 ++++++++++++++----- packages/web/src/Router/Route.php | 7 ++++ tests/api-server-owned-fields.test.php | 13 ++++++++ tests/utc-storage-boundary.test.php | 6 +++- 15 files changed, 85 insertions(+), 51 deletions(-) diff --git a/bin/psr4-scan.php b/bin/psr4-scan.php index 8c18d8f1a5..1a55cf06b5 100644 --- a/bin/psr4-scan.php +++ b/bin/psr4-scan.php @@ -239,6 +239,11 @@ // hostSpooler rows, it is not that row. 'PrinterFacts' => 'Agent', 'PrinterSet' => 'Agent', + // The second writer for hosts.hostSbState (design 0012). Named + // SecureBootFacts and not SecureBootState because THAT name is the Boot + // class holding the six state names -- this reports observations into + // that vocabulary, it does not define it. + 'SecureBootFacts' => 'Agent', 'UserSessions' => 'Agent', 'TaskingElement' => 'TaskHandling', 'TaskQueue' => 'TaskHandling', diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 82cbde9bba..9a96d41e5a 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -5486,6 +5486,9 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "Zuletzt hochgeladen" @@ -5496,9 +5499,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Zuletzt verteilt" @@ -10389,7 +10389,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 61cd4ac03d..610ec38c1b 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -5486,6 +5486,9 @@ msgstr "Language" msgid "Largest images" msgstr "Images" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "Host Created" @@ -5496,9 +5499,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Last Deployed" @@ -10398,7 +10398,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 457a3600df..b5fd5fc929 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -5580,6 +5580,9 @@ msgstr "" msgid "Largest images" msgstr "Imagen" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "Creado" @@ -5590,9 +5593,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "última Desplegado" @@ -10557,7 +10557,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c0d258a24b..bc51973346 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -5487,6 +5487,9 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "Zuletzt hochgeladen" @@ -5497,9 +5500,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Zuletzt verteilt" @@ -10390,7 +10390,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index da4a4e9e3f..190d6ec8a5 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -5486,6 +5486,9 @@ msgstr "La langue" msgid "Largest images" msgstr "Images" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "hôte Créé" @@ -5496,9 +5499,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Dernière Déployé" @@ -10382,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 74e587b351..6fa9df11fa 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -5340,6 +5340,9 @@ msgstr "Lingua" msgid "Largest images" msgstr "immagini" +msgid "Last Agent Check-In" +msgstr "" + msgid "Last Captured" msgstr "Ultima cattura" @@ -5349,9 +5352,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Ultima Distribuita" @@ -10105,7 +10105,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 91a800020b..0622eedba2 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -5308,6 +5308,10 @@ msgstr "言語" msgid "Largest images" msgstr "イメージ" +#, fuzzy +msgid "Last Agent Check-In" +msgstr "タスクチェックイン日" + msgid "Last Captured" msgstr "最終キャプチャ" @@ -5319,10 +5323,6 @@ msgstr "タスクチェックイン日" msgid "Last Check-In" msgstr "タスクチェックイン日" -#, fuzzy -msgid "Last Client Check-In" -msgstr "タスクチェックイン日" - msgid "Last Deployed" msgstr "最終展開" @@ -10062,7 +10062,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index d1eeba7d62..4e88ad1a96 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -4695,6 +4695,9 @@ msgstr "" msgid "Largest images" msgstr "" +msgid "Last Agent Check-In" +msgstr "" + msgid "Last Captured" msgstr "" @@ -4704,9 +4707,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "" @@ -8906,7 +8906,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 15d3a42fe7..03b9339447 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -5486,6 +5486,9 @@ msgstr "Língua" msgid "Largest images" msgstr "imagens" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "host criado" @@ -5496,9 +5499,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "Última Implantado" @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 2c9e059e4e..04748e4855 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -5486,6 +5486,9 @@ msgstr "语言" msgid "Largest images" msgstr "图片" +msgid "Last Agent Check-In" +msgstr "" + #, fuzzy msgid "Last Captured" msgstr "主机创建" @@ -5496,9 +5499,6 @@ msgstr "" msgid "Last Check-In" msgstr "" -msgid "Last Client Check-In" -msgstr "" - msgid "Last Deployed" msgstr "最后部署" @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index 9531851186..d1603b226b 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -1649,10 +1649,22 @@ public function hostGeneral() strtoupper($pingMethod) ); } - $lastCheckin = self::dateOrNever( - $this->obj->get('lastcheckin'), + // The fog-agent's poll heartbeat, written by Route::agentPoll() on + // every poll. + // + // This REPLACED "Last Client Check-In" (hostLastCheckin), which the + // legacy FOG Client wrote and which this form showed instead. On a + // host running the agent that field read "Never" -- or, worse, a + // real date from months ago -- for a machine that had checked in a + // minute earlier, because the two clients write different columns + // and the page only ever rendered the old one. The agent replaces + // the legacy client, so the form shows the agent's clock. + // hostLastCheckin itself is untouched: the legacy client still + // writes it, and the host list still has a column for it. + $agentCheckin = self::dateOrNever( + $this->obj->get('agentCheckin'), 'hosts', - 'hostLastCheckin' + 'hostAgentCheckin' ); // The Secure Boot ledger's two halves, prepared very differently on // purpose (schema steps 376 and 377). @@ -1884,17 +1896,20 @@ public function hostGeneral() true, true ), + // OBSERVED, and disabled for the same reason as the rest of this + // group: it is a record of when a machine spoke, not a claim + // anyone may make on its behalf. self::makeLabel( $labelClass, - 'lastcheckin', - _('Last Client Check-In') + 'agentcheckin', + _('Last Agent Check-In') ) => self::makeInput( - 'form-control hostlastcheckin-input', - 'lastcheckin', + 'form-control hostagentcheckin-input', + 'agentcheckin', '', 'text', - 'lastcheckin', - $lastCheckin, + 'agentcheckin', + $agentCheckin, false, false, -1, diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index f5f8cd9cff..94e452af88 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -545,6 +545,13 @@ class Route extends FOGBase // is how they drift. 'lastping', 'lastcheckin', + // The fog-agent's poll heartbeat, written by agentPoll() only + // after a certificate authenticated the caller. It belongs here + // for the same reason lastcheckin does, and for one more: + // WakeRelay picks which hosts are fresh enough to relay a wake + // by this column, so a host that could write it could nominate + // itself as a relay for a subnet it is not on. + 'agentCheckin', // The observed half of the Secure Boot ledger (schema step 376). // This is the field the HARD constraint in ADR 0029 is about: it // is a REPORT of what a machine said, so a caller asserting it diff --git a/tests/api-server-owned-fields.test.php b/tests/api-server-owned-fields.test.php index 34f9b054a0..5e8c961899 100644 --- a/tests/api-server-owned-fields.test.php +++ b/tests/api-server-owned-fields.test.php @@ -198,6 +198,19 @@ public function get($key = '') ) ); +// Every column that records WHEN a machine spoke. A caller that can write +// one of these is asserting an event that did not happen -- and agentCheckin +// is not only a display field: WakeRelay chooses which hosts are fresh +// enough to relay a wake by it, so a host able to write its own heartbeat +// could nominate itself as a relay for a subnet it is not on. +$check( + 'the observed check-in columns are server-owned, agentCheckin included', + [] === array_diff( + ['lastping', 'lastcheckin', 'agentCheckin', 'sbstate', 'sbstatetime'], + Route::serverOwnedFields('host') + ) +); + $check( 'user.token is not server-owned; writing it takes over that account ' . "'s API access", diff --git a/tests/utc-storage-boundary.test.php b/tests/utc-storage-boundary.test.php index f4a2d9a6a7..06de9d8072 100644 --- a/tests/utc-storage-boundary.test.php +++ b/tests/utc-storage-boundary.test.php @@ -162,7 +162,11 @@ $root . '/packages/web/src/Pages/' . $page . '.php' ); $hinted = preg_match_all( - "#dateOrNever\(\s*\\\$this->obj->get\('[a-z]+'\),\s*'[a-z]+',\s*'[A-Za-z]+'\s*\)#s", + // The PROPERTY name is [A-Za-z], not [a-z]: most of Host's friendly + // names are lowercase but not all of them are -- 'agentCheckin' is + // camelCase in Host::$databaseFields -- and a hinted call was + // reported as unhinted purely for its casing. + "#dateOrNever\(\s*\\\$this->obj->get\('[A-Za-z]+'\),\s*'[a-z]+',\s*'[A-Za-z]+'\s*\)#s", $src ); $all = preg_match_all('#self::dateOrNever\(#', $src); From 7b443fc3b5c5cee0b6889f2835fdaac95590654d Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 11:17:13 +0000 Subject: [PATCH 088/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 9a96d41e5a..99475ec8f1 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10389,6 +10389,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 610ec38c1b..891a8915c9 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10398,6 +10398,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index b5fd5fc929..fd5da448ed 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10557,6 +10557,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index bc51973346..3ae3abd144 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10390,6 +10390,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 190d6ec8a5..0bc55303e7 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 6fa9df11fa..5be0697c28 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10105,6 +10105,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 0622eedba2..fb4cabb82b 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10062,6 +10062,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 4e88ad1a96..d2e74425aa 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8906,6 +8906,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 03b9339447..9813649aba 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 04748e4855..19a63eeba7 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 258db290ac68f41f34de54f4244660a3c72706df Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 06:20:16 -0500 Subject: [PATCH 089/117] SecureBootFacts: the audit line said "Secure Boot" twice Found by reading the first real report on the lab server, not by a test: agent reported Secure Boot Secure Boot ON (was Secure Boot OFF) SecureBootState::label() already spells the words where they belong, so the sprintf prefix duplicated them. Dropping it also reads correctly for the labels that do not carry the words -- "agent reported UEFI, state unreadable". The 13 checks in this file all passed straight through that, because none of them looked at the audit line -- the thing an admin actually reads. Four checks now do: that a line is recorded, that it does not repeat itself, that it names both ends of a transition, and that a FIRST report does not append "(was Never reported)" to every newly enrolled host. Reintroducing either defect turns them red. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- packages/web/src/Agent/SecureBootFacts.php | 8 +++- tests/agent-secureboot-facts.test.php | 54 ++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/packages/web/src/Agent/SecureBootFacts.php b/packages/web/src/Agent/SecureBootFacts.php index a7ced62226..4c055f23ca 100644 --- a/packages/web/src/Agent/SecureBootFacts.php +++ b/packages/web/src/Agent/SecureBootFacts.php @@ -135,8 +135,14 @@ public static function report(Host $Host, array $block) 'renderable' => 1, 'affectedCount' => 1, 'text' => substr( + // No "Secure Boot" prefix: label() already spells it + // where it belongs ("Secure Boot ON"), and prepending + // produced "agent reported Secure Boot Secure Boot ON" + // on the first real report. It reads correctly for the + // labels that do NOT carry the words, too -- "agent + // reported UEFI, state unreadable". sprintf( - 'agent reported Secure Boot %s%s', + 'agent reported %s%s', SecureBootState::label($state), '' === $previous || $previous === $state ? '' diff --git a/tests/agent-secureboot-facts.test.php b/tests/agent-secureboot-facts.test.php index 3d3787c051..8e0fcb4121 100644 --- a/tests/agent-secureboot-facts.test.php +++ b/tests/agent-secureboot-facts.test.php @@ -245,4 +245,58 @@ function sbUpdates(array $binds) false !== $parsed && abs(time() - $parsed->getTimestamp()) < 120 ); +/** + * The text of the audit line the report recorded, or '' for none. + * + * @param array $binds the recorded statements + * + * @return string + */ +function sbAuditText(array $binds) +{ + foreach ($binds as list($sql, $params)) { + if (false === stripos($sql, 'auditLog')) { + continue; + } + foreach ((array)$params as $v) { + if (is_string($v) && false !== strpos($v, 'agent reported')) { + return $v; + } + } + } + + return ''; +} + +// The audit line is what an admin actually reads, and it shipped saying +// "agent reported Secure Boot Secure Boot ON" -- label() already spells the +// words. Every other check in this file passed straight through that. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '00'], + 'disabled' +); +$text = sbAuditText($binds); +$t->check('the report records an audit line', '' !== $text); +$t->check( + 'the audit line does not repeat "Secure Boot"', + '' !== $text && false === strpos($text, 'Secure Boot Secure Boot') +); +$t->check( + 'the audit line names both ends of the move', + false !== strpos($text, 'Secure Boot ON') + && false !== strpos($text, 'was Secure Boot OFF') +); + +// A first report has no previous value to name, and "(was Never reported)" +// is noise on every newly enrolled host. +list(, $binds) = sbReport( + $db, + ['platform' => 'efi', 'secure_boot' => '01', 'setup_mode' => '00'] +); +$t->check( + 'a first report does not claim a previous state', + false === strpos(sbAuditText($binds), '(was') +); + $t->finish(); From fe5c6e73ed33cdbcf4805a217405fa4fb6fc01e4 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 06:30:47 -0500 Subject: [PATCH 090/117] Host info card: show when the host last checked in The card carries what you want at a glance -- name, MAC, image, last deploy, group -- and said nothing about whether the machine is still talking to FOG. That answer was two tabs away even after the General tab started showing it. One note, "Last Check-In", from whichever client last spoke, labeled so the card is not ambiguous about which one: "2026-09-05 06:29:14 (agent)". The agent wins when both columns are set, and deliberately not by comparing dates: a host that has enrolled an agent is a host whose legacy check-in has stopped moving, so the agent is the live signal even on the day it is installed. Falling back to hostLastCheckin keeps the card useful for hosts that have not migrated, which during a migration is most of them. A host neither client has ever reached reads "Never" with no source named, because naming one there would assert a client that was never installed. Two literal dateOrNever() calls rather than one with a computed column name. The column decides whether a date can predate the UTC boundary, and a ternary hides that from both a reader and tests/utc-storage-boundary.test.php -- which caught it. Verified against the three real cases on the lab server: host 105 and 239 (agent set) resolve to the agent's stamp, host 48 (neither) to Never. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../en_US.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../web/management/languages/messages.pot | 4 ++- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 4 ++- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 4 ++- packages/web/src/Pages/HostManagement.php | 34 +++++++++++++++++++ 11 files changed, 64 insertions(+), 10 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 99475ec8f1..969609474e 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10389,7 +10389,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11822,6 +11821,9 @@ msgstr "zusätzliche MACs haben" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr "vor" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 891a8915c9..88435397ec 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10398,7 +10398,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11830,6 +11829,9 @@ msgstr "Additional MACs" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr " ago" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index fd5da448ed..4f00c3cf62 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10557,7 +10557,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11991,6 +11990,9 @@ msgstr "" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr " hace" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 3ae3abd144..2ce56ea567 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10390,7 +10390,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11823,6 +11822,9 @@ msgstr "zusätzliche MACs haben" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr "vor" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 0bc55303e7..49be32449e 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10382,7 +10382,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11815,6 +11814,9 @@ msgstr "MACs supplémentaires" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr " depuis" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 5be0697c28..192ba6febd 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10105,7 +10105,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11499,6 +11498,9 @@ msgstr "MAC aggiuntivi" msgid "after" msgstr "" +msgid "agent" +msgstr "" + msgid "ago" msgstr "fa" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index fb4cabb82b..7e70d9f7a2 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10062,7 +10062,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11455,6 +11454,9 @@ msgstr "追加 MAC アドレス" msgid "after" msgstr "" +msgid "agent" +msgstr "" + msgid "ago" msgstr "前" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index d2e74425aa..32f83bebf7 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8906,7 +8906,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10179,6 +10178,9 @@ msgstr "" msgid "after" msgstr "" +msgid "agent" +msgstr "" + msgid "ago" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 9813649aba..9bfc1e397a 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11818,6 +11817,9 @@ msgstr "MACs adicionais" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr " atrás" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 19a63eeba7..9ebec9d16a 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11818,6 +11817,9 @@ msgstr "附加的MAC" msgid "after" msgstr "" +msgid "agent" +msgstr "" + #, fuzzy msgid "ago" msgstr "前" diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index d1603b226b..ac4c72f507 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -4476,10 +4476,44 @@ public function edit() // tabs: which image is assigned, when it was last imaged, and which // group it belongs to. $primaryGroup = new Group(self::minId($this->obj->get('groups'))); + // Whichever of the two clients last spoke, named so the card is not + // ambiguous about which one it is reporting. + // + // The agent WINS when both are set, and not by date: a host that has + // enrolled an agent is a host whose legacy check-in has stopped + // moving, so the newer client is the live signal even on the day + // the agent is installed. Falling back keeps the card useful for the + // hosts that have not migrated yet, which during a migration is most + // of them. + // Two literal calls rather than one with a computed column: the + // column each date came out of decides whether it can predate the + // UTC boundary, so it is named where a reader -- and + // tests/utc-storage-boundary.test.php -- can see it. + $agentRaw = (string)$this->obj->get('agentCheckin'); + if ('' !== $agentRaw && self::validDate($agentRaw)) { + $checkinWho = _('agent'); + $lastSpoke = self::dateOrNever( + $this->obj->get('agentCheckin'), + 'hosts', + 'hostAgentCheckin' + ); + } else { + $checkinWho = _('client'); + $lastSpoke = self::dateOrNever( + $this->obj->get('lastcheckin'), + 'hosts', + 'hostLastCheckin' + ); + } + if (_('Never') !== $lastSpoke) { + $lastSpoke = sprintf('%s (%s)', $lastSpoke, $checkinWho); + } + $this->notes = [ _('Host') => $this->obj->get('name'), _('Primary MAC') => (string)$this->obj->get('mac'), _('Assigned Image') => $this->obj->getImageName(), + _('Last Check-In') => $lastSpoke, _('Last Deployed') => self::dateOrNever( $this->obj->get('deployed'), 'hosts', From cd96ab0303be39e0a08aeceb799621335ede888a Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 11:32:03 +0000 Subject: [PATCH 091/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 969609474e..a1dee7c84f 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10389,6 +10389,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 88435397ec..9ab695baa2 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10398,6 +10398,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 4f00c3cf62..a717aa01a5 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10557,6 +10557,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2ce56ea567..dbbb7b1e89 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10390,6 +10390,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 49be32449e..233b8f5de0 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10382,6 +10382,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 192ba6febd..dfdeb8033f 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10105,6 +10105,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 7e70d9f7a2..8c914f61cd 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10062,6 +10062,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 32f83bebf7..4f4647f40b 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8906,6 +8906,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 9bfc1e397a..07293544df 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 9ebec9d16a..1defcdba4c 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 93aa8d93ce97f8ffd1ad3863a3e6fb8ba2afd5ca Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 06:48:23 -0500 Subject: [PATCH 092/117] Agent Activity: read the agent's audit trail by host Eleven agent.* audit types are written today -- enroll, token, result, inventory, software, printers, directory, directory.move, secureboot, usersession, wake -- every one tagged subjectType `host` with the host's id. Nothing could read them by host. AuditManagement lists the whole install in one flat grid whose getList() takes no arguments, so "what has this machine's agent been doing" meant scrolling past every other machine. Two surfaces, one query behind both: - A per-host tab on the host page, beside Task/Snapin History where the other per-host ledgers already live. - A page under Logging listing one row per host -- hostname, event count, last activity, last event -- expanding to that host's rows on demand. SUMMARY FIRST rather than a flat grid with group headers. DataTables' rowGroup groups only within the current PAGE, and the audit grid is serverSide, so one hostname would head a dozen separate pages. registerTable() also auto-pages any table using rowGroup -- Scroller cannot reconcile injected header rows -- which fog.audit.list.js already records as why the audit grid does not group. And an agent writes a row per changed fact per host, so a flat list is the one thing that grows without bound while a per-host summary is bounded by the fleet. The grouping happens in SQL, which is also the only way it is exact across the whole table rather than per page. The summary joins auditLog back to itself on MAX(alID) to name the last event type. GROUP_CONCAT(alType ORDER BY alID DESC) with SUBSTRING_INDEX would build a string of every row's type per host to read the first one, and truncates silently at group_concat_max_len. The join to `hosts` is LEFT: rows outlive the host, and on the lab install most of them already have -- those render as "(deleted host N)" rather than an empty cell that reads as a rendering fault. Its own permission node, not an alias onto `audit`. ADR 0021 made audit.view narrow because the audit log discloses attempted usernames and refusals; agent rows are what a machine reported about itself. Aliasing would force anyone who may see what an agent did to also see every failed sign-in in the install. view only -- auditlog has no create, update or delete route (ADR 0021 Decision 8). Both surfaces filter through one TYPE_PREFIX constant rather than listing the type names, so a new fact kind stays what the route rule says it is: a registry entry and a block in the poll, not a third place to remember. Verified against the live lab: the summary returns 21 hosts, and the scoped endpoint returns 79 rows for host 105 and 134 for host 239 -- the same counts the aggregate computed by a different query. 98 audit rows exist for host 105, so the type filter correctly excludes the 19 that are not the agent's. An absent, zero, negative or injection-shaped id returns nothing rather than falling through to every host. Seven mutants killed: the scope guard, the shared constant, a hardcoded type, the node alias, the menu group, rowGroup, and a dropped column. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../agentactivity/fog.agentactivity.list.js | 161 +++++++++++ .../management/js/fog/host/fog.host.edit.js | 60 ++++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 24 +- .../en_US.UTF-8/LC_MESSAGES/messages.po | 24 +- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 23 +- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 24 +- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 24 +- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 24 +- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 24 +- .../web/management/languages/messages.pot | 20 +- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 24 +- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 24 +- packages/web/src/Auth/Authorization.php | 11 + packages/web/src/Base/FOGPage.php | 12 +- .../web/src/Pages/AgentActivityManagement.php | 267 ++++++++++++++++++ packages/web/src/Pages/HostManagement.php | 59 ++++ tests/agent-activity-page.test.php | 212 ++++++++++++++ 17 files changed, 1006 insertions(+), 11 deletions(-) create mode 100644 packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js create mode 100644 packages/web/src/Pages/AgentActivityManagement.php create mode 100644 tests/agent-activity-page.test.php diff --git a/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js new file mode 100644 index 0000000000..98c71382f3 --- /dev/null +++ b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js @@ -0,0 +1,161 @@ +/** + * Agent activity, one row per host, expanded on demand. + * + * Read only by construction, not by omission: `auditlog` has no create, + * update or delete route anywhere in FOG (ADR 0021 Decision 8), so there is + * nothing here to wire a row action to. + * + * NO rowGroup, and not for the reason fog.audit.list.js gives. Grouping by + * host is the whole point of this page, but rowGroup groups only within the + * current PAGE, and the audit grid is serverSide -- so one hostname would + * head a dozen separate pages. The summary this table renders is grouped in + * SQL instead, which is exact across the whole table, and it is bounded by + * the size of the fleet where a flat list of agent rows is not. The scroller + * survives as a side effect: registerTable() auto-pages any table using + * rowGroup, and this one does not use it. + * + * The summary is CLIENT side -- one row per host is a bounded set, and + * sorting a fleet by "last seen" has to sort the fleet, not the page. Each + * expanded host's rows are server side, because that set is not bounded. + */ +(function($) { + var $table = $('#agentactivity-table'); + + if (!$table.length) { + return; + } + + // Every column escapes. An audit row carries subject labels and detail + // text that came from a machine on the network, so its contents are + // hostile by definition -- and DataTables writes cell data as HTML unless + // a column supplies its own render. + function escaped(field) { + return { + data: field, + render: function(d, t) { + // display only: the Buttons CSV/copy exports ask for other types and + // escaping those would put & into the exported file. + return t === 'display' ? $.escapeHtml(d === null ? '' : String(d)) : d; + } + }; + } + + var outcomeClass = { + allowed: 'text-bg-success', + denied: 'text-bg-danger', + failed: 'text-bg-warning', + partial: 'text-bg-warning', + unknown: 'text-bg-secondary' + }; + + // The expand control. A button and not a styled cell: it is operated by + // keyboard as well as by mouse, and a div with a click handler is not. + function toggleColumn() { + return { + data: null, + orderable: false, + searchable: false, + className: 'agentactivity-toggle', + render: function() { + return ''; + } + }; + } + + var table = $table.registerTable(null, { + // Newest activity first: the question this page answers is "what have + // the agents been doing", and a host silent for a month is not it. + order: [ + [3, 'desc'] + ], + columns: [ + toggleColumn(), + escaped('hostName'), + escaped('events'), + escaped('lastTime'), + escaped('lastType') + ], + rowId: 'hostID', + processing: true, + // Client side on purpose -- see the file docblock. + serverSide: false, + select: false, + ajax: { + url: '../management/index.php?node=agentactivity&sub=getList', + type: 'post' + } + }); + + // One child table per expanded host, each its own DataTable against the + // scoped endpoint. Destroyed on collapse rather than hidden: leaving them + // alive means every host a user has ever opened keeps redrawing behind a + // closed row. + function childTable(hostID) { + var id = 'agentactivity-child-' + hostID; + + return '
    ' + + '' + + '' + + '' + + '' + + '
    ' + $.escapeHtml('When') + '' + $.escapeHtml('Event') + '' + $.escapeHtml('Detail') + '' + $.escapeHtml('Outcome') + '
    '; + } + + function buildChild(hostID) { + $('#agentactivity-child-' + hostID).DataTable({ + order: [ + [0, 'desc'] + ], + columns: [ + escaped('createdTime'), + escaped('type'), + escaped('text'), + { + data: 'outcome', + render: function(d, t) { + var v = d === null ? '' : String(d); + if (t !== 'display') { + return v; + } + return '' + + $.escapeHtml(v) + ''; + } + } + ], + processing: true, + serverSide: true, + searching: false, + lengthChange: false, + pageLength: 10, + ajax: { + url: '../management/index.php?node=agentactivity' + + '&sub=getHostActivity&id=' + encodeURIComponent(hostID), + type: 'post' + } + }); + } + + $table.on('click', '.agentactivity-expand', function() { + var $btn = $(this), + row = table.row($btn.closest('tr')), + hostID = row.id(); + + if (row.child.isShown()) { + $('#agentactivity-child-' + hostID).DataTable().destroy(); + row.child.hide(); + $btn.attr('aria-expanded', 'false') + .find('i').removeClass('fa-chevron-down').addClass('fa-chevron-right'); + return; + } + + row.child(childTable(hostID)).show(); + buildChild(hostID); + $btn.attr('aria-expanded', 'true') + .find('i').removeClass('fa-chevron-right').addClass('fa-chevron-down'); + }); +})(jQuery); diff --git a/packages/web/management/js/fog/host/fog.host.edit.js b/packages/web/management/js/fog/host/fog.host.edit.js index f6b0fef074..ef2bfde94e 100644 --- a/packages/web/management/js/fog/host/fog.host.edit.js +++ b/packages/web/management/js/fog/host/fog.host.edit.js @@ -1538,6 +1538,66 @@ } }); + // --------------------------------------------------------------- + // AGENT ACTIVITY TAB + // + // The same rows the Logging > Agent Activity page shows for this host, + // through the same query. Every column escapes: an audit row carries + // subject labels and detail text a machine on the network supplied, and + // DataTables writes cell data as HTML unless a column renders it. + var agentOutcomeClass = { + allowed: 'text-bg-success', + denied: 'text-bg-danger', + failed: 'text-bg-warning', + partial: 'text-bg-warning', + unknown: 'text-bg-secondary' + }; + function agentEscaped(field) { + return { + data: field, + render: function(d, t) { + // display only: the CSV/copy exports ask for other types and + // escaping those would put & into the exported file. + return t === 'display' + ? $.escapeHtml(d === null ? '' : String(d)) + : d; + } + }; + } + var hostAgentActivityTable = $('#host-agent-activity-table').registerTable(null, { + columns: [ + agentEscaped('createdTime'), + agentEscaped('type'), + agentEscaped('text'), + { + data: 'outcome', + render: function(d, t) { + var v = d === null ? '' : String(d); + if (t !== 'display') { + return v; + } + return '' + $.escapeHtml(v) + ''; + } + } + ], + order: [ + [0, 'desc'] + ], + rowId: 'id', + processing: true, + serverSide: true, + select: false, + ajax: { + url: '../management/index.php?node=' + + Common.node + + '&sub=getAgentActivity&id=' + + Common.id, + type: 'post' + } + }); + // Enable searching if (Common.search && Common.search.length > 0) { macsTable.search(Common.search).draw(); diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a1dee7c84f..a58a0372a4 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -293,6 +293,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Ausgewählten MAcs freigeben" + msgid "(deleted user)" msgstr "" @@ -1048,6 +1052,10 @@ msgstr "Erweitert" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Aktiv" + #, fuzzy msgid "Agent Approval Success" msgstr "Host erfolgreich erstellt" @@ -2729,6 +2737,10 @@ msgstr "" msgid "Destroy failed" msgstr "Zerstörung fehlgeschlagen: %s" +#, fuzzy +msgid "Detail" +msgstr "Details" + #, fuzzy msgid "Details" msgstr "Details" @@ -4135,6 +4147,9 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "Host %1$s finished deploying image %2$s." msgstr "Dieser Host ist bereits vorhanden." +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "Host erfolgreich erstellt" @@ -5486,6 +5501,10 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" +#, fuzzy +msgid "Last Activity" +msgstr "Aktiv" + msgid "Last Agent Check-In" msgstr "" @@ -5502,6 +5521,10 @@ msgstr "" msgid "Last Deployed" msgstr "Zuletzt verteilt" +#, fuzzy +msgid "Last Event" +msgstr "Zuletzt hochgeladen" + msgid "Last Ping" msgstr "" @@ -10389,7 +10412,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 9ab695baa2..6d8c36379f 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -298,6 +298,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Approve selected Hosts" + msgid "(deleted user)" msgstr "" @@ -1052,6 +1056,10 @@ msgstr "Advanced" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Active" + #, fuzzy msgid "Agent Approval Success" msgstr "Host Created" @@ -2732,6 +2740,10 @@ msgstr "" msgid "Destroy failed" msgstr "Destroy failed: %s" +#, fuzzy +msgid "Detail" +msgstr "Snapin Return Detail" + #, fuzzy msgid "Details" msgstr "Snapin Return Detail" @@ -4137,6 +4149,9 @@ msgstr "Printer already exists" msgid "Host %1$s finished deploying image %2$s." msgstr "Printer already exists" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "Host Created" @@ -5486,6 +5501,10 @@ msgstr "Language" msgid "Largest images" msgstr "Images" +#, fuzzy +msgid "Last Activity" +msgstr "Active" + msgid "Last Agent Check-In" msgstr "" @@ -5502,6 +5521,10 @@ msgstr "" msgid "Last Deployed" msgstr "Last Deployed" +#, fuzzy +msgid "Last Event" +msgstr "Host Created" + msgid "Last Ping" msgstr "" @@ -10398,7 +10421,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index a717aa01a5..45ee0bcab3 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -296,6 +296,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "retirar" + msgid "(deleted user)" msgstr "" @@ -1064,6 +1068,10 @@ msgstr "Avanzado" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Activo" + #, fuzzy msgid "Agent Approval Success" msgstr "Creado" @@ -2760,6 +2768,9 @@ msgstr "" msgid "Destroy failed" msgstr "Destruir fallado: %s" +msgid "Detail" +msgstr "" + msgid "Details" msgstr "" @@ -4188,6 +4199,9 @@ msgstr "Impresora ya existe" msgid "Host %1$s finished deploying image %2$s." msgstr "Impresora ya existe" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "Creado" @@ -5580,6 +5594,10 @@ msgstr "" msgid "Largest images" msgstr "Imagen" +#, fuzzy +msgid "Last Activity" +msgstr "Activo" + msgid "Last Agent Check-In" msgstr "" @@ -5596,6 +5614,10 @@ msgstr "" msgid "Last Deployed" msgstr "última Desplegado" +#, fuzzy +msgid "Last Event" +msgstr "Creado" + msgid "Last Ping" msgstr "" @@ -10557,7 +10579,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index dbbb7b1e89..2a7f0c59a7 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -293,6 +293,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Ausgewählten MAcs freigeben" + msgid "(deleted user)" msgstr "" @@ -1048,6 +1052,10 @@ msgstr "Erweitert" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Aktiv" + #, fuzzy msgid "Agent Approval Success" msgstr "Host erfolgreich erstellt" @@ -2729,6 +2737,10 @@ msgstr "" msgid "Destroy failed" msgstr "Zerstörung fehlgeschlagen: %s" +#, fuzzy +msgid "Detail" +msgstr "Details" + #, fuzzy msgid "Details" msgstr "Details" @@ -4135,6 +4147,9 @@ msgstr "Dieser Host ist bereits vorhanden." msgid "Host %1$s finished deploying image %2$s." msgstr "Dieser Host ist bereits vorhanden." +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "Host erfolgreich erstellt" @@ -5487,6 +5502,10 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" +#, fuzzy +msgid "Last Activity" +msgstr "Aktiv" + msgid "Last Agent Check-In" msgstr "" @@ -5503,6 +5522,10 @@ msgstr "" msgid "Last Deployed" msgstr "Zuletzt verteilt" +#, fuzzy +msgid "Last Event" +msgstr "Zuletzt hochgeladen" + msgid "Last Ping" msgstr "" @@ -10390,7 +10413,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 233b8f5de0..9fa86120c3 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -299,6 +299,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Approuver hôtes sélectionnés" + msgid "(deleted user)" msgstr "" @@ -1053,6 +1057,10 @@ msgstr "Avancée" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "actif" + #, fuzzy msgid "Agent Approval Success" msgstr "hôte Créé" @@ -2732,6 +2740,10 @@ msgstr "" msgid "Destroy failed" msgstr "Destroy a échoué: %s" +#, fuzzy +msgid "Detail" +msgstr "Snapin Retour Détail" + #, fuzzy msgid "Details" msgstr "Snapin Retour Détail" @@ -4137,6 +4149,9 @@ msgstr "Imprimante existe déjà" msgid "Host %1$s finished deploying image %2$s." msgstr "Imprimante existe déjà" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "hôte Créé" @@ -5486,6 +5501,10 @@ msgstr "La langue" msgid "Largest images" msgstr "Images" +#, fuzzy +msgid "Last Activity" +msgstr "actif" + msgid "Last Agent Check-In" msgstr "" @@ -5502,6 +5521,10 @@ msgstr "" msgid "Last Deployed" msgstr "Dernière Déployé" +#, fuzzy +msgid "Last Event" +msgstr "hôte Créé" + msgid "Last Ping" msgstr "" @@ -10382,7 +10405,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index dfdeb8033f..09f254b94c 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -298,6 +298,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Approvare MAC selezionati" + msgid "(deleted user)" msgstr "" @@ -1026,6 +1030,10 @@ msgstr "Avanzate" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Attivo" + #, fuzzy msgid "Agent Approval Success" msgstr "Creazione Host con successo" @@ -2668,6 +2676,10 @@ msgstr "" msgid "Destroy failed" msgstr "Deistruzione fallita" +#, fuzzy +msgid "Detail" +msgstr "Dettagli della macchina" + #, fuzzy msgid "Details" msgstr "Dettagli della macchina" @@ -4044,6 +4056,9 @@ msgstr "Questo host esiste già" msgid "Host %1$s finished deploying image %2$s." msgstr "Questo host esiste già" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "Creazione Host con successo" @@ -5340,6 +5355,10 @@ msgstr "Lingua" msgid "Largest images" msgstr "immagini" +#, fuzzy +msgid "Last Activity" +msgstr "Attivo" + msgid "Last Agent Check-In" msgstr "" @@ -5355,6 +5374,10 @@ msgstr "" msgid "Last Deployed" msgstr "Ultima Distribuita" +#, fuzzy +msgid "Last Event" +msgstr "Ultima cattura" + msgid "Last Ping" msgstr "" @@ -10105,7 +10128,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 8c914f61cd..5f0e7959ea 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -289,6 +289,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "選択したホストを承認" + #, fuzzy msgid "(deleted user)" msgstr "選択したユーザーを追加" @@ -1007,6 +1011,10 @@ msgstr "詳細" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "有効" + #, fuzzy msgid "Agent Approval Success" msgstr "承認に成功しました" @@ -2655,6 +2663,10 @@ msgstr "AD ドメイン" msgid "Destroy failed" msgstr "解除に失敗しました" +#, fuzzy +msgid "Detail" +msgstr "マシン詳細" + #, fuzzy msgid "Details" msgstr "マシン詳細" @@ -4023,6 +4035,9 @@ msgstr "このホストは既に存在します" msgid "Host %1$s finished deploying image %2$s." msgstr "このホストは既に存在します" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "承認に成功しました" @@ -5308,6 +5323,10 @@ msgstr "言語" msgid "Largest images" msgstr "イメージ" +#, fuzzy +msgid "Last Activity" +msgstr "有効" + #, fuzzy msgid "Last Agent Check-In" msgstr "タスクチェックイン日" @@ -5326,6 +5345,10 @@ msgstr "タスクチェックイン日" msgid "Last Deployed" msgstr "最終展開" +#, fuzzy +msgid "Last Event" +msgstr "最終キャプチャ" + msgid "Last Ping" msgstr "" @@ -10062,7 +10085,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 4f4647f40b..e55df94790 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -274,6 +274,10 @@ msgstr "" msgid "(all)" msgstr "" +#, php-format +msgid "(deleted host %d)" +msgstr "" + msgid "(deleted user)" msgstr "" @@ -914,6 +918,9 @@ msgstr "" msgid "Agent" msgstr "" +msgid "Agent Activity" +msgstr "" + msgid "Agent Approval Success" msgstr "" @@ -2358,6 +2365,9 @@ msgstr "" msgid "Destroy failed" msgstr "" +msgid "Detail" +msgstr "" + msgid "Details" msgstr "" @@ -3565,6 +3575,9 @@ msgstr "" msgid "Host %1$s finished deploying image %2$s." msgstr "" +msgid "Host Agent Activity" +msgstr "" + msgid "Host Approval Success" msgstr "" @@ -4695,6 +4708,9 @@ msgstr "" msgid "Largest images" msgstr "" +msgid "Last Activity" +msgstr "" + msgid "Last Agent Check-In" msgstr "" @@ -4710,6 +4726,9 @@ msgstr "" msgid "Last Deployed" msgstr "" +msgid "Last Event" +msgstr "" + msgid "Last Ping" msgstr "" @@ -8906,7 +8925,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 07293544df..c2e5aa1a5c 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -298,6 +298,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "Aprovar Hosts selecionados" + msgid "(deleted user)" msgstr "" @@ -1052,6 +1056,10 @@ msgstr "avançado" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "Ativo" + #, fuzzy msgid "Agent Approval Success" msgstr "host criado" @@ -2732,6 +2740,10 @@ msgstr "" msgid "Destroy failed" msgstr "Destrua falhou: %s" +#, fuzzy +msgid "Detail" +msgstr "Detalhe Snapin Retorno" + #, fuzzy msgid "Details" msgstr "Detalhe Snapin Retorno" @@ -4137,6 +4149,9 @@ msgstr "Impressora já existe" msgid "Host %1$s finished deploying image %2$s." msgstr "Impressora já existe" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "host criado" @@ -5486,6 +5501,10 @@ msgstr "Língua" msgid "Largest images" msgstr "imagens" +#, fuzzy +msgid "Last Activity" +msgstr "Ativo" + msgid "Last Agent Check-In" msgstr "" @@ -5502,6 +5521,10 @@ msgstr "" msgid "Last Deployed" msgstr "Última Implantado" +#, fuzzy +msgid "Last Event" +msgstr "host criado" + msgid "Last Ping" msgstr "" @@ -10385,7 +10408,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 1defcdba4c..49c27297ea 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -298,6 +298,10 @@ msgstr "" msgid "(all)" msgstr "" +#, fuzzy, php-format +msgid "(deleted host %d)" +msgstr "批准选定主机" + msgid "(deleted user)" msgstr "" @@ -1052,6 +1056,10 @@ msgstr "高级" msgid "Agent" msgstr "" +#, fuzzy +msgid "Agent Activity" +msgstr "活性" + #, fuzzy msgid "Agent Approval Success" msgstr "主机创建" @@ -2732,6 +2740,10 @@ msgstr "" msgid "Destroy failed" msgstr "摧毁失败: %s" +#, fuzzy +msgid "Detail" +msgstr "管理单元返回详细" + #, fuzzy msgid "Details" msgstr "管理单元返回详细" @@ -4137,6 +4149,9 @@ msgstr "打印机已经存在" msgid "Host %1$s finished deploying image %2$s." msgstr "打印机已经存在" +msgid "Host Agent Activity" +msgstr "" + #, fuzzy msgid "Host Approval Success" msgstr "主机创建" @@ -5486,6 +5501,10 @@ msgstr "语言" msgid "Largest images" msgstr "图片" +#, fuzzy +msgid "Last Activity" +msgstr "活性" + msgid "Last Agent Check-In" msgstr "" @@ -5502,6 +5521,10 @@ msgstr "" msgid "Last Deployed" msgstr "最后部署" +#, fuzzy +msgid "Last Event" +msgstr "主机创建" + msgid "Last Ping" msgstr "" @@ -10385,7 +10408,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 5ffd73d8d1..5845a8bf8f 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -665,6 +665,17 @@ public static function coreRegistry() // narrowly: an audit row necessarily discloses attempted // usernames. 'audit' => ['view', 'manage'], + // The agent's half of that trail, read by host. Its OWN node + // rather than an alias onto `audit`: ADR 0021 made audit.view + // narrow because the audit log discloses attempted usernames and + // refusals, and agent rows are enrollments, inventories and task + // results a machine reported -- a different disclosure with a + // different audience. Aliasing would force anyone who may see + // what an agent did to also see every failed sign-in. + // + // `view` only. Nothing here writes: auditlog has no create, + // update or delete route anywhere in FOG (ADR 0021 Decision 8). + 'agentactivity' => ['view'], // The log viewer. Third sibling under Logging, and the one that // shipped without an entry here when it moved to its own node // (#1507) -- so it fell into "a node absent from the registry is diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index a92b4f6d2a..16afb38253 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -647,6 +647,16 @@ public static function buildMainMenuItems(&$main = '', &$hookMain = '') _('Log Viewer'), 'fas fa-file-lines' ], + // Fourth under Logging. The audit log above holds these rows + // too, but only as one flat install-wide grid -- this reads them + // by host, which is the question anyone actually has about an + // agent. Its own gate for the reason its coreRegistry() entry + // gives: what a machine reported and who failed to sign in are + // different disclosures. + 'agentactivity' => [ + _('Agent Activity'), + 'fas fa-satellite-dish' + ], 'service' => [ self::$foglang['ClientSettings'], 'fas fa-gears' @@ -851,7 +861,7 @@ private static function _menuGroups() 'logging' => [ 'title' => _('Logging'), 'icon' => 'fas fa-scroll', - 'children' => ['activity', 'audit', 'logviewer'], + 'children' => ['activity', 'audit', 'logviewer', 'agentactivity'], ], ]; } diff --git a/packages/web/src/Pages/AgentActivityManagement.php b/packages/web/src/Pages/AgentActivityManagement.php new file mode 100644 index 0000000000..be5e460891 --- /dev/null +++ b/packages/web/src/Pages/AgentActivityManagement.php @@ -0,0 +1,267 @@ + + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +namespace FOG\Pages; + +use FOG\Base\FOGPage; +use FOG\Router\HTTPResponseCodes; +use FOG\Router\Route; + +/** + * The agent's side of the audit trail, one row per host. + * + * The rows are already in `auditLog`, written by the classes under + * src/Agent and tagged subjectType `host` with the host's id. What was + * missing was any way to read them by host: AuditManagement lists the whole + * install in one flat grid with no host filter, so "what has this machine's + * agent been doing" meant scrolling past every other machine. + * + * SUMMARY FIRST, ROWS ON DEMAND. This page lists one row per host -- + * hostname, how many agent events it has, when the last one was and what it + * was -- and fetches a host's actual rows only when it is expanded. + * + * That shape rather than a flat grid with group headers, for three reasons. + * DataTables' rowGroup would group only within the current PAGE under + * `serverSide`, so one hostname would head a dozen separate pages. Any table + * using rowGroup is auto-paged out of the infinite scroll by registerTable() + * -- Scroller's virtual row-height math cannot reconcile injected header + * rows -- and fog.audit.list.js already records that as the reason the audit + * grid does not group. And an agent writes an audit row per changed fact per + * host, so a flat list is the one thing that grows without bound while the + * summary is bounded by the size of the fleet. + * + * The expand fetches through the SAME endpoint the host page's Agent + * Activity tab uses, so there is one query behind both surfaces. + * + * READ ONLY, like the audit log it reads. `auditlog` has no create, update + * or delete route anywhere in FOG (ADR 0021 Decision 8), so there is nothing + * here to wire a row action to. + * + * @category AgentActivity + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ +class AgentActivityManagement extends FOGPage +{ + /** + * The node this page answers for. + * + * Its own node rather than an alias onto `audit`. ADR 0021 made + * audit.view narrow because the audit log discloses attempted usernames + * and refusals; agent rows are enrollments, inventories and task results + * reported by a machine, which is a different disclosure and a different + * audience. Aliasing would have forced anyone who may see what an agent + * did to also see every failed sign-in in the install. + * + * @var string + */ + public $node = 'agentactivity'; + /** + * The prefix every type this page shows begins with. + * + * A LIKE rather than a list of the eleven current type names: a new + * fact kind is a registry entry and a block in the poll (the route + * rule), and it must not also be a third place to remember to edit + * before its rows become visible. + * + * @var string + */ + const TYPE_PREFIX = 'agent.'; + /** + * How many hosts the summary will list. + * + * The summary is bounded by the fleet, not by the log, so this is high + * enough that no real install reaches it and low enough that a runaway + * cannot render a million rows into a browser. + * + * @var int + */ + const MAX_HOSTS = 5000; + /** + * Initializes the page. + * + * @param string $name the name to construct with. + */ + public function __construct($name = '') + { + $this->name = _('Agent Activity'); + parent::__construct($this->name); + } + /** + * Presents the per-host summary. + * + * Variadic to match FOGPage::index(...$args) -- PHP rejects the + * declaration outright otherwise, so the class does not load at all. + * + * @param mixed ...$args unused, present for signature compatibility + * + * @return void + */ + public function index(...$args) + { + $this->title = _('Agent Activity'); + + // The first column is the expand control and carries no heading of + // its own: a header on it would be a label for a button. + $this->headerData = [ + '', + _('Host'), + _('Events'), + _('Last Activity'), + _('Last Event') + ]; + $this->attributes = [ + ['class' => 'agentactivity-toggle'], + [], + [], + [], + [] + ]; + + echo '
    '; + echo '
    '; + echo '

    '; + echo _('Agent Activity'); + echo '

    '; + echo '
    '; + echo '
    '; + + $this->render(12, 'agentactivity-table'); + + echo '
    '; + echo '
    '; + } + /** + * Serves the summary: one row per host that has agent activity. + * + * Direct SQL rather than a manager read, for the reason getChanges() + * gives in AuditManagement: this is an aggregate, and Route::listem() + * has no GROUP BY. It would also materialize an object per audit row in + * the install to hand back four numbers per host. + * + * The join to `hosts` is LEFT: a host deleted after its agent reported + * still has rows, and dropping them would make the counts here disagree + * with the audit log itself. Those rows show as the host id with no name + * rather than vanishing -- on the lab install most of them are that. + * + * @return void + */ + public function getList() + { + header('Content-type: application/json'); + + // LIMIT is a literal because it is a class constant, never request + // input. The one value that varies is bound. + // + // The derived table groups once, and the outer join back to + // auditLog on MAX(alID) is what names the LAST event type. The + // obvious alternative -- GROUP_CONCAT(alType ORDER BY alID DESC) + // with SUBSTRING_INDEX -- builds a string of every row's type for + // every host just to read the first one, and silently truncates at + // group_concat_max_len on any host with a long history. + $rows = self::$DB->query( + 'SELECT a.`alSubjectID` AS `hostID`, h.`hostName` AS `hostName`, ' + . 's.`events` AS `events`, s.`lastTime` AS `lastTime`, ' + . 'a.`alType` AS `lastType` ' + . 'FROM `auditLog` a ' + . 'INNER JOIN (' + . 'SELECT `alSubjectID`, COUNT(*) AS `events`, ' + . 'MAX(`alID`) AS `maxID`, MAX(`alCreatedTime`) AS `lastTime` ' + . 'FROM `auditLog` ' + . 'WHERE `alType` LIKE :prefix AND `alSubjectType` = \'host\' ' + . 'GROUP BY `alSubjectID`' + . ') s ON s.`alSubjectID` = a.`alSubjectID` ' + . 'AND s.`maxID` = a.`alID` ' + // LEFT, not INNER: a host deleted after its agent reported still + // has rows, and dropping them would make these counts disagree + // with the audit log itself. Measured on the lab install, where + // most rows belong to hosts that no longer exist. + . 'LEFT JOIN `hosts` h ON h.`hostID` = a.`alSubjectID` ' + . 'ORDER BY s.`lastTime` DESC ' + . 'LIMIT ' . self::MAX_HOSTS, + [], + [':prefix' => self::TYPE_PREFIX . '%'] + )->fetch(\PDO::FETCH_ASSOC, 'fetch_all')->get(); + + $out = []; + foreach ((array) $rows as $row) { + $id = (int) ($row['hostID'] ?? 0); + $out[] = [ + 'hostID' => $id, + // A host removed after its agent reported leaves rows with + // no name to join to. Saying so beats an empty cell that + // reads as a rendering fault. + 'hostName' => '' === (string) ($row['hostName'] ?? '') + ? sprintf(_('(deleted host %d)'), $id) + : (string) $row['hostName'], + 'events' => (int) ($row['events'] ?? 0), + // Displayed in the VIEWER's zone like every other date on a + // page, not the storage zone it was written in. + 'lastTime' => '' === (string) ($row['lastTime'] ?? '') + ? '' + : self::toDisplayStored( + (string) $row['lastTime'] + )->format('Y-m-d H:i:s'), + 'lastType' => (string) ($row['lastType'] ?? '') + ]; + } + + http_response_code(HTTPResponseCodes::HTTP_SUCCESS); + echo json_encode(['data' => $out]); + exit; + } + /** + * Serves one host's agent rows, for an expanded summary row. + * + * The same read the host page's Agent Activity tab performs, so the two + * surfaces cannot drift into showing different histories for one host. + * Route::listem() puts DataTables' start/length through + * FOGManagerController::limit(), so an expanded host with thousands of + * rows still pages. + * + * Ordered by id rather than listem()'s default of `name`: an audit row + * has no name, and id orders the same way createdTime does without ties + * between rows written in the same second. + * + * @return void + */ + public function getHostActivity() + { + header('Content-type: application/json'); + $hostID = (int) Route::queryParam('id'); + if ($hostID < 1) { + // An absent or malformed id must not fall through to "every + // host": this endpoint exists to scope, and an unscoped answer + // is the one thing it must never give. + http_response_code(HTTPResponseCodes::HTTP_SUCCESS); + echo json_encode(['data' => []]); + exit; + } + Route::listem( + 'auditlog', + [ + 'type' => self::TYPE_PREFIX . '%', + 'subjectType' => 'host', + 'subjectID' => $hostID + ], + false, + 'AND', + 'id' + ); + http_response_code(HTTPResponseCodes::HTTP_SUCCESS); + echo Route::getData(); + exit; + } +} diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index ac4c72f507..7d7b39c6cb 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -4358,6 +4358,25 @@ public function hostImageHistory() * * @return void */ + public function hostAgentActivity() + { + $this->renderHistoryTab( + [ + _('When'), + _('Event'), + _('Detail'), + _('Outcome') + ], + [[], [], [], []], + _('Host Agent Activity'), + 'host-agent-activity-table' + ); + } + /** + * Renders the host's snapin history tab. + * + * @return void + */ public function hostSnapinHistory() { $this->renderHistoryTab( @@ -4707,6 +4726,18 @@ public function edit() $this->hostInstalledSoftware(); } ], + [ + // The agent's own trail for THIS host. The rows have + // always been in auditLog tagged with the host id; + // nothing could read them by host, so answering + // "what has this machine's agent been doing" meant + // scrolling the whole install's audit grid. + 'name' => _('Agent Activity'), + 'id' => 'host-agent-activity', + 'generator' => function () { + $this->hostAgentActivity(); + } + ], ]) ] ]; @@ -7517,6 +7548,34 @@ public function getImageHist() * * @return void */ + public function getAgentActivity() + { + header('Content-type: application/json'); + // The same read AgentActivityManagement::getHostActivity() performs, + // so the host tab and the Logging page cannot drift into showing + // different histories for one machine. A LIKE on the prefix rather + // than a list of type names: a new fact kind is a registry entry and + // a block in the poll, not a third place to remember to edit. + Route::listem( + 'auditlog', + [ + 'type' => AgentActivityManagement::TYPE_PREFIX . '%', + 'subjectType' => 'host', + 'subjectID' => (int)$this->obj->get('id') + ], + false, + 'AND', + 'id' + ); + http_response_code(HTTPResponseCodes::HTTP_SUCCESS); + echo Route::getData(); + exit; + } + /** + * Serves the host's snapin history rows. + * + * @return void + */ public function getSnapinHist() { $this->renderSnapinHistoryData($this->obj->get('id')); diff --git a/tests/agent-activity-page.test.php b/tests/agent-activity-page.test.php new file mode 100644 index 0000000000..d3931615a1 --- /dev/null +++ b/tests/agent-activity-page.test.php @@ -0,0 +1,212 @@ +check( + 'the page class exists and declares its own node', + class_exists('FOG\Pages\AgentActivityManagement') + && 'agentactivity' === (new \FOG\Pages\AgentActivityManagement())->node +); + +$registry = \FOG\Auth\Authorization::coreRegistry(); +$t->check( + 'agentactivity is a permission node of its own, not an alias onto audit', + isset($registry['agentactivity']) + && in_array('view', (array)$registry['agentactivity'], true) +); + +// Read-only by construction: auditlog has no create, update or delete route +// anywhere in FOG (ADR 0021 Decision 8), so granting anything else here +// would name an action nothing can perform. +$t->check( + 'agentactivity grants view and nothing else', + isset($registry['agentactivity']) + && ['view'] === array_values((array)$registry['agentactivity']) +); + +$aliases = (new \ReflectionClass('FOG\Auth\Authorization')) + ->getConstant('NODE_ALIASES'); +$t->check( + 'agentactivity is not aliased onto another node\'s gate', + !isset($aliases['agentactivity']) +); + +$fogPageSrc = (string)file_get_contents( + __DIR__ . '/../packages/web/src/Base/FOGPage.php' +); +$t->check( + 'the node has a sidebar entry', + 1 === preg_match( + "#'agentactivity'\s*=>\s*\[\s*_\('Agent Activity'\)#", + $fogPageSrc + ) +); +$t->check( + 'the sidebar entry sits under the Logging group', + 1 === preg_match( + "#'logging'\s*=>\s*\[.*?'children'\s*=>\s*\[[^\]]*'agentactivity'#s", + $fogPageSrc + ) +); + +// The JS is loaded by convention, js/fog//fog..list.js +// (FOGPageManager::render()), so the filename IS the wiring. +$t->check( + 'the grid script is where the node convention will look for it', + is_file($jsFile) +); + +// ------------------------------------------------- one filter, two places + +$t->check( + 'the page filters on the shared prefix constant', + false !== strpos($pageSrc, 'self::TYPE_PREFIX') +); +$t->check( + 'the host tab filters on the SAME constant, not its own copy', + false !== strpos($hostSrc, 'AgentActivityManagement::TYPE_PREFIX') +); + +// The drift this prevents: eleven agent.* types exist today and a twelfth +// is one registry entry away. A surface that listed them would show a host +// an incomplete history and give no sign it was doing so. +$knownTypes = [ + 'agent.enroll', 'agent.token', 'agent.result', 'agent.inventory', + 'agent.software', 'agent.printers', 'agent.directory', + 'agent.directory.move', 'agent.secureboot', 'agent.usersession', + 'agent.wake' +]; +$listed = 0; +foreach ($knownTypes as $type) { + if (false !== strpos($pageSrc, "'" . $type . "'")) { + $listed++; + } +} +$t->check( + 'the page names no individual agent.* type, so a new kind needs no edit', + 0 === $listed +); + +// Both surfaces must ask auditLog for host-subject rows. A filter that +// dropped subjectType would collide with any other model numbered the same. +foreach ([['the page', $pageSrc], ['the host tab', $hostSrc]] as $pair) { + list($who, $src) = $pair; + $t->check( + $who . ' scopes to host subjects', + 1 === preg_match("#'subjectType'\s*=>\s*'host'#", $src) + ); +} + +// ------------------------------------------------ the scope cannot be lost + +// An unscoped answer is the one thing the scoped endpoint must never give, +// and the guard is what stops a missing id becoming "every host". +$t->check( + 'the scoped endpoint refuses an id below 1 before it reads anything', + 1 === preg_match( + '#\$hostID\s*=\s*\(int\)\s*Route::queryParam\(\'id\'\);\s*' + . 'if\s*\(\$hostID\s*<\s*1\)#s', + $pageSrc + ) +); +$t->check( + 'the id is cast to int, so no request string reaches the query', + 1 === preg_match("#\(int\)\s*Route::queryParam\('id'\)#", $pageSrc) +); +$t->check( + 'the host tab takes its id from the loaded host, never the query string', + 1 === preg_match( + "#'subjectID'\s*=>\s*\(int\)\\\$this->obj->get\('id'\)#", + $hostSrc + ) +); + +// ------------------------------------------------------ the grid contract + +$t->check( + 'the grid does not use rowGroup, which would auto-page it out of the ' + . 'infinite scroll and group only within one page', + false === strpos($jsSrc, 'rowGroup:') +); + +// headerData and attributes are positional; a mismatch silently shifts +// every column's attributes one to the left. +$page = new \FOG\Pages\AgentActivityManagement(); +ob_start(); +$page->index(); +$html = (string)ob_get_clean(); +$ref = new \ReflectionObject($page); +$headerProp = $ref->getProperty('headerData'); +$headerProp->setAccessible(true); +$attrProp = $ref->getProperty('attributes'); +$attrProp->setAccessible(true); +$headers = (array)$headerProp->getValue($page); +$attrs = (array)$attrProp->getValue($page); + +$t->check( + 'the header and attribute arrays are the same length', + count($headers) === count($attrs) +); + +// The JS column list must match the rendered header count or DataTables +// throws and the grid never draws. +$cols = 0; +if (preg_match('#columns:\s*\[(.*?)\n \],#s', $jsSrc, $m)) { + $cols = preg_match_all('#^\s{6}[a-zA-Z{]#m', $m[1]); +} +$t->check( + 'the grid declares one column per header (' . count($headers) . ')', + $cols === count($headers) +); + +$t->check( + 'the page renders its table container', + false !== strpos($html, 'agentactivity-table') +); + +$t->finish(); From 07972265542911df738d1b820a047ceb228a6973 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 11:49:51 +0000 Subject: [PATCH 093/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index a58a0372a4..4b47e8f510 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10412,6 +10412,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 6d8c36379f..394a393447 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10421,6 +10421,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 45ee0bcab3..1caee998b9 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10579,6 +10579,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 2a7f0c59a7..03abe4584f 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10413,6 +10413,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 9fa86120c3..34d4e9fc2b 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10405,6 +10405,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 09f254b94c..5f5ba1a7fd 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10128,6 +10128,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 5f0e7959ea..9ee1c1f9d7 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10085,6 +10085,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e55df94790..370ce07fc0 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8925,6 +8925,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index c2e5aa1a5c..6f8a33f5b2 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10408,6 +10408,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 49c27297ea..ee144b805d 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10408,6 +10408,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 81618a821015ddec75c85600f64811d1b44684ad Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 07:43:58 -0500 Subject: [PATCH 094/117] Info card: one-click tasking, and no more "Primary Group" Two changes to the same strip at the top of the host and group edit pages. REMOVED: the host card's "Primary Group". It meant minId($this->obj->get('groups')) -- the lowest-id group the host happened to be in. There is no primary group. A group GRANTS, and what a host ends up with is resolved from every group it belongs to at task time, ordered by groupOrder (ADR 0038, FOG\Assign\Resolver). So the label named a rank that does not exist and the value picked one membership arbitrarily. Nothing replaces it. Listing every group instead was considered and dropped: a host in eight groups blows the card out, which makes the card worse rather than better, and the Group Associations tab already shows them properly. ADDED: Deploy and Capture on the host card, Deploy and Multi-Cast on the group card. One click, from whichever tab is open, so an admin already looking at a host does not have to go back to the grid and find the row again to do the obvious thing to it. This is not a new pattern. The host LIST has carried the same three buttons since _quickTaskItems(), for the same reason: these are the task types that need no options, which is the whole reason they can be one click. The pairing is the one that method already documents -- Deploy and Capture are what a single host wants, Deploy and Multi-Cast what a set of them wants. On a group it is also what the server will accept: GroupManagement::deployPost() throws "Groups cannot create capture tasks" outright, so a Capture button there could only produce a toast saying no. Every button confirms first, and this is the one place it deliberately differs from the list. There you tick a row to get the buttons; here you arrive on the page just by clicking a host name, so one stray click would deploy over a running machine -- or, from a group, over all of them. The text is built server side because it is translated, and it names the target: the host, or the group AND its member count, which is the fact that decides whether you meant to press it and the one thing the button itself cannot show. Mechanically: - FOGPage::$noteActions, a pre-rendered string, echoed by renderInfoCard() in a right-aligned column. ms-auto, not a float: the row is display:flex and a float would do nothing there. - It rides the existing EDIT_INFO_DATA hook alongside notes/noteSources, so a plugin adding a button does it the same way it already adds a line. A second event would mean two registrations for one card. - FOGPageRender::renderQuickTaskActions() builds them, gated on {node}.task -- the action ?node=X&sub=deploy resolves to through Authorization::_subToAction(), so the gate here and the gate the POST hits are the same string by construction. - The script posts straight to ?node={node}&sub=deploy with scheduleType=instant, skipping the options form. Nothing on it these types need, and everything its POST is checked for -- pending host, assigned and enabled image, protected image on a capture, one image across a multicast -- is checked in deployPost(), not in the form. scheduleType is the one field that must be sent: validateScheduleType() throws on an absent value rather than defaulting. - Suppressed where the server would refuse anyway: a pending host, an empty group. Neutral outline buttons rather than a type color. Nothing here is the card's commit action -- the General tab's Update is -- and these are shortcuts in a header strip, not a decision cluster in a form footer. The weight a red button would carry is carried by the confirmation instead. tests/info-card-quick-tasks.test.php pins the four things that fail silently: the permission gate per node (host.task must not unlock the group card), the confirmation being present AND naming the target, the values being read through get() rather than as properties, and the pair each page asks for. Each was verified by reintroducing the defect and watching it go red. FOG_BCACHE_VER 360 -> 361 for the changed script. Co-Authored-By: Claude --- packages/web/management/js/fog/fog.common.js | 68 ++++ packages/web/src/Base/FOGPage.php | 12 + packages/web/src/Base/FOGPageRender.php | 109 +++++- packages/web/src/Base/System.php | 2 +- packages/web/src/Pages/GroupManagement.php | 28 +- packages/web/src/Pages/HostManagement.php | 32 +- tests/info-card-quick-tasks.test.php | 344 +++++++++++++++++++ 7 files changed, 584 insertions(+), 11 deletions(-) create mode 100644 tests/info-card-quick-tasks.test.php diff --git a/packages/web/management/js/fog/fog.common.js b/packages/web/management/js/fog/fog.common.js index 3f985d604a..ba001cbbc7 100644 --- a/packages/web/management/js/fog/fog.common.js +++ b/packages/web/management/js/fog/fog.common.js @@ -5316,6 +5316,7 @@ function reinitialize() { setupPasswordReveal(); setupUniversalSearch(); setupInfoCard(); + setupInfoCardActions(); }; /** @@ -5383,6 +5384,73 @@ function setupInfoCard() { }); } + +/** + * The info card's one-click task buttons. + * + * Straight to the create endpoint. The options form at + * ?node={node}&sub=deploy is skipped deliberately -- Deploy, Capture and + * Multi-Cast need nothing off it, and fetching it only to post it back + * unchanged is the click this exists to remove. Everything that form's POST + * is checked for -- a pending host, an assigned and enabled image, a + * protected image on a capture, one image across a multicast -- is checked + * in deployPost(), not in the form, so nothing is skipped but the rendering. + * + * scheduleType is the one field that must be sent: validateScheduleType() + * throws on an absent value rather than defaulting, so an empty body would + * come back "Invalid scheduling type". + * + * Every button confirms first. The list grid's equivalents do not, but there + * you have ticked a row to get them; here you arrive on this page just by + * clicking a host name, and one stray click would deploy over a running + * machine -- or, from a group, over all of them. The text is built server + * side (FOGPageRender::renderQuickTaskActions) because it is translated. + */ +function setupInfoCardActions() { + // Guards the window between the click and the server's answer. Per button + // rather than one flag for the card: Deploy and Capture are different + // taskings and there is no reason firing one should block the other. The + // button stays enabled underneath so nothing has to re-enable it on a + // refusal; this is only about the impatient double-click, which would + // otherwise be two identical taskings. + var running = {}; + + // Delegated and namespaced: the card is torn down and rebuilt with the + // page on every AJAX nav, so a direct binding would be lost on the first + // one and doPageLoad() would stack a new one per visit. + $(document) + .off('click.fogQuickTask') + .on('click.fogQuickTask', '.fog-quicktask', function(e) { + e.preventDefault(); + var btn = $(this), + node = btn.data('node'), + id = btn.data('id'), + type = btn.data('type'), + key = node + ':' + id + ':' + type; + if (running[key]) { + return; + } + if (!window.confirm(btn.data('confirm'))) { + return; + } + running[key] = true; + $.apiCall( + 'post', + '../management/index.php?node=' + encodeURIComponent(node) + + '&sub=deploy&id=' + encodeURIComponent(id) + + '&type=' + encodeURIComponent(type), + {scheduleType: 'instant'}, + function() { + // Both outcomes land here and both only need the lock released. + // apiCall has already drawn the toast, and there is nothing on + // this page to repaint: the card's Last Deployed is the date the + // last task FINISHED, which a task just queued has not. + running[key] = false; + } + ); + }); +} + // Select2 builds its search inputs with neither id/name nor a label, tripping // the browser's "form field should have an id or name" autofill advisory and the // "no label associated with a form field" accessibility advisory. There are two diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index a92b4f6d2a..43315db48b 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -99,6 +99,18 @@ abstract class FOGPage extends FOGBase * @var array */ public $noteSources = []; + /** + * Pre-rendered action buttons for the info card, or ''. + * + * Sits to the right of the notes in the same card. Built by a page's + * edit() -- renderQuickTaskActions() is the only builder today -- and + * echoed by renderInfoCard(). A string rather than a spec array because + * the only thing the renderer does with it is echo it, and the pages + * that set it are already building markup with makeButton(). + * + * @var string + */ + public $noteActions = ''; /** * Table header data * diff --git a/packages/web/src/Base/FOGPageRender.php b/packages/web/src/Base/FOGPageRender.php index bde8ae1056..a8002ff601 100644 --- a/packages/web/src/Base/FOGPageRender.php +++ b/packages/web/src/Base/FOGPageRender.php @@ -546,23 +546,120 @@ protected static function noteSourceAttrs($source) return $attrs; } + /** + * The info card's one-click task buttons. + * + * The host and group LIST grids have carried these since + * HostManagement::_quickTaskItems(): the two or three task types that + * take no options, fired straight at the create endpoint instead of + * fetching an options form only to post it back untouched. This is the + * same affordance on the edit pages, so an admin already looking at a + * host or a group does not have to go back to the grid, find the row + * again and tick it to do the obvious thing to it. + * + * Deploy and Capture for a host, Deploy and Multi-Cast for a group -- + * the same pairing _quickTaskItems() documents, and for the same + * reason: those are the types that need no options, which is the whole + * reason they can be one click. + * + * Neutral outline buttons rather than a type color. Nothing here is the + * card's commit action -- the General tab's Update is -- and these are + * shortcuts sitting in a header strip, not a decision cluster in a form + * footer. The weight that a red button would carry is carried instead + * by the confirmation, which names the target and cannot be skipped. + * + * @param string $node Page node, e.g. 'host' or 'group'. Also decides + * the permission: ?node=X&sub=deploy resolves to + * X.task via Authorization::_subToAction(), so + * the gate here and the gate the POST hits are + * the same string by construction. + * @param int $id The entity being edited. + * @param array $typeIds TaskType ids, in the order they should appear. + * @param string $target Already-translated description of what the task + * lands on, e.g. 'host "foo"'. Interpolated into + * the confirmation. Built by the caller because + * only the caller knows whether it is one machine + * or a group of them. + * + * @return string The button group markup, or '' if none may be shown. + */ + public static function renderQuickTaskActions( + $node, + $id, + array $typeIds, + $target + ) { + // Same refusal _quickTaskItems() makes: a user without the + // permission would be shown buttons whose POST can only be denied. + if (!Authorization::can($node . '.task')) { + return ''; + } + + $buttons = ''; + foreach ($typeIds as $typeId) { + $TaskType = new TaskType($typeId); + // A server whose taskTypes row was deleted simply loses that + // button, the same way the accordion and the grid lose theirs. + if (!$TaskType->isValid()) { + continue; + } + $name = (string)$TaskType->get('name'); + // Built here, not in the script. gettext runs server side, so a + // sentence assembled in JS would never reach the .pot -- the + // same reason noteSourceAttrs() builds its on/off labels here. + $confirm = sprintf( + _('Create a %1$s task for %2$s?'), + $name, + $target + ); + $buttons .= self::makeButton( + 'quicktask-' . \Initiator::e($node) . '-' . (int)$TaskType->get('id'), + ' ' . \Initiator::e($name), + 'btn btn-outline-secondary fog-quicktask', + 'type="button"' + . ' data-node="' . \Initiator::e($node) . '"' + . ' data-id="' . (int)$id . '"' + . ' data-type="' . (int)$TaskType->get('id') . '"' + . ' data-confirm="' . \Initiator::e($confirm) . '"' + ); + } + if ('' === $buttons) { + return ''; + } + + return '
    ' . $buttons . '
    '; + } + protected function renderInfoCard() { $notes = (array)$this->notes; $sources = (array)$this->noteSources; + $actions = (string)$this->noteActions; // Mirrors PLUGINS_INJECT_TABDATA in tabFields(): a plugin that adds // a tab to a core page can add its line here too. 1.5's equivalent // rode SUB_MENULINK_DATA, which 1.6 repurposed for the sidebar node // menu, so there is no back-compat name to keep. + // + // 'actions' rides the same event rather than getting one of its own: + // a plugin adding a button to this card is doing the same thing as a + // plugin adding a line to it, and a second event would mean two + // registrations for one card. self::$HookManager->processEvent( 'EDIT_INFO_DATA', [ 'notes' => &$notes, 'noteSources' => &$sources, + 'noteActions' => &$actions, 'obj' => &$this->obj ] ); - if (!count($notes)) { + // Either half is enough to be worth drawing. A page with buttons and + // no notes is unusual but not wrong, and returning early on the note + // count alone would silently drop the buttons. + if (!count($notes) && '' === trim($actions)) { return; } echo '
    '; @@ -593,6 +690,16 @@ protected function renderInfoCard() echo '
    '; echo ''; } + // Last in the row and pushed to the far edge with ms-auto, so the + // buttons sit clear of the notes however many notes there are. A + // flex auto margin, not a float: the row is display:flex and a float + // would do nothing here. + if ('' !== trim($actions)) { + echo '
    '; + echo $actions; + echo '
    '; + } echo ''; // Once, and only when something above actually carries the marker. // A standing disclaimer on every edit page would be noise on the diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 799288156b..d055b817ef 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -131,7 +131,7 @@ public function __construct() // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. define('FOG_SCHEMA', 430); - define('FOG_BCACHE_VER', 360); + define('FOG_BCACHE_VER', 361); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as // a release asset. Pinned here rather than tracked as "latest" so a diff --git a/packages/web/src/Pages/GroupManagement.php b/packages/web/src/Pages/GroupManagement.php index ceef9e3550..0219d9d84e 100644 --- a/packages/web/src/Pages/GroupManagement.php +++ b/packages/web/src/Pages/GroupManagement.php @@ -1757,9 +1757,12 @@ public function groupSnapinHistory() */ public function edit() { + // Read once: the note below and the quick-task confirmation both + // want it, and getHostCount() is a query. + $hostCount = (int)$this->obj->getHostCount(); $this->notes = [ _('Group') => $this->obj->get('name'), - _('Members') => (string)$this->obj->getHostCount() + _('Members') => (string)$hostCount ]; // Info-card notes that mirror a General-tab control, so the card // tracks the form instead of going stale until the next page @@ -1769,6 +1772,29 @@ public function edit() $this->noteSources = [ _('Group') => '#group' ]; + // Deploy and Multi-Cast, one click, from whichever tab is open. + // Multi-Cast rather than Capture: capturing is a thing you do to + // one machine, and a group is by definition more than one. Not a + // style call -- deployPost() below throws "Groups cannot create + // capture tasks" outright, so a Capture button here could only ever + // produce a toast saying no. The list grid draws the same line on + // ttIsAccess ('host' vs 'group'). + // + // The member count goes in the confirmation, not just the name. A + // group's size is the fact that decides whether you meant to press + // this, and it is the one thing the button itself cannot show. + if ($hostCount > 0) { + $this->noteActions = self::renderQuickTaskActions( + 'group', + (int)$this->obj->get('id'), + [TaskType::DEPLOY, TaskType::MULTICAST], + sprintf( + _('all %1$d hosts in group "%2$s"'), + $hostCount, + $this->obj->get('name') + ) + ); + } $tabData = []; // General diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index ac4c72f507..78a4aced62 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -4473,9 +4473,18 @@ public function hostInstalledSoftware() public function edit() { // Identity plus the facts you cannot see from the other twenty - // tabs: which image is assigned, when it was last imaged, and which - // group it belongs to. - $primaryGroup = new Group(self::minId($this->obj->get('groups'))); + // tabs: which image is assigned and when it was last imaged. + // + // No group line. It used to say "Primary Group", meaning + // minId($this->obj->get('groups')) -- the lowest-id group the host + // happened to be in. There is no primary group: a group GRANTS, and + // what a host ends up with is resolved from every group it belongs + // to at task time, ordered by groupOrder (ADR 0038, and see + // FOG\Assign\Resolver). So the label named a rank that does not + // exist and the value picked one membership arbitrarily. Listing + // them all instead was considered and dropped: a host in eight + // groups blows the card out, and the Group Associations tab already + // shows them properly. // Whichever of the two clients last spoke, named so the card is not // ambiguous about which one it is reporting. // @@ -4518,11 +4527,6 @@ public function edit() $this->obj->get('deployed'), 'hosts', 'hostLastDeploy' - ), - _('Primary Group') => ( - $primaryGroup->isValid() ? - $primaryGroup->get('name') : - _('None') ) ]; // Info-card notes that mirror a General-tab control, so the card @@ -4534,6 +4538,18 @@ public function edit() _('Host') => '#host', _('Assigned Image') => '#image' ]; + // Deploy and Capture, one click, from whichever tab is open. Gated + // on pending for the same reason the Tasks tab below is: a pending + // host cannot be tasked, and deployPost() refuses it anyway, so + // offering the button only produces a toast saying no. + if (!$this->obj->get('pending')) { + $this->noteActions = self::renderQuickTaskActions( + 'host', + (int)$this->obj->get('id'), + [TaskType::DEPLOY, TaskType::CAPTURE], + sprintf(_('host "%s"'), $this->obj->get('name')) + ); + } $tabData = []; // General diff --git a/tests/info-card-quick-tasks.test.php b/tests/info-card-quick-tasks.test.php new file mode 100644 index 0000000000..52425ab5db --- /dev/null +++ b/tests/info-card-quick-tasks.test.php @@ -0,0 +1,344 @@ +name` is null and `(int)$TaskType->id` is 0 with + * no warning that survives the page's output buffer. Here it would draw a + * nameless button pointed at type 0. + * + * 4. WHICH PAIR EACH PAGE OFFERS. Deploy and Capture for a host; Deploy and + * Multi-Cast for a group. Capturing is something you do to one machine + * and a group is by definition more than one, so a group offering Capture + * is a real defect -- and one the server would happily act on, since + * GroupManagement::deployPost() does not check ttIsAccess. + * + * Usage: php tests/info-card-quick-tasks.test.php + * Exit status 0 = pass, 1 = fail. + * + * PHP version 7.4+ + * + * @category Tests + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +use FOG\Items\TaskType; +use FOG\Items\User; +use FOG\Pages\GroupManagement; +use FOG\Pages\HostManagement; + +require_once __DIR__ . '/lib/fog-test-harness.php'; + +FogTestHarness::boot('info-card-quick-tasks'); + +$db = FogTestHarness::fakeDb(); + +/** + * One taskTypes row. Every column, not just the three the builder reads -- + * FOGController::setQuery() walks $databaseFields and warns on each one it + * cannot find, which buries the test's own output. + * + * @param int $id ttID + * @param string $name ttName + * @param string $icon ttIcon + * + * @return array + */ +$row = static function ($id, $name, $icon) { + return [ + 'ttID' => $id, + 'ttName' => $name, + 'ttDescription' => $name, + 'ttIcon' => $icon, + 'ttKernel' => '', + 'ttKernelArgs' => '', + 'ttType' => '', + 'ttIsAdvanced' => 0, + 'ttIsAccess' => 'both', + 'ttInitrd' => '' + ]; +}; +$rows = [ + TaskType::DEPLOY => $row(TaskType::DEPLOY, 'Deploy', 'download'), + TaskType::CAPTURE => $row(TaskType::CAPTURE, 'Capture', 'upload'), + TaskType::MULTICAST => $row(TaskType::MULTICAST, 'Multi-Cast', 'share-alt') +]; +// A single flat row, not a list of rows: FOGController::load() reads +// fetch()->get() as ONE record, and handing it a nested array leaves the +// object invalid with nothing said. +$db->responder = static function ($sql, $params) use ($db, $rows) { + $db->error = false; + if (false === strpos($sql, 'taskTypes')) { + return null; + } + foreach ($params as $value) { + if (isset($rows[(int)$value])) { + return $rows[(int)$value]; + } + } + + return []; +}; + +$t = new FogChecks(); + +/** + * Runs the builder as a user holding the given permissions. + * + * @param array $perms the effective permission list for the acting user + * @param string $node 'host' or 'group' + * @param array $typeIds the task types that page asks for + * @param string $target the confirmation's description of the target + * + * @return string the emitted markup + */ +$emit = static function ( + array $perms, + $node, + array $typeIds, + $target = 'host "bench-01"' +) { + $user = (new User())->set('id', 1)->set('name', 'fog'); + foreach (['FOGBase', 'Authorization', 'Route'] as $cls) { + FogTestHarness::setStatic($cls, 'FOGUser', $user); + } + FogTestHarness::setStatic('Authorization', '_permCache', [1 => $perms]); + + return (string)HostManagement::renderQuickTaskActions( + $node, + 42, + $typeIds, + $target + ); +}; + +$hostTypes = [TaskType::DEPLOY, TaskType::CAPTURE]; +$groupTypes = [TaskType::DEPLOY, TaskType::MULTICAST]; + +// ------------------------------------------------------------------------- +// 1. The permission gate, per node. +// ------------------------------------------------------------------------- +$t->check( + 'a reader without host.task is offered no quick tasks on a host', + '' === $emit(['host.view', 'host.edit'], 'host', $hostTypes) +); +$t->check( + 'host.task alone is enough', + false !== strpos($emit(['host.task'], 'host', $hostTypes), 'fog-quicktask') +); +$t->check( + 'a reader without group.task is offered no quick tasks on a group', + '' === $emit(['group.view', 'group.edit'], 'group', $groupTypes) +); +$t->check( + 'host.task does NOT unlock the group card', + '' === $emit(['host.task'], 'group', $groupTypes) +); +$t->check( + 'group.task alone is enough', + false !== strpos( + $emit(['group.task'], 'group', $groupTypes), + 'fog-quicktask' + ) +); +$t->check( + 'and a wildcard unlocks both', + false !== strpos($emit(['*'], 'host', $hostTypes), 'fog-quicktask') + && false !== strpos($emit(['*'], 'group', $groupTypes), 'fog-quicktask') +); + +// ------------------------------------------------------------------------- +// 2. The confirmation. The reason a one-click deploy is safe to sit here. +// ------------------------------------------------------------------------- +/** + * Every button in a run of markup, as [type => [icon, name, confirm]]. + * + * @param string $markup the emitted button group + * + * @return array + */ +$parse = static function ($markup) { + preg_match_all( + '/'; + data: 'hostName', + visible: false, + className: 'noVis', + render: function(d, t, row) { + if (t === 'sort' || t === 'type') { + return row.groupSort; + } + return t === 'display' + ? $.escapeHtml(d === null ? '' : String(d)) + : d; } }; } + // Collapse hides a group's event rows and leaves its anchor. Registered + // once and scoped to this table by node identity -- ext.search is global, + // so an unscoped filter would silently apply to every grid on the page. + var tableNode = $table.get(0); + + $.fn.dataTable.ext.search.push(function(settings, data, dataIndex, row) { + if (settings.nTable !== tableNode) { + return true; + } + return row.anchor === true || expanded[row.hostName] === true; + }); + + // The group header. It carries what used to be four columns of a summary + // grid -- the host, its event count, and now the expand control -- which + // is what frees the columns below to be the events themselves. + function groupHeader(rows, name) { + var d = rows.data()[0] || {}, + open = expanded[name] === true, + count = parseInt(d.events, 10) || 0, + note = ''; + + if (truncated[name]) { + // Said on the header rather than in a row: it is a fact about the + // group, and a row saying it would sort and filter like an event. + note = ' ' + + $.escapeHtml('showing the newest ' + ROWS_PER_HOST) + ''; + } + + return $('') + .addClass('agentactivity-group') + .attr('data-host', name) + .append( + $('') + .attr('colspan', 5) + .html( + '' + + '' + $.escapeHtml(name) + '' + + ' ' + + $.escapeHtml(String(count)) + '' + + note + ) + ); + } + var table = $table.registerTable(null, { - // Newest activity first: the question this page answers is "what have - // the agents been doing", and a host silent for a month is not it. + // Newest activity first. Ordering by the time column also keeps each + // host's rows in the order its agent wrote them, since RowGroup orders + // groups by the first ordering column it is grouped on and rows within + // a group by whatever follows. + // Groups first, then time within a group. Both descending: the most + // recently active host heads the page, and its newest event heads it. order: [ - [3, 'desc'] + [4, 'desc'], + [0, 'desc'] ], columns: [ - toggleColumn(), - escaped('hostName'), - escaped('events'), - escaped('lastTime'), - escaped('lastType') + escaped('createdTime'), + escaped('type'), + escaped('text'), + outcomeColumn(), + groupSortColumn() ], - rowId: 'hostID', + rowGroup: { + dataSrc: 'hostName', + startRender: groupHeader + }, processing: true, - // Client side on purpose -- see the file docblock. + // Client side: the seed is one row per host, which is bounded by the + // fleet, and rowGroup cannot group a server-side grid beyond one page. serverSide: false, + // Nothing here is selectable -- auditlog has no write route at all (ADR + // 0021 Decision 8) -- and registerTable() drops Select All / Deselect + // All when it sees this. select: false, ajax: { url: '../management/index.php?node=agentactivity&sub=getList', - type: 'post' + type: 'post', + dataSrc: function(json) { + var out = []; + $.each(json.data || [], function(i, host) { + out.push(seedRow(host)); + }); + return out; + } } }); - // One child table per expanded host, each its own DataTable against the - // scoped endpoint. Destroyed on collapse rather than hidden: leaving them - // alive means every host a user has ever opened keeps redrawing behind a - // closed row. - function childTable(hostID) { - var id = 'agentactivity-child-' + hostID; - - return '
    ' - + '' - + '' - + '' - + '' - + '
    ' + $.escapeHtml('When') + '' + $.escapeHtml('Event') + '' + $.escapeHtml('Detail') + '' + $.escapeHtml('Outcome') + '
    '; - } + // Groups start collapsed, which is the whole point: the page opens as a + // list of hosts and what each last did, and you go looking from there. + // Nothing to do to arrange it -- `expanded` starts empty and the filter + // above keeps every non-anchor row out until a header is clicked. - // registerTable(), not a bare .DataTable(). The first version of this file - // hand-rolled the child and so opted out of every convention the helper - // applies -- including its `dom`, which is where the pager lives. The - // result showed the first ten of a host's events with no way to reach the - // rest, on a host with seventy-nine of them. - // - // Going through the helper also means the child gets the SAME infinite - // scroll as every other grid: registerTable() turns Scroller on unless the - // table uses rowGroup or opts out. So the collapsed-by-host view and - // continuous scrolling are not in tension after all -- the tension was - // between rowGroup and Scroller, and this design has no rowGroup. - function buildChild(hostID) { - var dt = $('#agentactivity-child-' + hostID).registerTable(null, { - order: [ - [0, 'desc'] - ], - columns: [ - escaped('createdTime'), - escaped('type'), - escaped('text'), - { - data: 'outcome', - render: function(d, t) { - var v = d === null ? '' : String(d); - if (t !== 'display') { - return v; - } - return '' - + $.escapeHtml(v) + ''; + function loadHost(name, hostID, done) { + if (loading[name]) { + return; + } + loading[name] = true; + + $.ajax({ + url: '../management/index.php?node=agentactivity&sub=getHostActivity&id=' + + encodeURIComponent(hostID), + type: 'post', + dataType: 'json', + // Route::listem() reads DataTables' own paging parameters, so the cap + // is expressed the way that endpoint already understands rather than + // by teaching it a second one. + data: {start: 0, length: ROWS_PER_HOST}, + success: function(json) { + var rows = (json && json.data) || [], + // recordsFiltered, NOT recordsTotal. listem()'s recordsTotal is + // every row in auditLog -- 1435 on the lab install -- so testing + // against it declared a 134-event host truncated at 500. + total = (json && json.recordsFiltered) || rows.length, + seed = table.rows().data().toArray().filter(function(r) { + return r.hostName === name && r.anchor; + })[0], + add = []; + + if (total > rows.length) { + truncated[name] = true; + } + + $.each(rows, function(i, row) { + // The newest row is already on screen as the anchor. Adding it + // again would show one event twice under its own header. + if (seed && String(row.createdTime) === String(seed.createdTime) + && String(row.type) === String(seed.type)) { + return; } + // A host with no anchor cannot be reached from the UI (there is + // no header to click), but the key still has to be per-host so a + // programmatic load cannot merge two hosts into one group. + add.push(eventRow( + seed || { + hostID: hostID, + hostName: name, + events: rows.length, + groupSort: String(row.createdTime) + '|' + String(hostID) + }, + row + )); + }); + + if (add.length) { + table.rows.add(add); } - ], - rowId: 'id', - processing: true, - serverSide: true, - // Nothing here is selectable: auditlog has no write route at all - // (ADR 0021 Decision 8), so a selection could not act on anything. - select: false, - // The helper's default viewport is 55vh, which is most of the screen -- - // right for a page that IS the table, wrong for one nested inside a row - // of another. This is tall enough to scroll and short enough that the - // host rows below stay reachable. - scrollY: '18rem', - ajax: { - url: '../management/index.php?node=agentactivity' - + '&sub=getHostActivity&id=' + encodeURIComponent(hostID), - type: 'post' + loaded[name] = true; + }, + complete: function() { + loading[name] = false; + done(); } }); - - // A child row is inserted AFTER the page has laid out, which is the same - // situation as a table built inside a hidden tab: Scroller measures a - // table that has no height and no width yet, and the header/body split - // stays misaligned until something re-measures. fogBindTableAutosize() - // does this on shown.bs.tab; there is no such event here, so the sizing - // pass runs once the row is actually in the document. - setTimeout(function() { - try { - fogSizeScroller(dt); - dt.columns.adjust(); - } catch (e) {} - }, 0); - - return dt; } - $table.on('click', '.agentactivity-expand', function() { - var $btn = $(this), - row = table.row($btn.closest('tr')), - hostID = row.id(); + // Delegated to the table: RowGroup redraws its headers on every draw, so + // a handler bound to the header elements themselves would be lost the + // first time anything sorted, searched or added a row. + $table.on('click', '.agentactivity-group', function(e) { + var name = $(this).attr('data-host'), + row = table.rows().data().toArray().filter(function(r) { + return r.hostName === name && r.anchor; + })[0]; + + e.preventDefault(); - if (row.child.isShown()) { - $('#agentactivity-child-' + hostID).DataTable().destroy(); - row.child.hide(); - $btn.attr('aria-expanded', 'false') - .find('i').removeClass('fa-chevron-down').addClass('fa-chevron-right'); + if (expanded[name]) { + expanded[name] = false; + table.draw(false); return; } - row.child(childTable(hostID)).show(); - buildChild(hostID); - $btn.attr('aria-expanded', 'true') - .find('i').removeClass('fa-chevron-right').addClass('fa-chevron-down'); + expanded[name] = true; + + if (loaded[name] || !row) { + table.draw(false); + return; + } + + // Drawn before the fetch as well as after: the chevron turns over + // immediately, so a slow endpoint reads as loading rather than as a + // click that did nothing. + table.draw(false); + loadHost(name, row.hostID, function() { + table.draw(false); + }); }); })(jQuery); diff --git a/packages/web/management/js/fog/fog.common.js b/packages/web/management/js/fog/fog.common.js index 5d2d29e156..3718c9a983 100644 --- a/packages/web/management/js/fog/fog.common.js +++ b/packages/web/management/js/fog/fog.common.js @@ -4423,6 +4423,23 @@ $.fn.registerTable = function(onSelect, opts) { } delete opts.extraButtons; + // A table that does not select does not get the buttons that select. + // + // `select: false` used to turn selection off and leave Select All and + // Deselect All sitting in the toolbar, where they were enabled, clickable + // and did nothing -- on 33 tables, including every report pane and the + // read-only event logs. The buttons live in `defaults.buttons` while the + // opt-out arrives in `opts`, so nothing connected the two. + // + // Dropped rather than disabled: a permanently grayed-out control still asks + // the reader what would enable it. The PHP half of the same statement is + // FOGPage::$selectable, which suppresses "Delete selected". + if (opts.select === false) { + defaults.buttons = defaults.buttons.filter(function(b) { + return !b || (b.extend !== 'selectAll' && b.extend !== 'selectNone'); + }); + } + // Column resizing is on for every table. Pulled off opts before they reach // DataTables, which has no such option and would only carry it around. var columnResize = opts.columnResize !== false; diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 30d193d88f..d779f058f0 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -5509,10 +5509,6 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" -#, fuzzy -msgid "Last Activity" -msgstr "Aktiv" - msgid "Last Agent Check-In" msgstr "" @@ -5529,10 +5525,6 @@ msgstr "" msgid "Last Deployed" msgstr "Zuletzt verteilt" -#, fuzzy -msgid "Last Event" -msgstr "Zuletzt hochgeladen" - msgid "Last Ping" msgstr "" @@ -10424,7 +10416,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13443,6 +13434,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "LDAP-Server" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Aktiv" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "Zuletzt hochgeladen" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Neueste SVN-Version" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 714426c0cf..7a3647c979 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -5509,10 +5509,6 @@ msgstr "Language" msgid "Largest images" msgstr "Images" -#, fuzzy -msgid "Last Activity" -msgstr "Active" - msgid "Last Agent Check-In" msgstr "" @@ -5529,10 +5525,6 @@ msgstr "" msgid "Last Deployed" msgstr "Last Deployed" -#, fuzzy -msgid "Last Event" -msgstr "Host Created" - msgid "Last Ping" msgstr "" @@ -10433,7 +10425,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13434,6 +13425,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "LDAP Server" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Active" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "Host Created" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Latest Version" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 8254071c71..6b76a179d7 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -5602,10 +5602,6 @@ msgstr "" msgid "Largest images" msgstr "Imagen" -#, fuzzy -msgid "Last Activity" -msgstr "Activo" - msgid "Last Agent Check-In" msgstr "" @@ -5622,10 +5618,6 @@ msgstr "" msgid "Last Deployed" msgstr "última Desplegado" -#, fuzzy -msgid "Last Event" -msgstr "Creado" - msgid "Last Ping" msgstr "" @@ -10591,7 +10583,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13599,6 +13590,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "servidor TFTP" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Activo" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "Creado" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Versión del sistema" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index c0c443cd22..d039e3ec87 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -5510,10 +5510,6 @@ msgstr "Sprache" msgid "Largest images" msgstr "Images" -#, fuzzy -msgid "Last Activity" -msgstr "Aktiv" - msgid "Last Agent Check-In" msgstr "" @@ -5530,10 +5526,6 @@ msgstr "" msgid "Last Deployed" msgstr "Zuletzt verteilt" -#, fuzzy -msgid "Last Event" -msgstr "Zuletzt hochgeladen" - msgid "Last Ping" msgstr "" @@ -10425,7 +10417,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13444,6 +13435,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "LDAP-Server" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Aktiv" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "Zuletzt hochgeladen" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Neueste SVN-Version" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 2d85394b8a..013b295c62 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -5509,10 +5509,6 @@ msgstr "La langue" msgid "Largest images" msgstr "Images" -#, fuzzy -msgid "Last Activity" -msgstr "actif" - msgid "Last Agent Check-In" msgstr "" @@ -5529,10 +5525,6 @@ msgstr "" msgid "Last Deployed" msgstr "Dernière Déployé" -#, fuzzy -msgid "Last Event" -msgstr "hôte Créé" - msgid "Last Ping" msgstr "" @@ -10417,7 +10409,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13419,6 +13410,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "Serveur LDAP" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "actif" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "hôte Créé" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Dernière version" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 6fb6bdae9b..c4cd0f1323 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -5363,10 +5363,6 @@ msgstr "Lingua" msgid "Largest images" msgstr "immagini" -#, fuzzy -msgid "Last Activity" -msgstr "Attivo" - msgid "Last Agent Check-In" msgstr "" @@ -5382,10 +5378,6 @@ msgstr "" msgid "Last Deployed" msgstr "Ultima Distribuita" -#, fuzzy -msgid "Last Event" -msgstr "Ultima cattura" - msgid "Last Ping" msgstr "" @@ -10140,7 +10132,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13061,6 +13052,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "Server LDAP" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Attivo" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "Ultima cattura" + #~ msgid "Latest SVN Version" #~ msgstr "Ultima versione di SVN" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 04ac8c62bd..6e668b661d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -5331,10 +5331,6 @@ msgstr "言語" msgid "Largest images" msgstr "イメージ" -#, fuzzy -msgid "Last Activity" -msgstr "有効" - #, fuzzy msgid "Last Agent Check-In" msgstr "タスクチェックイン日" @@ -5353,10 +5349,6 @@ msgstr "タスクチェックイン日" msgid "Last Deployed" msgstr "最終展開" -#, fuzzy -msgid "Last Event" -msgstr "最終キャプチャ" - msgid "Last Ping" msgstr "" @@ -10097,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13637,6 +13628,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "ユーザーフィルター" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "有効" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "最終キャプチャ" + #~ msgid "Last Updated Time" #~ msgstr "最終更新時刻" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index d52b2c6e08..60481b80a0 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -4715,9 +4715,6 @@ msgstr "" msgid "Largest images" msgstr "" -msgid "Last Activity" -msgstr "" - msgid "Last Agent Check-In" msgstr "" @@ -4733,9 +4730,6 @@ msgstr "" msgid "Last Deployed" msgstr "" -msgid "Last Event" -msgstr "" - msgid "Last Ping" msgstr "" @@ -8935,7 +8929,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 5766d6cb43..676b302e05 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -5509,10 +5509,6 @@ msgstr "Língua" msgid "Largest images" msgstr "imagens" -#, fuzzy -msgid "Last Activity" -msgstr "Ativo" - msgid "Last Agent Check-In" msgstr "" @@ -5529,10 +5525,6 @@ msgstr "" msgid "Last Deployed" msgstr "Última Implantado" -#, fuzzy -msgid "Last Event" -msgstr "host criado" - msgid "Last Ping" msgstr "" @@ -10420,7 +10412,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13422,6 +13413,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "Servidor LDAP" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "Ativo" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "host criado" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "Última versão" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 3434e75929..da055d8fad 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -5509,10 +5509,6 @@ msgstr "语言" msgid "Largest images" msgstr "图片" -#, fuzzy -msgid "Last Activity" -msgstr "活性" - msgid "Last Agent Check-In" msgstr "" @@ -5529,10 +5525,6 @@ msgstr "" msgid "Last Deployed" msgstr "最后部署" -#, fuzzy -msgid "Last Event" -msgstr "主机创建" - msgid "Last Ping" msgstr "" @@ -10420,7 +10412,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -13422,6 +13413,14 @@ msgstr "" #~ msgid "LDAP User Filter" #~ msgstr "LDAP服务器" +#, fuzzy +#~ msgid "Last Activity" +#~ msgstr "活性" + +#, fuzzy +#~ msgid "Last Event" +#~ msgstr "主机创建" + #, fuzzy #~ msgid "Latest SVN Version" #~ msgstr "最新版本" diff --git a/packages/web/src/Base/FOGPage.php b/packages/web/src/Base/FOGPage.php index c7d3635072..cd1567408f 100644 --- a/packages/web/src/Base/FOGPage.php +++ b/packages/web/src/Base/FOGPage.php @@ -68,6 +68,29 @@ abstract class FOGPage extends FOGBase * @var string */ public $title; + /** + * Whether this page's grid lets you select and act on rows. + * + * The toolbar used to decide by NODE NAME -- a hardcoded list of + * ['plugin', 'task', 'activity', 'audit'] that a read-only page had to + * be added to, and which the next one silently was not: Agent Activity + * shipped drawing a red "Delete selected" over a grid whose own JS sets + * `select: false` and whose table has no delete route anywhere in FOG + * (ADR 0021 Decision 8). + * + * A page says what it is instead of the toolbar guessing from its URL. + * The pages that were in that list set this false, and so does any page + * added later -- forgetting it now means the buttons appear, which is + * visible, rather than a name missing from a list nobody reads. + * + * This is the PHP half. Select All / Deselect All are DataTables Buttons + * and are dropped by registerTable() when a table passes + * `select: false`, which is the same statement made in the layer that + * owns those buttons. + * + * @var bool + */ + public $selectable = true; /** * The menu (always display) * @@ -1902,18 +1925,20 @@ public function process( if ($sub == 'list') { // Tasks are canceled per-pane, never deleted; the tabbed // task page hits sub=list via the no-sub default, so keep - // the delete actionbox off it. Activity and the audit log - // are read-only views of the event logs -- ?node=X&sub=list - // resolves to index() like any unknown sub does, and without - // this they would draw a "Delete selected" neither page - // implements. For the audit log there is nothing to - // implement it WITH: auditlog and auditchange have no delete - // route anywhere in FOG (ADR 0021 Decision 8). - if (!in_array( - $node, - ['plugin', 'task', 'activity', 'audit'], - true - )) { + // the delete actionbox off it. Activity, the audit log and + // agent activity are read-only views of the event logs -- + // ?node=X&sub=list resolves to index() like any unknown sub + // does, and without this they would draw a "Delete selected" + // none of them implements. For the audit trail there is + // nothing to implement it WITH: auditlog and auditchange + // have no delete route anywhere in FOG (ADR 0021 + // Decision 8). + // + // Read from the page, not from its node name. The list this + // replaces had to be edited every time a read-only page was + // added and was not when Agent Activity arrived, so that + // page shipped a red Delete selected it could not honor. + if ($this->selectable) { $actionbox .= self::makeButton( 'deleteSelected', _('Delete selected'), diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index cb3bc95eb2..0b831d2e28 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -131,7 +131,7 @@ public function __construct() // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. define('FOG_SCHEMA', 430); - define('FOG_BCACHE_VER', 362); + define('FOG_BCACHE_VER', 363); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as // a release asset. Pinned here rather than tracked as "latest" so a diff --git a/packages/web/src/Pages/ActivityManagement.php b/packages/web/src/Pages/ActivityManagement.php index 00dd8ea1d6..456600dadf 100644 --- a/packages/web/src/Pages/ActivityManagement.php +++ b/packages/web/src/Pages/ActivityManagement.php @@ -61,6 +61,15 @@ class ActivityManagement extends FOGPage * @var string */ public $node = 'activity'; + /** + * This grid does not select. + * + * Read only: this is a view of the event log, not a table anything here + * removes rows from. + * + * @var bool + */ + public $selectable = false; /** * Every source this viewer knows, before any permission is applied. * diff --git a/packages/web/src/Pages/AgentActivityManagement.php b/packages/web/src/Pages/AgentActivityManagement.php index be5e460891..75892b68a3 100644 --- a/packages/web/src/Pages/AgentActivityManagement.php +++ b/packages/web/src/Pages/AgentActivityManagement.php @@ -26,19 +26,40 @@ * install in one flat grid with no host filter, so "what has this machine's * agent been doing" meant scrolling past every other machine. * - * SUMMARY FIRST, ROWS ON DEMAND. This page lists one row per host -- - * hostname, how many agent events it has, when the last one was and what it - * was -- and fetches a host's actual rows only when it is expanded. + * ONE GRID, GROUPED BY HOST. Every host that has agent activity gets a + * RowGroup header carrying its name and its event count, and under it that + * host's newest event. Expanding a header loads the rest of that host's + * events and shows them as ordinary rows of the same grid. * - * That shape rather than a flat grid with group headers, for three reasons. - * DataTables' rowGroup would group only within the current PAGE under - * `serverSide`, so one hostname would head a dozen separate pages. Any table - * using rowGroup is auto-paged out of the infinite scroll by registerTable() - * -- Scroller's virtual row-height math cannot reconcile injected header - * rows -- and fog.audit.list.js already records that as the reason the audit - * grid does not group. And an agent writes an audit row per changed fact per - * host, so a flat list is the one thing that grows without bound while the - * summary is bounded by the size of the fleet. + * The first version of this page was a summary grid whose rows each opened + * a nested DataTable through `row.child()`. That never worked in the + * browser and could not have: a DataTables row has ONE child slot, + * registerTable() turns Responsive on for every grid, and Responsive claims + * that slot for its own hidden-column detail. Clicking expand rendered + * Responsive's field list and the nested table was never constructed -- + * measured on a live install at 1920px with no columns hidden. Nothing + * errored, which is why it survived a fix (`the expanded host was stuck at + * ten rows`) aimed at a pager inside a table that did not exist. + * + * So the drill-down uses no child row at all. Rows are added to the grid + * itself, which is also what makes them look like the rest of FOG rather + * than a grid nested inside a grid with its own scrollbar and its own pager. + * + * WHY EACH GROUP KEEPS A ROW. Collapsing is a search filter over the event + * rows, and RowGroup builds its headers from the rows that SURVIVE the + * filter -- a group with none left renders no header and the host + * disappears. Measured, not assumed. So the newest event of every host is + * seeded into the table and never filtered: it is the anchor its header is + * drawn from, and it doubles as the thing worth seeing when everything is + * collapsed, which is what each agent last did. + * + * WHY THE SEED IS A SUMMARY QUERY AND THE REST IS NOT. An agent writes a + * row per changed fact per host and FOG_AUDIT_RETENTION_DAYS defaults to 0, + * "keep everything forever" -- so the flat event set is unbounded and + * cannot be loaded client side, which is also why rowGroup over a + * `serverSide` grid is not an option here (it would group within one page). + * The seed is bounded by the size of the fleet; each expansion is bounded + * by a cap. Nothing on this page loads a set bounded by neither. * * The expand fetches through the SAME endpoint the host page's Agent * Activity tab uses, so there is one query behind both surfaces. @@ -68,6 +89,16 @@ class AgentActivityManagement extends FOGPage * @var string */ public $node = 'agentactivity'; + /** + * This grid does not select. + * + * Read only, like the audit log it reads: `auditlog` has no create, update + * or delete route anywhere in FOG (ADR 0021 Decision 8), so there is + * nothing here for a selection to act on. + * + * @var bool + */ + public $selectable = false; /** * The prefix every type this page shows begins with. * @@ -113,21 +144,31 @@ public function index(...$args) { $this->title = _('Agent Activity'); - // The first column is the expand control and carries no heading of - // its own: a header on it would be a label for a button. + // ONE column set for both kinds of row. The grid holds each host's + // newest event and, once its group is expanded, the rest of that + // host's events -- and those are the same shape, so they are the + // same columns. The host itself is not a column: it is the RowGroup + // header, which is where the per-host counts and the expand control + // live too. $this->headerData = [ - '', - _('Host'), - _('Events'), - _('Last Activity'), - _('Last Event') + _('When'), + _('Event'), + _('Detail'), + _('Outcome'), + _('Host') ]; + // The host column is hidden and stays hidden -- `noVis` keeps it out + // of the Column Visibility picker. It exists because RowGroup needs + // the table SORTED by its group, and a group is only contiguous if + // something orders it: ordering by time alone lets one host's older + // rows fall past the next host's newest one, which splits the group + // and draws its header a second time. Observed before this existed. $this->attributes = [ - ['class' => 'agentactivity-toggle'], [], [], [], - [] + [], + ['class' => 'noVis'] ]; echo '
    '; @@ -174,7 +215,8 @@ public function getList() $rows = self::$DB->query( 'SELECT a.`alSubjectID` AS `hostID`, h.`hostName` AS `hostName`, ' . 's.`events` AS `events`, s.`lastTime` AS `lastTime`, ' - . 'a.`alType` AS `lastType` ' + . 'a.`alType` AS `lastType`, a.`alText` AS `lastText`, ' + . 'a.`alOutcome` AS `lastOutcome` ' . 'FROM `auditLog` a ' . 'INNER JOIN (' . 'SELECT `alSubjectID`, COUNT(*) AS `events`, ' @@ -214,7 +256,14 @@ public function getList() : self::toDisplayStored( (string) $row['lastTime'] )->format('Y-m-d H:i:s'), - 'lastType' => (string) ($row['lastType'] ?? '') + 'lastType' => (string) ($row['lastType'] ?? ''), + // The newest row IN FULL, not just its type. It is the one + // row of the host that is always on screen -- the anchor its + // group header is drawn from -- so it has to carry the same + // four fields an expanded row does or the collapsed view + // would have empty cells under populated headings. + 'lastText' => (string) ($row['lastText'] ?? ''), + 'lastOutcome' => (string) ($row['lastOutcome'] ?? '') ]; } diff --git a/packages/web/src/Pages/AuditManagement.php b/packages/web/src/Pages/AuditManagement.php index 989a78842e..c83cb9a87e 100644 --- a/packages/web/src/Pages/AuditManagement.php +++ b/packages/web/src/Pages/AuditManagement.php @@ -49,6 +49,16 @@ class AuditManagement extends FOGPage * @var string */ public $node = 'audit'; + /** + * This grid does not select. + * + * Read only. `auditlog` and `auditchange` have no delete route anywhere in + * FOG (ADR 0021 Decision 8), so there is nothing here for a selection to + * act on. + * + * @var bool + */ + public $selectable = false; /** * How many change rows one header may show. * diff --git a/packages/web/src/Pages/PluginManagement.php b/packages/web/src/Pages/PluginManagement.php index 1663f63f07..0a8195c38e 100644 --- a/packages/web/src/Pages/PluginManagement.php +++ b/packages/web/src/Pages/PluginManagement.php @@ -38,6 +38,15 @@ class PluginManagement extends FOGPage * @var string */ public $node = 'plugin'; + /** + * This grid does not select. + * + * A plugin is installed and uninstalled by its own row action, never by + * ticking rows and pressing Delete. + * + * @var bool + */ + public $selectable = false; /** * Initialize the plugin page * diff --git a/packages/web/src/Pages/TaskManagement.php b/packages/web/src/Pages/TaskManagement.php index 9022259a67..81b12888f0 100644 --- a/packages/web/src/Pages/TaskManagement.php +++ b/packages/web/src/Pages/TaskManagement.php @@ -48,6 +48,15 @@ class TaskManagement extends FOGPage * @var string */ public $node = 'task'; + /** + * This grid does not select. + * + * Tasks are canceled per-pane, never deleted, so there is nothing for a + * selection to act on. + * + * @var bool + */ + public $selectable = false; /** * Initializes the task page items. * diff --git a/tests/agent-activity-grouping.test.php b/tests/agent-activity-grouping.test.php new file mode 100644 index 0000000000..098feba3b2 --- /dev/null +++ b/tests/agent-activity-grouping.test.php @@ -0,0 +1,250 @@ +", and the group splits exactly as before. So the key is + * COPIED, and that is what this pins. + * + * 4. TRUNCATION READS recordsFiltered. Route::listem()'s recordsTotal is + * every row in auditLog -- 1435 on the lab install -- not the host's. A + * cap test against it called a 134-event host truncated at 500 and put + * "showing the newest 500" on a header that was showing all of them. + * + * And the toolbar half, which is not specific to this page: a grid that + * passes `select: false` must not be given Select All and Deselect All, and + * a page that declares itself unselectable must not be given "Delete + * selected". That used to be decided by a hardcoded list of node names, + * which this page was not added to -- so it shipped a red Delete selected + * over a table with no delete route anywhere in FOG (ADR 0021 Decision 8). + * + * Usage: php tests/agent-activity-grouping.test.php + * Exit status 0 = pass, 1 = fail. + * + * PHP version 7.4+ + * + * @category Tests + * @package FOGProject + * @author Tom Elliott + * @license http://opensource.org/licenses/gpl-3.0 GPLv3 + * @link https://fogproject.org + */ + +use FOG\Base\FOGPage; +use FOG\Pages\ActivityManagement; +use FOG\Pages\AgentActivityManagement; +use FOG\Pages\AuditManagement; +use FOG\Pages\PluginManagement; +use FOG\Pages\TaskManagement; + +require_once __DIR__ . '/lib/fog-test-harness.php'; + +FogTestHarness::boot('agent-activity-grouping'); + +$t = new FogChecks(); + +/** + * A file's CODE, with its comment lines removed. + * + * Every check below asks whether the source still does something, and the + * source explains at length why it does it -- including by quoting the + * broken shape it replaced. Scanning the raw file makes those explanations + * fail the build: the first run of this test went red on `row.child()` and + * on the old node list, both of which appear only inside the comments that + * say never to go back to them. A gate that cannot survive being described + * is a gate that pressures the next person to delete the description. + * + * Whole comment lines only, which is all this codebase writes -- so no + * string containing // is at risk, and every check here matches on a line + * of code. + * + * @param string $path file to read + * + * @return string + */ +$code = static function ($path) { + $out = []; + foreach (preg_split('/\R/', (string)file_get_contents($path)) as $line) { + $trim = ltrim($line); + if ('' !== $trim + && (0 === strpos($trim, '//') + || 0 === strpos($trim, '/*') + || 0 === strpos($trim, '*')) + ) { + continue; + } + $out[] = $line; + } + return implode("\n", $out); +}; + +$web = dirname(__DIR__) . '/packages/web'; +$js = $code( + $web . '/management/js/fog/agentactivity/fog.agentactivity.list.js' +); +$common = $code($web . '/management/js/fog/fog.common.js'); +$page = $code( + (new \ReflectionClass(AgentActivityManagement::class))->getFileName() +); +$base = $code( + (new \ReflectionClass(FOGPage::class))->getFileName() +); + +// 1. No child rows on this page, at all. Responsive owns the one slot. +$t->check( + 'the grid uses no row.child() -- Responsive owns that slot', + false === strpos($js, 'row.child') + && false === strpos($js, '.child(') +); + +// The replacement it must be using instead: rows added to the grid itself. +$t->check( + 'expanded events are added to the grid (rows.add)', + false !== strpos($js, 'table.rows.add(') +); + +// 2. The collapse filter passes the anchor unconditionally. Pinned on the +// shape of the expression, because a filter that happens to return true for +// an anchor today by testing something else would not survive a rename. +$t->check( + 'the collapse filter lets the anchor row through', + 1 === preg_match( + '/return\s+row\.anchor\s*===\s*true\s*\|\|\s*expanded\[row\.hostName\]/', + $js + ) +); +$t->check( + 'the seed row is marked as the anchor', + 1 === preg_match('/anchor:\s*true/', $js) + && 1 === preg_match('/anchor:\s*false/', $js) +); + +// 3. eventRow() COPIES the group key rather than recomputing it. Recomputing +// from a seed row is the exact bug: seed.lastTime is undefined. +$t->check( + 'eventRow copies groupSort from the seed, never recomputes it', + 1 === preg_match('/groupSort:\s*seed\.groupSort/', $js) + && false === strpos($js, 'String(seed.lastTime)') +); + +// The table has to be ORDERED by the group, or contiguity is luck. +$t->check( + 'the grid orders by the hidden group column first', + 1 === preg_match('/order:\s*\[\s*\[\s*4,\s*\'desc\'\s*\]/', $js) +); +$t->check( + 'the hidden group column sorts on groupSort', + 1 === preg_match( + '/if\s*\(t\s*===\s*\'sort\'\s*\|\|\s*t\s*===\s*\'type\'\)\s*\{\s*return\s+row\.groupSort;/', + $js + ) +); +// A fifth has to exist for a fifth column to be addressable, and it +// must be out of the Column Visibility picker. +$t->check( + 'the page emits the hidden Host column as noVis', + false !== strpos($page, "['class' => 'noVis']") + && 1 === preg_match("/_\('Outcome'\),\s*_\('Host'\)/", $page) +); + +// 4. The cap is judged against this host's count, not the whole audit log. +$t->check( + 'truncation reads recordsFiltered, not recordsTotal', + false !== strpos($js, 'json.recordsFiltered') + && false === strpos($js, 'json.recordsTotal') +); + +// The anchor needs the newest row IN FULL or the collapsed view has empty +// cells under populated headings. +foreach (['lastText', 'lastOutcome'] as $field) { + $t->check( + sprintf('getList() returns %s for the anchor row', $field), + false !== strpos($page, "'" . $field . "' =>") + && false !== strpos($js, $field) + ); +} + +// ---- the toolbar half ---- + +// The PHP seam: a property, not a list of node names. +$t->check( + 'FOGPage declares $selectable and defaults it true', + 1 === preg_match('/public\s+\$selectable\s*=\s*true;/', $base) +); +$t->check( + 'the delete actionbox is gated on $this->selectable', + false !== strpos($base, 'if ($this->selectable) {') +); +$t->check( + 'the hardcoded read-only node list is gone', + false === strpos($base, "['plugin', 'task', 'activity', 'audit']") +); + +// Every page that was in that list, plus the one it missed. +foreach ( + [ + 'agentactivity' => AgentActivityManagement::class, + 'audit' => AuditManagement::class, + 'activity' => ActivityManagement::class, + 'plugin' => PluginManagement::class, + 'task' => TaskManagement::class + ] as $node => $class +) { + $src = $code((new \ReflectionClass($class))->getFileName()); + $t->check( + sprintf('%s declares itself unselectable', $node), + 1 === preg_match('/public\s+\$selectable\s*=\s*false;/', $src) + ); +} + +// The JS seam: the buttons that select are dropped when nothing selects. +$t->check( + 'registerTable drops selectAll/selectNone on select:false', + 1 === preg_match( + '/if\s*\(opts\.select\s*===\s*false\)\s*\{\s*defaults\.buttons\s*=\s*defaults\.buttons\.filter/', + $common + ) + && false !== strpos($common, "b.extend !== 'selectAll'") + && false !== strpos($common, "b.extend !== 'selectNone'") +); + +// The page's own grid has to actually make that statement, or the seam is +// wired to nothing here. +$t->check( + 'the agent activity grid passes select: false', + 1 === preg_match('/select:\s*false/', $js) +); + +$t->finish(); diff --git a/tests/agent-activity-page.test.php b/tests/agent-activity-page.test.php index e772620db5..b5976dec83 100644 --- a/tests/agent-activity-page.test.php +++ b/tests/agent-activity-page.test.php @@ -20,10 +20,16 @@ * audit log discloses attempted usernames and refusals; aliasing this * onto it would force anyone who may see what an agent did to also see * every failed sign-in. - * - NO rowGroup. fog.audit.list.js records why the audit grid does not - * group, and registerTable() auto-pages any table that does -- so a - * grouped grid would silently lose the infinite scroll. The grouping here - * is done in SQL instead. + * - rowGroup, grouped on hostName. This bullet used to say the opposite -- + * that the grid must NOT group, because registerTable() auto-pages any + * table that does and a grouped grid loses the infinite scroll. That trade + * was real but it was the wrong side of it: the summary-plus-child-table + * arrangement it protected never worked in a browser at all, because + * Responsive owns the one row.child() slot every DataTables row has. The + * page now groups, is paged rather than infinitely scrolled, and expands a + * host by adding rows to the same grid. tests/agent-activity-grouping + * .test.php holds the detail; what is pinned here is that the child table + * does not come back. * * Usage: php tests/agent-activity-page.test.php * Exit status 0 = pass, 1 = fail. @@ -169,9 +175,9 @@ class_exists('FOG\Pages\AgentActivityManagement') // ------------------------------------------------------ the grid contract $t->check( - 'the grid does not use rowGroup, which would auto-page it out of the ' - . 'infinite scroll and group only within one page', - false === strpos($jsSrc, 'rowGroup:') + 'the grid groups on hostName with rowGroup', + false !== strpos($jsSrc, 'rowGroup:') + && 1 === preg_match('~dataSrc:\s*\x27hostName\x27~', $jsSrc) ); // headerData and attributes are positional; a mismatch silently shifts @@ -214,16 +220,26 @@ class_exists('FOG\Pages\AgentActivityManagement') // -- including its `dom`, which is where the pager lives, and its Scroller // setup. An expanded host showed the first ten of its events with no way to // reach the rest. Reported against a host with seventy-nine of them. -$t->check( - 'the expanded host table goes through registerTable, not a bare DataTable', - 1 === preg_match( - '~agentactivity-child-\x27 \+ hostID\)\.registerTable\(~', - $jsSrc +// Comment lines dropped first. The file explains at length why row.child() +// is not used here, and scanning the prose that says "never do this" for the +// thing not to do fails the build on the documentation. +$jsCode = implode( + "\n", + array_filter( + preg_split('/\R/', $jsSrc), + static function ($line) { + $trim = ltrim($line); + return '' === $trim + || (0 !== strpos($trim, '//') + && 0 !== strpos($trim, '/*') + && 0 !== strpos($trim, '*')); + } ) ); $t->check( - 'no bare .DataTable( construction builds the child grid', - 0 === preg_match('~child-\x27 \+ hostID\)\.DataTable\(\{~', $jsSrc) + 'no child table is built for an expanded host, by any route', + false === strpos($jsCode, 'agentactivity-child-') + && 0 === preg_match('~\brow\.child\(~', $jsCode) ); // registerTable() sizes a Scroller table on a setTimeout(0) after init. A From 74af35c568d542fd7362f00967af0e2690d04fad Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 15:01:18 +0000 Subject: [PATCH 102/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index d779f058f0..1a05730791 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10416,6 +10416,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 7a3647c979..070edf0a87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10425,6 +10425,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 6b76a179d7..6c1c93fc98 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10583,6 +10583,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index d039e3ec87..634d05e1fe 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10417,6 +10417,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 013b295c62..e0a424abfb 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10409,6 +10409,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index c4cd0f1323..053a87d8f8 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10132,6 +10132,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 6e668b661d..089143db3b 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 60481b80a0..54b04f34a3 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8929,6 +8929,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 676b302e05..77fe001f03 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10412,6 +10412,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index da055d8fad..c014fbef54 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10412,6 +10412,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 60b8a9bd6d76ce3ccd9ad37d91539362e61e8689 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 10:38:30 -0500 Subject: [PATCH 103/117] Agent Activity: stop paging a grid whose unit is hosts Three faults, one root. Paging counts ROWS; this page's unit is HOSTS. Expanding one host with 83 events at 25 rows a page filled pages one to three with that host and pushed every other host onto page four. rowGroup redraws a group's header on every page the group spans, so the same host then appeared four times, each apparently expanded -- which is what it looked like to the person reading it, and it is not a rowGroup bug. It is what paging by row does when the thing grouped is larger than a page. No page length fixes that, because the number of rows an expansion adds is a property of the host and not of the setting. So paging is off. Collapsed, the grid is one row per host; expanded, it gets longer and you scroll. The seed is still bounded by MAX_HOSTS and each expansion by ROWS_PER_HOST, so this is not "no limit". Scroller is not the alternative -- registerTable() excludes any rowGroup table from it. With paging gone the "entries per page" control has nothing to put in itself, and rendered as an empty box beside its own label. Reported as unreadable in both themes, which it was: there was nothing in it to read. lengthChange: false removes the control rather than styling an empty one. And the toolbar's Refresh is dt.clear().draw() + ajax.reload() -- it throws the rows away and re-fetches the seed. The per-host maps survived that, so a host still marked `loaded` was never re-fetched, its rows were gone, and clicking its header did nothing at all. It read as the expander breaking permanently after one press of Refresh. They now reset on xhr.dt, which fires when the new seed lands. Verified on the live install: 21 group headers collapsed and 21 expanded with no duplicates, no host displaced, the length control and pager both absent, and expand working again after a Refresh. Three more gates in tests/agent-activity-grouping.test.php, each proven by reintroducing the defect and watching it go red. FOG_BCACHE_VER 363 -> 364. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01JWJMQYE2br8E7Ehr55SJp2 --- .../agentactivity/fog.agentactivity.list.js | 44 +++++++++++++++++++ .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - .../web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Base/System.php | 2 +- tests/agent-activity-grouping.test.php | 36 +++++++++++++++ 13 files changed, 81 insertions(+), 11 deletions(-) diff --git a/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js index a370caab64..7c9bdf0398 100644 --- a/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js +++ b/packages/web/management/js/fog/agentactivity/fog.agentactivity.list.js @@ -243,6 +243,33 @@ startRender: groupHeader }, processing: true, + // NO PAGING, and that is the whole reason this page reads correctly. + // + // Paging counts ROWS; this page's unit is HOSTS. Expanding one host with + // 83 events at 25 rows a page filled pages one to three with that host + // and pushed every other host onto page four -- and RowGroup redraws a + // group's header on each page the group spans, so the same host appeared + // four times, expanded, which is what it looked like to the person + // reading it. Neither is a bug in RowGroup: both are what paging by row + // means when the thing being grouped is bigger than a page. + // + // There is no page length that fixes it, because the number of rows an + // expansion adds is a property of the host, not of the setting. Turning + // paging off removes the unit mismatch instead of tuning it: collapsed, + // the grid is one row per host; expanded, it gets longer and you scroll. + // The seed is bounded by the fleet (MAX_HOSTS) and each expansion by + // ROWS_PER_HOST, so "no paging" is not "no limit". + // + // Scroller is not the alternative -- registerTable() excludes any + // rowGroup table from it, because its virtual row-height math cannot + // reconcile injected header rows. + paging: false, + // ...and with it the "entries per page" control, which paging is the + // only thing that gives a value. Left in, it renders as an empty box + // beside its own label in both themes -- not a contrast problem, a + // control with nothing to say. dom keeps `l` for every other grid, so + // this is the switch that removes it here. + lengthChange: false, // Client side: the seed is one row per host, which is bounded by the // fleet, and rowGroup cannot group a server-side grid beyond one page. serverSide: false, @@ -268,6 +295,23 @@ // Nothing to do to arrange it -- `expanded` starts empty and the filter // above keeps every non-anchor row out until a header is clicked. + // Every reload starts over, because a reload throws the rows away. + // + // The toolbar's Refresh is `dt.clear().draw(); dt.ajax.reload();` -- it + // empties the table and re-fetches the seed. Without this the maps below + // survived that: a host still marked `loaded` was never re-fetched, so its + // rows were gone and clicking its header did nothing at all. It read as + // the expander breaking permanently after one press of Refresh. + // + // xhr.dt fires when the new seed lands, including the first load, where + // resetting empty maps costs nothing. + $table.on('xhr.dt', function() { + expanded = {}; + loaded = {}; + loading = {}; + truncated = {}; + }); + function loadHost(name, hostID, done) { if (loading[name]) { return; diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 1a05730791..d779f058f0 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10416,7 +10416,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 070edf0a87..7a3647c979 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10425,7 +10425,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 6c1c93fc98..6b76a179d7 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10583,7 +10583,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 634d05e1fe..d039e3ec87 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10417,7 +10417,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index e0a424abfb..013b295c62 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10409,7 +10409,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 053a87d8f8..c4cd0f1323 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10132,7 +10132,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 089143db3b..6e668b661d 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10089,7 +10089,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 54b04f34a3..60481b80a0 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8929,7 +8929,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 77fe001f03..676b302e05 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10412,7 +10412,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index c014fbef54..da055d8fad 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10412,7 +10412,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 0b831d2e28..02630cd14b 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -131,7 +131,7 @@ public function __construct() // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. define('FOG_SCHEMA', 430); - define('FOG_BCACHE_VER', 363); + define('FOG_BCACHE_VER', 364); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as // a release asset. Pinned here rather than tracked as "latest" so a diff --git a/tests/agent-activity-grouping.test.php b/tests/agent-activity-grouping.test.php index 098feba3b2..93d479aa93 100644 --- a/tests/agent-activity-grouping.test.php +++ b/tests/agent-activity-grouping.test.php @@ -179,6 +179,42 @@ && 1 === preg_match("/_\('Outcome'\),\s*_\('Host'\)/", $page) ); +// 5. Paging is OFF, and that is load bearing rather than cosmetic. +// +// Paging counts rows; this page's unit is hosts. With 25 rows a page, one +// expanded host with 83 events filled pages one to three and pushed every +// other host onto page four -- and rowGroup redraws a group's header on each +// page the group spans, so that host appeared four times, expanded. No page +// length fixes it: the row count an expansion adds belongs to the host, not +// to the setting. +$t->check( + 'the grid does not page', + 1 === preg_match('/paging:\s*false/', $js) +); +// The length control has no value once paging is off, so it renders as an +// empty box next to its own label -- reported in both themes as unreadable, +// which it was, because there was nothing in it to read. +$t->check( + 'the entries-per-page control is removed with it', + 1 === preg_match('/lengthChange:\s*false/', $js) +); + +// 6. A reload starts over. Refresh is dt.clear().draw() + ajax.reload(), so +// the rows go away; a host still marked loaded was never re-fetched and its +// header stopped responding entirely. That read as the expander breaking +// permanently after one press of Refresh. +$t->check( + 'every per-host map is reset when new data arrives', + 1 === preg_match( + '/on\(\x27xhr\.dt\x27[^)]*\)?[^{]*\{\s*' + . 'expanded\s*=\s*\{\};\s*' + . 'loaded\s*=\s*\{\};\s*' + . 'loading\s*=\s*\{\};\s*' + . 'truncated\s*=\s*\{\};/s', + $js + ) +); + // 4. The cap is judged against this host's count, not the whole audit log. $t->check( 'truncation reads recordsFiltered, not recordsTotal', From 8738ea9938c0ac091c08c666cda21c83484b8e54 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 15:43:13 +0000 Subject: [PATCH 104/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index d779f058f0..1a05730791 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10416,6 +10416,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 7a3647c979..070edf0a87 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10425,6 +10425,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 6b76a179d7..6c1c93fc98 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10583,6 +10583,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index d039e3ec87..634d05e1fe 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10417,6 +10417,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 013b295c62..e0a424abfb 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10409,6 +10409,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index c4cd0f1323..053a87d8f8 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10132,6 +10132,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 6e668b661d..089143db3b 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10089,6 +10089,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 60481b80a0..54b04f34a3 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8929,6 +8929,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 676b302e05..77fe001f03 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10412,6 +10412,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index da055d8fad..c014fbef54 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10412,6 +10412,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From f2370b77c28b59492afa008ef6c63a53791d8659 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 13:01:35 -0500 Subject: [PATCH 105/117] Display Manager goes; Auto Log Out is rebuilt for the agent Two changes to the same surface, so they land together: they touch the same module list, the same service-configuration tabs and the same schema file, and splitting them would mean a commit that half-edits all three. DISPLAY MANAGER IS REMOVED, AND ITS TABLE WITH IT. The module reset a client's screen resolution to a fixed size at logoff and at startup. That was reasonable for a lab in 2010 and it is the wrong layer now: Windows has honored each monitor's own preferred mode by itself for a decade, a fixed resolution pushed over the top of it is wrong on every machine whose panel is not the size the setting names, and a laptop that docks changes its answer twice a day. Nothing in the rebuilt agent implements it and nothing will. This is the greenfog removal (step 375) repeated with one difference: Display Manager owns a table, and the table goes too. Step 431 deletes the per-host module answers, the `modules` row and the four FOG_CLIENT_DISPLAYMANAGER_* settings in that order, then drops `hostScreenSettings` through Schema::dropTable() -- which is also what lets tests/schema-retired-tables.test.php see the drop and account for the table's absence from the manifest. THE PER-HOST WIDTH, HEIGHT AND REFRESH ARE GONE AND NOT RECOVERABLE. That was considered and chosen. Every consumer is removed in this commit -- the client endpoint, the host card, the mass-edit field, Host::getDispVals() and setDisp(), Group::setDisp(), Setting::setDisplay() -- so keeping the table would mean keeping HostScreenSetting, its manager, its `hostscreensetting` REST route, its Authorization mapping and its foreign key alive to serve data nothing writes and nothing honors. Step 375 named that failure: a setting that lies is worse than no setting, and an API route reporting a resolution the fleet does not apply is a setting that lies. MassEdit::resolveComposite() goes with it, not as tidying. It existed for exactly one field -- a resolution is three numbers written as one row -- and removing that field orphans it, along with the whole `composite` concept in the host mass edit. The array-value guard in columnUpdates() stays: it protects against a plugin naming `field` on a key whose posted value is an array, which is still reachable. AUTO LOG OUT IS KEPT, AND IS NOW AN AGENT CAPABILITY. It is the one legacy module the rebuild reimplements rather than drops: a machine left logged in at a desk holds a profile and, on a shared lab machine, somebody else's turn, and nothing else in the stack answers that. State::CAPABILITIES gains `autologout`, gated on the existing module exactly as every other capability is, so an admin's per-host and per-group choices carry over untouched. Host::getAlo() is unchanged and the block is withheld below five minutes, so a policy under the floor CLEARS what the agent stored rather than sitting there as a number nobody acts on. FOG_CLIENT_AUTOLOGOFF_WARN is new (step 432, default 60): how long the user is told first. It is edited on the existing Auto Log Out tab next to the timeout it modifies, and 0 means log the user out with no warning. FOG_CLIENT_AUTOLOGOFF_BGIMAGE is removed in the same step. It named the background of the .NET client's countdown window, and there is no countdown window: the agent is a service in session 0, which has had no visible desktop since Vista, so it warns through WTSSendMessage. Nothing has read the setting since the legacy client stopped shipping and no page ever rendered it -- the FOG_PLUGINSYS_DIR and greenfog defect exactly. Design 0014 in fog-agent has the rest. THREE THINGS FOUND WHILE DOING IT. FOG_SCHEMA was 430 and is now 432. tests/schema-gate.test.php caught this and it was not cosmetic: the coarse gate is `mySchema < FOG_SCHEMA`, so both new steps would have applied to NOBODY on any existing install, with no error and no log line. Only a fresh install, which runs from 0, would ever have seen them. packages/web/vendor/composer/autoload_classmap.php was stale on this branch -- it listed none of the FOG\Agent\* classes this branch added, nor FOG\Base\SmbiosIdentity or FOG\Assign\Resolver. dump-autoload had to run anyway to drop the deleted classes; the additions are that staleness corrected, not churn. phpstan-tests-baseline.neon needed three patterns updated, by hand rather than regenerated. A baselined message spells out the whole inferred array shape, so a ninth key in State::CAPABILITIES un-matches every entry naming it. Regenerating the file moved forty-odd unrelated entries; three lines are edited instead. VERIFIED: sh tests/run-all.sh, both phpstan passes, php -l on every file. certificate-table.test.php fails identically on working-1.6 at 48dc1c5cb and is inherited, not from this. NOT VERIFIED HERE: tests/schema-executes.test.php and bin/upgrade-rehearsal.php both need a database user that may CREATE DATABASE, which a FOG service account deliberately is not. Steps 431 and 432 have not been executed against a server by this commit; CI's schema matrix is what does that. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- bin/fk-lab-fixture.php | 1 - bin/upgrade-rehearsal.php | 2 +- ...l-integrity-is-declared-in-the-database.md | 2 +- docs/development/foreign-keys.md | 5 +- docs/development/group-split.md | 1 - docs/release/1.6.0-release-notes.DRAFT.md | 36 ++- packages/web/commons/schema-constraints.php | 1 - packages/web/commons/schema-expected.php | 17 +- packages/web/commons/schema.php | 84 ++++++ .../management/js/fog/host/fog.host.edit.js | 43 --- .../js/fog/service/fog.service.list.js | 1 - .../de_DE.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../en_US.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../es_ES.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../it_IT.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 83 +++--- .../web/management/languages/messages.pot | 46 +-- .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 80 +++-- .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 80 +++-- packages/web/service/displaymanager.php | 41 --- packages/web/src/Agent/State.php | 28 ++ packages/web/src/Auth/Authorization.php | 1 - packages/web/src/Base/FOGBase.php | 1 - packages/web/src/Base/System.php | 2 +- packages/web/src/Client/DisplayManager.php | 40 --- packages/web/src/Client/FOGClient.php | 1 - packages/web/src/Items/Group.php | 38 --- packages/web/src/Items/Host.php | 79 ----- packages/web/src/Items/HostScreenSetting.php | 67 ----- packages/web/src/Items/Setting.php | 24 -- .../src/Managers/HostScreenSettingManager.php | 35 --- .../web/src/Pages/FOGConfigurationPage.php | 6 +- packages/web/src/Pages/HostManagement.php | 278 +----------------- .../src/Pages/ServiceConfigurationPage.php | 140 +++------ packages/web/src/Router/Route.php | 9 +- packages/web/src/Util/MassEdit.php | 88 +----- .../web/vendor/composer/autoload_classmap.php | 73 ++++- .../web/vendor/composer/autoload_static.php | 73 ++++- phpstan-baseline.neon | 8 +- phpstan-tests-baseline.neon | 6 +- tests/fixtures/route-cascade-contract.txt | 2 - tests/fixtures/route-column-contract.txt | 10 - tests/foreign-key-map.test.php | 1 - tests/group-grants-are-owned.test.php | 2 +- tests/mass-edit-endpoint-is-gated.test.php | 42 +-- tests/mass-edit-fails-closed.test.php | 74 ----- tests/mass-edit-form.test.php | 34 +-- 49 files changed, 701 insertions(+), 1464 deletions(-) delete mode 100644 packages/web/service/displaymanager.php delete mode 100644 packages/web/src/Client/DisplayManager.php delete mode 100644 packages/web/src/Items/HostScreenSetting.php delete mode 100644 packages/web/src/Managers/HostScreenSettingManager.php diff --git a/bin/fk-lab-fixture.php b/bin/fk-lab-fixture.php index 810c032c5c..341c659c05 100644 --- a/bin/fk-lab-fixture.php +++ b/bin/fk-lab-fixture.php @@ -260,7 +260,6 @@ function ex(\PDO $pdo, string $sql): int 'snapinJobs' => 'sjHostID', 'tasks' => 'taskHostID', 'hostAutoLogOut' => 'haloHostID', - 'hostScreenSettings' => 'hssHostID', 'groupMembers' => 'gmHostID', 'snapinAssoc' => 'saHostID', 'printerAssoc' => 'paHostID', diff --git a/bin/upgrade-rehearsal.php b/bin/upgrade-rehearsal.php index ac2b86c33c..4c64a8845f 100644 --- a/bin/upgrade-rehearsal.php +++ b/bin/upgrade-rehearsal.php @@ -1068,7 +1068,7 @@ function seedRow($label, $table, array $values) 'moduleStatusByHost', 'inventory', 'tasks', 'scheduledTasks', 'images', 'nfsGroups', 'nfsGroupMembers', 'users', 'snapinJobs', 'snapinTasks', 'multicastSessions', 'multicastSessionsAssoc', 'greenFog', - 'hostScreenSettings', 'hostAutoLogOut', 'powerManagement', 'taskLog', + 'hostAutoLogOut', 'powerManagement', 'taskLog', 'sites', 'siteHostMembers', 'siteUserMembers', 'siteGroupMembers']; printf("census %s (schema %d)\n", $db, $runner->version()); foreach ($tables as $t) { diff --git a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md index 1e7a409c7c..680a18c5a2 100644 --- a/docs/adr/0031-referential-integrity-is-declared-in-the-database.md +++ b/docs/adr/0031-referential-integrity-is-declared-in-the-database.md @@ -15,7 +15,7 @@ windowskey 2, ldap 6, oidc 8, capone 2, subnetgroup 1 -- are declared in core's map and applied by a step in each plugin's own `schema()` in `FOGProject/fog-plugins`. -**124 of the map's 139 relationships are declared.** The other 15 are not +**123 of the map's 138 relationships are declared.** The other 15 are not pending work: they carry action `none`, which the map's docblock defines as a decision rather than an omission. Nine are audit rows, which MUST NOT constrain the thing they record (ADR 0021, `schema.php` step 341); six are diff --git a/docs/development/foreign-keys.md b/docs/development/foreign-keys.md index cc38bc67da..e74d3833d2 100644 --- a/docs/development/foreign-keys.md +++ b/docs/development/foreign-keys.md @@ -224,7 +224,7 @@ today and the class where PHP already agrees. ### 1:1 and 1:N satellites — CASCADE. Agreed. -`inventory`, `hostScreenSettings`, `hostAutoLogOut`, `powerManagement`, +`inventory`, `hostAutoLogOut`, `powerManagement`, `greenFog`, `apiTokens`, `userAuths`, `nfsGroupMembers`, and the plugins' `LDAPGroups`, `OIDCGroups`, `oidcIdentity`. @@ -603,7 +603,7 @@ half-converted column. ## Phase D — plugins, and the direction rule 18 plugin tables ship in `FOGProject/fog-plugins`. All 18 clone cleanly into -the survey and 25 of the map's 139 relationships live in them. +the survey and 25 of the map's 138 relationships live in them. ### Direction is the whole rule @@ -877,7 +877,6 @@ snapinAssoc saHostID -> hosts saSnapinID -> snapins printerAssoc paHostID -> hosts paPrinterID -> printers moduleStatusByHost msHostID -> hosts msModuleID -> modules inventory iHostID -> hosts -hostScreenSettings hssHostID -> hosts hostAutoLogOut haloHostID -> hosts powerManagement pmHostID -> hosts greenFog gfHostID -> hosts diff --git a/docs/development/group-split.md b/docs/development/group-split.md index 6116f0130e..b76aa59925 100644 --- a/docs/development/group-split.md +++ b/docs/development/group-split.md @@ -46,7 +46,6 @@ grep -n 'function ' packages/web/src/Items/Group.php | `removeSnapin()` | 293 | `deletemass` | over membership | | `setSnapinOrder()` | 316 | `saSequence` | `new Host()` per member | | `addModule()` | 346 | `moduleStatusByHost` | one per host × module | -| `setDisp()` | 401 | delete-all + insert | one per host | | `setAlo()` | 436 | delete-all + insert | one per host | | `addImage()` | 501 | `hosts.hostImage` | one `UPDATE ... IN` | | `setAD()` | 1100 | five `hosts` columns | one `UPDATE ... IN` | diff --git a/docs/release/1.6.0-release-notes.DRAFT.md b/docs/release/1.6.0-release-notes.DRAFT.md index 61e31db068..bf24537442 100644 --- a/docs/release/1.6.0-release-notes.DRAFT.md +++ b/docs/release/1.6.0-release-notes.DRAFT.md @@ -220,6 +220,36 @@ pre-upgrade *dump* — it is not a schema rollback. ## Breaking changes +- **Display Manager is removed, and its per-host screen settings are deleted.** + The module reset a client's resolution to a fixed size at logoff and at + startup. That is the wrong layer now: Windows has honored each monitor's + own preferred mode for a decade, a fixed resolution pushed over the top of + it is wrong on every machine whose panel is not the size the setting names, + and a laptop that docks changes its answer twice a day. + + The upgrade drops the `hostScreenSettings` table, the `displaymanager` + module and its per-host answers, and the four `FOG_CLIENT_DISPLAYMANAGER_*` + settings. **The per-host width, height and refresh values are not + recoverable after the upgrade** — take the backup the warning above asks + for if you want them. The `hostscreensetting` API route goes with them, so + tooling that reads it must stop. + + Nothing replaces it. The new agent does not implement it and will not. + +- **Auto Log Out is kept, and gains a warning you can set.** It is the one + legacy client module the new agent reimplements rather than drops: the + per-host time, the global `FOG_CLIENT_AUTOLOGOFF_MIN` default and the + five-minute floor all behave exactly as they did. New alongside them is + **FOG_CLIENT_AUTOLOGOFF_WARN** — how many seconds before the log out the + user is told, on the same Auto Log Out settings tab, defaulting to 60. Set + it to 0 to log the user out with no warning. + + The warning is a message box in the user's own session rather than the old + client's countdown window with a background image, because the agent is a + service and a service has had no visible desktop since Vista. + `FOG_CLIENT_AUTOLOGOFF_BGIMAGE` is removed; nothing has read it since the + legacy client stopped shipping. + - **The `site` plugin's own tables are retired** once its data has moved into core's `sites` table (see the one-way-upgrade warning above). Custom tooling querying the old plugin tables directly must repoint at the core tables. If @@ -312,7 +342,7 @@ pre-upgrade *dump* — it is not a schema rollback. - **The group page's push-to-all controls are gone.** Setting an image, kernel, kernel arguments, init, primary disk, product key, BIOS/EFI exit - type, AD details, printer level, screen resolution, auto-logout or hostname + type, AD details, printer level, auto-logout or hostname enforcement from a group applied the value **once**, to whichever hosts were members at that moment. A host added afterward did not get it; a host removed kept it. That was always true and was never visible. @@ -414,8 +444,8 @@ pre-upgrade *dump* — it is not a schema rollback. The fields it covers are the ones the group page pushed: image, kernel, kernel arguments, primary disk, init, BIOS and EFI exit type, product key, - printer management level, the AD settings, hostname enforcement, auto-logout - and screen resolution. + printer management level, the AD settings, hostname enforcement and + auto-logout. **For plugin authors:** `HOST_MASSEDIT_FIELDS` and `HOST_MASSEDIT_APPLY` let a plugin put its own field in that form with the same three states, and diff --git a/packages/web/commons/schema-constraints.php b/packages/web/commons/schema-constraints.php index 1a4c51ec30..9c310745a9 100644 --- a/packages/web/commons/schema-constraints.php +++ b/packages/web/commons/schema-constraints.php @@ -103,7 +103,6 @@ // ---- satellite: rows wholly owned by one parent ---------------------- ['child' => 'inventory', 'column' => 'iHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], - ['child' => 'hostScreenSettings', 'column' => 'hssHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'hostAutoLogOut', 'column' => 'haloHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'powerManagement', 'column' => 'pmHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], ['child' => 'greenFog', 'column' => 'gfHostID', 'parent' => 'hosts', 'pcolumn' => 'hostID', 'class' => 'satellite', 'action' => 'CASCADE', 'enabled' => true, 'group' => 1], diff --git a/packages/web/commons/schema-expected.php b/packages/web/commons/schema-expected.php index d9e13d5960..105ad8aeea 100644 --- a/packages/web/commons/schema-expected.php +++ b/packages/web/commons/schema-expected.php @@ -72,6 +72,10 @@ 'table' => 'imagingLog', 'reason' => 'ADR 0022 decision 3 -- taskLog records an imaging run now, so the table was retired rather than ported', ], + [ + 'table' => 'hostScreenSettings', + 'reason' => 'design 0014 -- Display Manager is removed, so the per-host width, height and refresh have nothing that reads or applies them. Schema step 431 drops the table with the module', + ], [ 'table' => 'printerAssoc', 'column' => 'paAnon1', @@ -526,19 +530,6 @@ 'hostAgentCheckin' => 'datetime DEFAULT NULL', ], ], - 'hostScreenSettings' => [ - 'create' => 'CREATE TABLE IF NOT EXISTS `hostScreenSettings` ( `hssID` int(11) NOT NULL AUTO_INCREMENT, `hssHostID` int(11) NOT NULL, `hssWidth` int(11) NOT NULL DEFAULT 0, `hssHeight` int(11) NOT NULL DEFAULT 0, `hssRefresh` int(11) NOT NULL DEFAULT 0, `hssOrientation` int(11) NOT NULL DEFAULT 0, `hssOther1` int(11) NOT NULL DEFAULT 0, `hssOther2` int(11) NOT NULL DEFAULT 0, PRIMARY KEY (`hssID`), UNIQUE KEY `hssHostID` (`hssHostID`), KEY `new_index` (`hssHostID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', - 'columns' => [ - 'hssID' => 'int(11) NOT NULL', - 'hssHostID' => 'int(11) NOT NULL', - 'hssWidth' => 'int(11) NOT NULL DEFAULT 0', - 'hssHeight' => 'int(11) NOT NULL DEFAULT 0', - 'hssRefresh' => 'int(11) NOT NULL DEFAULT 0', - 'hssOrientation' => 'int(11) NOT NULL DEFAULT 0', - 'hssOther1' => 'int(11) NOT NULL DEFAULT 0', - 'hssOther2' => 'int(11) NOT NULL DEFAULT 0', - ], - ], 'hostSoftware' => [ 'create' => 'CREATE TABLE IF NOT EXISTS `hostSoftware` ( `hsID` int(11) NOT NULL AUTO_INCREMENT, `hsHostID` int(11) NOT NULL, `hsName` varchar(255) NOT NULL, `hsVersion` varchar(128) NOT NULL DEFAULT \'\', `hsPublisher` varchar(255) NOT NULL DEFAULT \'\', `hsSource` varchar(16) NOT NULL DEFAULT \'\', `hsArch` varchar(16) NOT NULL DEFAULT \'\', `hsInstallDate` date DEFAULT NULL, `hsFirstSeen` datetime DEFAULT NULL, `hsLastSeen` datetime DEFAULT NULL, `hsRemovedAt` datetime DEFAULT NULL, PRIMARY KEY (`hsID`), UNIQUE KEY `hsHostNameSrcVer` (`hsHostID`,`hsName`,`hsSource`,`hsVersion`), KEY `hsName` (`hsName`), KEY `hsHostRemoved` (`hsHostID`,`hsRemovedAt`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_general_ci ROW_FORMAT=DYNAMIC', 'columns' => [ diff --git a/packages/web/commons/schema.php b/packages/web/commons/schema.php index 0a4d9ab32e..f62f8cb184 100644 --- a/packages/web/commons/schema.php +++ b/packages/web/commons/schema.php @@ -11479,3 +11479,87 @@ function () { . "storage node on them, which cannot be woken otherwise. Off by " . "default. (Valid values: 0 or 1).','0','FOG Agent')", ]; + +// 431 +$this->schema[] = [ + // Display Manager goes, and its table with it. + // + // The module reset a client's screen resolution to a fixed size at + // logoff and at startup. That was a reasonable thing for a lab in 2010 + // and it is the wrong layer now: Windows has honored the per-monitor + // EDID-preferred mode by itself for a decade, a fixed resolution pushed + // over the top of it is wrong on every machine whose panel is not the + // size the setting names, and a laptop that docks changes its answer + // twice a day. Nothing in the rebuilt agent implements it and nothing + // will; the answer to "my screens are wrong" is not a FOG setting. + // + // This is the greenfog removal (step 375) repeated with one difference: + // Display Manager owns a table, and the table goes too. + // + // That is a deliberate, irreversible loss of the per-host `hssWidth`, + // `hssHeight` and `hssRefresh` an admin configured. Keeping the rows + // was considered and rejected. Every consumer is removed in this same + // commit -- the client endpoint, the host card, the mass-edit field, + // Host::getDispVals()/setDisp(), Group::setDisp() -- so keeping the + // table would mean keeping `HostScreenSetting`, its manager, its + // `hostscreensetting` REST route, its Authorization mapping and its + // foreign key alive to serve data that nothing writes and nothing + // honors. Step 375 named that failure exactly: a setting that lies is + // worse than no setting, and an API route reporting a resolution the + // fleet does not apply is a setting that lies. + // + // Ordered so nothing ever references something already gone: the + // per-host module answers first, then the module row, then the four + // globalSettings rows, then the table. msModuleID is a VARCHAR and step + // 34 seeded these with the short name before a later step rewrote them + // to the numeric id, so a server upgraded across that boundary can hold + // either spelling and both are matched -- the same care step 375 took. + // + // The seed steps that created all of this are deliberately NOT edited. + // schema.php is a replay log; step 326 set that precedent and step 375 + // followed it. A fresh install creates these and removes them one step + // later, which costs nothing and keeps the history readable. + "DELETE FROM `moduleStatusByHost` " + . "WHERE `msModuleID` IN ('3', 'displaymanager')", + "DELETE FROM `modules` WHERE `short_name` = 'displaymanager'", + "DELETE FROM `globalSettings` WHERE `settingKey` IN (" + . "'FOG_CLIENT_DISPLAYMANAGER_ENABLED'," + . "'FOG_CLIENT_DISPLAYMANAGER_X'," + . "'FOG_CLIENT_DISPLAYMANAGER_Y'," + . "'FOG_CLIENT_DISPLAYMANAGER_R')", + Schema::dropTable('hostScreenSettings'), +]; + +// 432 +$this->schema[] = [ + // Auto Log Out gets a configurable warning, and loses a setting that + // has never done anything. + // + // FOG_CLIENT_AUTOLOGOFF_WARN is how long the user is told before they + // are logged off. The legacy .NET client baked that countdown in; the + // rebuilt agent (design 0014) takes it from here, and 0 means log the + // user off with no warning at all -- legal, and what a kiosk wants. + // Sixty seconds is the default because it is long enough to notice and + // short enough that the machine is actually freed. + // + // FOG_CLIENT_AUTOLOGOFF_BGIMAGE goes. It named the 300x300 background + // of the .NET client's countdown window, and there is no countdown + // window any more: the agent is a service in session 0, which has had + // no visible desktop since Vista, so it warns through WTSSendMessage -- + // rendered by winlogon inside the user's own session, and not something + // an image can be attached to. Nothing has read this setting since the + // legacy client stopped shipping, and the FOG Configuration page never + // rendered it, so it is a row that promises a thing FOG cannot do. That + // is the same defect step 375 removed FOG_CLIENT_GREENFOG_ENABLED for + // and step 326 removed FOG_PLUGINSYS_DIR for: a setting that lies is + // worse than no setting. + "INSERT IGNORE INTO `globalSettings` " + . "(`settingKey`,`settingDesc`,`settingValue`,`settingCategory`) VALUES " + . "('FOG_CLIENT_AUTOLOGOFF_WARN','This setting defines how many seconds " + . "before an automatic log out the user is warned. 0 logs the user out " + . "with no warning. The warning is a message box shown in the users own " + . "session; moving the mouse or pressing a key cancels the log out.'," + . "'60','FOG Client - Auto Log Off')", + "DELETE FROM `globalSettings` " + . "WHERE `settingKey` = 'FOG_CLIENT_AUTOLOGOFF_BGIMAGE'", +]; diff --git a/packages/web/management/js/fog/host/fog.host.edit.js b/packages/web/management/js/fog/host/fog.host.edit.js index ef2bfde94e..14de7dafa9 100644 --- a/packages/web/management/js/fog/host/fog.host.edit.js +++ b/packages/web/management/js/fog/host/fog.host.edit.js @@ -1127,49 +1127,6 @@ } }); - // Display manager area - var hostModuleDisplaymanBtn = $('#host-displayman-send'), - hostModuleDisplayForm = $('#host-displayman-form'); - - function disableModuleDisplayButtons(disable) { - hostModuleDisplaymanBtn.prop('disabled', disable); - } - - hostModuleDisplayForm.on('submit', function(e) { - e.preventDefault(); - }); - - hostModuleDisplaymanBtn.on('click', function(e) { - e.preventDefault(); - var method = $(this).attr('method'), - action = $(this).attr('action'), - opts = { - confirmdisplaysend: 1, - x: $('#x').val(), - y: $('#y').val(), - r: $('#r').val() - }; - disableModuleDisplayButtons(true); - $.apiCall(method,action,opts,function(err) { - disableModuleDisplayButtons(false); - if (err) { - return; - } - var url = '../management/index.php?node=' - + Common.node - + '&sub=getHostDisplayManVals' - + '&id=' - + Common.id; - Pace.ignore(function() { - $.get(url, function(data) { - $('#x').val(data.x); - $('#y').val(data.y); - $('#r').val(data.r); - }, 'json'); - }); - }); - }); - // Auto log out area var hostModuleAloBtn = $('#host-alo-send'), hostModuleAloForm = $('#host-alo-form'); diff --git a/packages/web/management/js/fog/service/fog.service.list.js b/packages/web/management/js/fog/service/fog.service.list.js index 9a7d0027d4..7cf8e32b0d 100644 --- a/packages/web/management/js/fog/service/fog.service.list.js +++ b/packages/web/management/js/fog/service/fog.service.list.js @@ -6,7 +6,6 @@ // is needed here -- which also removes the old click/submit inconsistency // that left the user-tracker form wired differently from the rest. var services = [ - {btn: '#displaymanager-update', form: '#displaymanagerupdate-form'}, {btn: '#autologout-update', form: '#autologoutupdate-form'}, {btn: '#snapinclient-update', form: '#snapinclientupdate-form'}, {btn: '#hostregister-update', form: '#hostregisterupdate-form'}, diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 1a05730791..65cdd68371 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -319,6 +319,9 @@ msgstr "Kernel-Argumente" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 Stunde" @@ -2601,15 +2604,6 @@ msgstr "Standard" msgid "Default Choice" msgstr "Standard-Eintrag:" -msgid "Default Height" -msgstr "Standardhöhe" - -msgid "Default Refresh Rate" -msgstr "Standard-Bildwiederholrate" - -msgid "Default Width" -msgstr "Standardbreite" - #, fuzzy msgid "Default init, ARM64" msgstr "Standardbreite" @@ -4079,10 +4073,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Mitternacht" - msgid "Height must be 120 pixels." msgstr "Höhe muss 120 Pixel betragen" @@ -4193,10 +4183,6 @@ msgstr "Standard-Eintrag:" msgid "Host Description" msgstr "Host-Beschreibung" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "iPXE Menüeinstellungen" - msgid "Host EFI Exit Type" msgstr "Host-EFI-Exit-Typ" @@ -4301,10 +4287,6 @@ msgstr "Host-Produkt-Schlüssel" msgid "Host Registration" msgstr "Host-Registrierung" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Host-Registrierung" - #, fuzzy msgid "Host Snapin Associations" msgstr "zugeordneter Host" @@ -7952,10 +7934,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Standard-Bildwiederholrate" - #, fuzzy msgid "Refresh Settings Cache" msgstr "Service-Status" @@ -8478,16 +8456,6 @@ msgstr "iPXE-Einstellungen erfolgreich aktualisiert!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Standard-Bildwiederholrate" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Suche" @@ -10416,7 +10384,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11534,6 +11501,9 @@ msgstr "Wake On Lan" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11636,9 +11606,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "Breite muss 650 Pixel betragen" @@ -12274,9 +12241,6 @@ msgstr "" msgid "in" msgstr "Minuten" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12284,9 +12248,6 @@ msgstr "" msgid "in minutes" msgstr "Minuten" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "in Sekunden" @@ -13087,6 +13048,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "Pfad ist nicht verfügbar" +#~ msgid "Default Height" +#~ msgstr "Standardhöhe" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Standard-Bildwiederholrate" + +#~ msgid "Default Width" +#~ msgstr "Standardbreite" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Löschen fehlgeschlagen" @@ -13286,10 +13256,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "Gruppeneinstellungen Module" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Mitternacht" + #, fuzzy #~ msgid "History Report" #~ msgstr "Verlaufs-ID" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "iPXE Menüeinstellungen" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Hostliste" @@ -13306,6 +13284,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "Aktualisierung der Rolle fehlgeschlagen" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Host-Registrierung" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Hostliste" @@ -13547,6 +13529,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "(empfohlen)" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Standard-Bildwiederholrate" + #~ msgid "Register must be managed from hooks or events" #~ msgstr "Register muss von Haken oder Ereignissen gemanagt werden" @@ -13622,6 +13608,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "Drucker-Update fehlgeschlagen!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Standard-Bildwiederholrate" + #, fuzzy #~ msgid "Serial" #~ msgstr "Seriennummer" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 070edf0a87..68678da903 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -324,6 +324,9 @@ msgstr "Kernel Arguments" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 hour" @@ -2604,15 +2607,6 @@ msgstr "Default" msgid "Default Choice" msgstr "Default Item:" -msgid "Default Height" -msgstr "Default Height" - -msgid "Default Refresh Rate" -msgstr "Default Refresh Rate" - -msgid "Default Width" -msgstr "Default Width" - #, fuzzy msgid "Default init, ARM64" msgstr "Default Width" @@ -4081,10 +4075,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Midnight" - msgid "Height must be 120 pixels." msgstr "" @@ -4195,10 +4185,6 @@ msgstr "Default Item:" msgid "Host Description" msgstr "Host Description" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "Settings" - msgid "Host EFI Exit Type" msgstr "Host EFI Exit Type" @@ -4303,10 +4289,6 @@ msgstr "Host Product Key" msgid "Host Registration" msgstr "Host Registration" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Host Registration" - #, fuzzy msgid "Host Snapin Associations" msgstr "No node associated" @@ -7963,10 +7945,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Default Refresh Rate" - #, fuzzy msgid "Refresh Settings Cache" msgstr "Service Status" @@ -8489,16 +8467,6 @@ msgstr "Install / Update Successful!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Default Refresh Rate" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Search" @@ -10425,7 +10393,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11542,6 +11509,9 @@ msgstr "Wake on lan?" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11643,9 +11613,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -12280,9 +12247,6 @@ msgstr "" msgid "in" msgstr "minutes" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12290,9 +12254,6 @@ msgstr "" msgid "in minutes" msgstr "minutes" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" @@ -13084,6 +13045,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "No hash available" +#~ msgid "Default Height" +#~ msgstr "Default Height" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Default Refresh Rate" + +#~ msgid "Default Width" +#~ msgstr "Default Width" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Delete file data" @@ -13277,10 +13247,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "Update Settings" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Midnight" + #, fuzzy #~ msgid "History Report" #~ msgstr "Host ID" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "Settings" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Host List" @@ -13297,6 +13275,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "User update failed" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Host Registration" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Host List" @@ -13527,6 +13509,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "Current Records" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Default Refresh Rate" + #, fuzzy #~ msgid "Release Version" #~ msgstr "Latest Version" @@ -13593,6 +13579,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "Printer update failed!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Default Refresh Rate" + #, fuzzy #~ msgid "Serial" #~ msgstr "System Serial" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 6c1c93fc98..cd30d0ba82 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -322,6 +322,9 @@ msgstr "Argumentos grupo Kernel" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 hora" @@ -2628,15 +2631,6 @@ msgstr "Defecto" msgid "Default Choice" msgstr "Tema por defecto:" -msgid "Default Height" -msgstr "Altura por defecto" - -msgid "Default Refresh Rate" -msgstr "Frecuencia de actualización predeterminado" - -msgid "Default Width" -msgstr "Ancho por defecto" - #, fuzzy msgid "Default init, ARM64" msgstr "Ancho por defecto" @@ -4130,10 +4124,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Medianoche" - msgid "Height must be 120 pixels." msgstr "" @@ -4246,10 +4236,6 @@ msgstr "Tema por defecto:" msgid "Host Description" msgstr "Descripción del Grupo" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "Configuración Tecla" - #, fuzzy msgid "Host EFI Exit Type" msgstr "Grupo EFI Tipo de salida" @@ -4365,10 +4351,6 @@ msgstr "Clave del producto Grupo" msgid "Host Registration" msgstr "Lista de host" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Lista de host" - #, fuzzy msgid "Host Snapin Associations" msgstr "No nodo asociado" @@ -8082,10 +8064,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Frecuencia de actualización predeterminado" - #, fuzzy msgid "Refresh Settings Cache" msgstr "Estado del servicio" @@ -8613,16 +8591,6 @@ msgstr "actualización de servicio no pudo" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Frecuencia de actualización predeterminado" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Buscar" @@ -10583,7 +10551,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11703,6 +11670,9 @@ msgstr "¿Activación de la LAN?" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11803,9 +11773,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -12442,9 +12409,6 @@ msgstr "" msgid "in" msgstr "minutos" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12452,9 +12416,6 @@ msgstr "" msgid "in minutes" msgstr "minutos" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" @@ -13238,6 +13199,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "No se dispone de hash" +#~ msgid "Default Height" +#~ msgstr "Altura por defecto" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Frecuencia de actualización predeterminado" + +#~ msgid "Default Width" +#~ msgstr "Ancho por defecto" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Eliminar los datos del archivo" @@ -13437,10 +13407,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "Descripción del Grupo" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Medianoche" + #, fuzzy #~ msgid "History Report" #~ msgstr "ID de host" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "Configuración Tecla" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Hospedadores" @@ -13457,6 +13435,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "actualización Valoración falló" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Lista de host" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Hospedadores" @@ -13686,6 +13668,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "Registros actuales" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Frecuencia de actualización predeterminado" + #, fuzzy #~ msgid "Release Version" #~ msgstr "Versión del sistema" @@ -13749,6 +13735,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "actualización de la impresora ha fallado!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Frecuencia de actualización predeterminado" + #, fuzzy #~ msgid "Serial" #~ msgstr "sistema de serie" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index 634d05e1fe..db17348d4e 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -319,6 +319,9 @@ msgstr "Kernel-Argumente" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 Stunde" @@ -2601,15 +2604,6 @@ msgstr "Standard" msgid "Default Choice" msgstr "Standard-Eintrag:" -msgid "Default Height" -msgstr "Standardhöhe" - -msgid "Default Refresh Rate" -msgstr "Standard-Bildwiederholrate" - -msgid "Default Width" -msgstr "Standardbreite" - #, fuzzy msgid "Default init, ARM64" msgstr "Standardbreite" @@ -4079,10 +4073,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Mitternacht" - msgid "Height must be 120 pixels." msgstr "Höhe muss 120 Pixel betragen" @@ -4193,10 +4183,6 @@ msgstr "Standard-Eintrag:" msgid "Host Description" msgstr "Host-Beschreibung" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "iPXE Menüeinstellungen" - msgid "Host EFI Exit Type" msgstr "Host-EFI-Exit-Typ" @@ -4301,10 +4287,6 @@ msgstr "Host-Produkt-Schlüssel" msgid "Host Registration" msgstr "Host-Registrierung" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Host-Registrierung" - #, fuzzy msgid "Host Snapin Associations" msgstr "zugeordneter Host" @@ -7953,10 +7935,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Standard-Bildwiederholrate" - #, fuzzy msgid "Refresh Settings Cache" msgstr "Service-Status" @@ -8479,16 +8457,6 @@ msgstr "iPXE-Einstellungen erfolgreich aktualisiert!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Standard-Bildwiederholrate" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Suche" @@ -10417,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11535,6 +11502,9 @@ msgstr "Wake On Lan" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11637,9 +11607,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "Breite muss 650 Pixel betragen" @@ -12275,9 +12242,6 @@ msgstr "" msgid "in" msgstr "Minuten" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12285,9 +12249,6 @@ msgstr "" msgid "in minutes" msgstr "Minuten" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "in Sekunden" @@ -13088,6 +13049,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "Pfad ist nicht verfügbar" +#~ msgid "Default Height" +#~ msgstr "Standardhöhe" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Standard-Bildwiederholrate" + +#~ msgid "Default Width" +#~ msgstr "Standardbreite" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Löschen fehlgeschlagen" @@ -13287,10 +13257,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "Gruppeneinstellungen Module" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Mitternacht" + #, fuzzy #~ msgid "History Report" #~ msgstr "Verlaufs-ID" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "iPXE Menüeinstellungen" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Hostliste" @@ -13307,6 +13285,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "Aktualisierung der Rolle fehlgeschlagen" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Host-Registrierung" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Hostliste" @@ -13548,6 +13530,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "(empfohlen)" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Standard-Bildwiederholrate" + #~ msgid "Register must be managed from hooks or events" #~ msgstr "Register muss von Haken oder Ereignissen gemanagt werden" @@ -13623,6 +13609,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "Drucker-Update fehlgeschlagen!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Standard-Bildwiederholrate" + #, fuzzy #~ msgid "Serial" #~ msgstr "Seriennummer" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index e0a424abfb..df040c4a15 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -325,6 +325,9 @@ msgstr "Arguments du noyau" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 heure" @@ -2604,15 +2607,6 @@ msgstr "Défaut" msgid "Default Choice" msgstr "Point par défaut:" -msgid "Default Height" -msgstr "Hauteur par défaut" - -msgid "Default Refresh Rate" -msgstr "Par défaut Taux de rafraîchissement" - -msgid "Default Width" -msgstr "Par défaut Largeur" - #, fuzzy msgid "Default init, ARM64" msgstr "Par défaut Largeur" @@ -4081,10 +4075,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Minuit" - msgid "Height must be 120 pixels." msgstr "" @@ -4195,10 +4185,6 @@ msgstr "Point par défaut:" msgid "Host Description" msgstr "hôte description de" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "Paramètres" - msgid "Host EFI Exit Type" msgstr "Hôte EFI Type de sortie" @@ -4303,10 +4289,6 @@ msgstr "Hôte clé de produit" msgid "Host Registration" msgstr "Enregistrement de l'hôte" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Enregistrement de l'hôte" - #, fuzzy msgid "Host Snapin Associations" msgstr "Aucun noeud associé" @@ -7948,10 +7930,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Par défaut Taux de rafraîchissement" - #, fuzzy msgid "Refresh Settings Cache" msgstr "État du service" @@ -8473,16 +8451,6 @@ msgstr "Installation / Mise à jour réussie!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Par défaut Taux de rafraîchissement" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Chercher" @@ -10409,7 +10377,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11527,6 +11494,9 @@ msgstr "Wake on lan?" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11628,9 +11598,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -12265,9 +12232,6 @@ msgstr "" msgid "in" msgstr "minutes" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12275,9 +12239,6 @@ msgstr "" msgid "in minutes" msgstr "minutes" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" @@ -13069,6 +13030,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "Pas de hachage disponible" +#~ msgid "Default Height" +#~ msgstr "Hauteur par défaut" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Par défaut Taux de rafraîchissement" + +#~ msgid "Default Width" +#~ msgstr "Par défaut Largeur" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Supprimer les données de fichiers" @@ -13262,10 +13232,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "hôte description de" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Minuit" + #, fuzzy #~ msgid "History Report" #~ msgstr "ID d'hôte" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "Paramètres" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Liste des hôtes" @@ -13282,6 +13260,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "mise à jour de l'utilisateur a échoué" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Enregistrement de l'hôte" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Liste des hôtes" @@ -13516,6 +13498,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "Enregistrements courants" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Par défaut Taux de rafraîchissement" + #, fuzzy #~ msgid "Release Version" #~ msgstr "Dernière version" @@ -13582,6 +13568,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "mise à jour de l'imprimante a échoué!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Par défaut Taux de rafraîchissement" + #, fuzzy #~ msgid "Serial" #~ msgstr "Serial System" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 053a87d8f8..381b625ed6 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -324,6 +324,9 @@ msgstr "argomenti del kernel" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + msgid "1 Hour" msgstr "1 ora" @@ -2545,15 +2548,6 @@ msgstr "Predefinito" msgid "Default Choice" msgstr "Elemento predefinito:" -msgid "Default Height" -msgstr "Altezza di default" - -msgid "Default Refresh Rate" -msgstr "Predefinito: frequenza aggiornamento" - -msgid "Default Width" -msgstr "Larghezza predefinita" - #, fuzzy msgid "Default init, ARM64" msgstr "Larghezza predefinita" @@ -3988,10 +3982,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "Mezzanotte" - msgid "Height must be 120 pixels." msgstr "Altezza deve essere di 120 pixel." @@ -4100,10 +4090,6 @@ msgstr "Elemento predefinito:" msgid "Host Description" msgstr "Host Descrizione" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "Impostazioni del menu iPXE" - msgid "Host EFI Exit Type" msgstr "Host EFI Tipo Exit" @@ -4206,10 +4192,6 @@ msgstr "Tasti del prodotto host" msgid "Host Registration" msgstr "registrazione Host" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "registrazione Host" - #, fuzzy msgid "Host Snapin Associations" msgstr "Host associato" @@ -7744,10 +7726,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Predefinito: frequenza aggiornamento" - #, fuzzy msgid "Refresh Settings Cache" msgstr "Stato servizio" @@ -8258,16 +8236,6 @@ msgstr "Impostazioni iPXE aggiornate correttamente!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Predefinito: frequenza aggiornamento" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Ricerca" @@ -10132,7 +10100,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11219,6 +11186,9 @@ msgstr "Wake On LAN" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11318,9 +11288,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "Larghezza deve essere di 650 pixel." @@ -11930,9 +11897,6 @@ msgstr "" msgid "in" msgstr "minuti" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -11940,9 +11904,6 @@ msgstr "" msgid "in minutes" msgstr "minuti" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "in secondi" @@ -12711,6 +12672,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "Percorso non disponibile" +#~ msgid "Default Height" +#~ msgstr "Altezza di default" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Predefinito: frequenza aggiornamento" + +#~ msgid "Default Width" +#~ msgstr "Larghezza predefinita" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Cancellare i file" @@ -12907,10 +12877,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "Impostazioni modulo del gruppo" +#, fuzzy +#~ msgid "Height" +#~ msgstr "Mezzanotte" + #, fuzzy #~ msgid "History Report" #~ msgstr "Grafico storico" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "Impostazioni del menu iPXE" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Lista Host" @@ -12927,6 +12905,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "Aggiornamento ruolo non riuscito" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "registrazione Host" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Lista Host" @@ -13162,6 +13144,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "Consigliato" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Predefinito: frequenza aggiornamento" + #~ msgid "Register must be managed from hooks or events" #~ msgstr "Registro deve essere gestito da hook o eventi" @@ -13234,6 +13220,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "Aggiornamento della stampante non è riuscito!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Predefinito: frequenza aggiornamento" + #, fuzzy #~ msgid "Serial" #~ msgstr "Sistema seriale" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 089143db3b..241ae46a64 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -316,6 +316,9 @@ msgstr "カーネル引数" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + msgid "1 Hour" msgstr "1時間" @@ -2532,15 +2535,6 @@ msgstr "既定" msgid "Default Choice" msgstr "既定の項目" -msgid "Default Height" -msgstr "既定の高さ" - -msgid "Default Refresh Rate" -msgstr "既定の更新レート" - -msgid "Default Width" -msgstr "既定の幅" - #, fuzzy msgid "Default init, ARM64" msgstr "既定の幅" @@ -3967,10 +3961,6 @@ msgstr "ホストはアクセス用にロックされていません" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "午前 0 時" - msgid "Height must be 120 pixels." msgstr "高さは 120 ピクセルである必要があります。" @@ -4079,10 +4069,6 @@ msgstr "既定のプリンターを更新" msgid "Host Description" msgstr "ホストの説明" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "ホストモジュール設定" - msgid "Host EFI Exit Type" msgstr "ホスト EFI 終了方法" @@ -4183,9 +4169,6 @@ msgstr "ホスト プロダクトキー" msgid "Host Registration" msgstr "ホスト登録" -msgid "Host Screen Resolution" -msgstr "ホスト画面解像度" - #, fuzzy msgid "Host Snapin Associations" msgstr "スナップインロケーション" @@ -7724,9 +7707,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -msgid "Refresh" -msgstr "" - msgid "Refresh Settings Cache" msgstr "" @@ -8229,18 +8209,6 @@ msgstr "iPXE 設定を更新しました!" msgid "Scopes" msgstr "" -#, fuzzy -msgid "Screen Height" -msgstr "画面の高さ(ピクセル)" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "画面のリフレッシュレート(Hz)" - -#, fuzzy -msgid "Screen Width" -msgstr "画面の幅(ピクセル)" - msgid "Search" msgstr "検索" @@ -10089,7 +10057,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11175,6 +11142,9 @@ msgstr "Wake on LAN" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11274,9 +11244,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "幅は 650 ピクセルである必要があります。" @@ -11887,9 +11854,6 @@ msgstr "" msgid "in" msgstr "分" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -11897,9 +11861,6 @@ msgstr "" msgid "in minutes" msgstr "分" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "秒単位" @@ -12867,6 +12828,15 @@ msgstr "" #~ msgid "Date of checkout" #~ msgstr "貸出日" +#~ msgid "Default Height" +#~ msgstr "既定の高さ" + +#~ msgid "Default Refresh Rate" +#~ msgstr "既定の更新レート" + +#~ msgid "Default Width" +#~ msgstr "既定の幅" + #~ msgid "Delayed Start" #~ msgstr "遅延開始" @@ -13304,6 +13274,10 @@ msgstr "" #~ msgid "HD Serial" #~ msgstr "HD シリアル" +#, fuzzy +#~ msgid "Height" +#~ msgstr "午前 0 時" + #~ msgid "Hide Menu" #~ msgstr "メニューを非表示" @@ -13346,6 +13320,10 @@ msgstr "" #~ msgid "Host Desc" #~ msgstr "ホスト説明" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "ホストモジュール設定" + #~ msgid "Host FOG Client Module configuration" #~ msgstr "ホスト FOG クライアントモジュール設定" @@ -13367,6 +13345,9 @@ msgstr "" #~ msgid "Host Printers" #~ msgstr "ホスト プリンター" +#~ msgid "Host Screen Resolution" +#~ msgstr "ホスト画面解像度" + #~ msgid "Host Site" #~ msgstr "ホスト サイト" @@ -14248,6 +14229,18 @@ msgstr "" #~ msgid "Schedule with shutdown" #~ msgstr "シャットダウンを含めてスケジュール" +#, fuzzy +#~ msgid "Screen Height" +#~ msgstr "画面の高さ(ピクセル)" + +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "画面のリフレッシュレート(Hz)" + +#, fuzzy +#~ msgid "Screen Width" +#~ msgstr "画面の幅(ピクセル)" + #~ msgid "Search pattern" #~ msgstr "検索パターン" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 54b04f34a3..045e80fba0 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -300,6 +300,9 @@ msgstr "" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + msgid "1 Hour" msgstr "" @@ -2250,15 +2253,6 @@ msgstr "" msgid "Default Choice" msgstr "" -msgid "Default Height" -msgstr "" - -msgid "Default Refresh Rate" -msgstr "" - -msgid "Default Width" -msgstr "" - msgid "Default init, ARM64" msgstr "" @@ -3519,9 +3513,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -msgid "Height" -msgstr "" - msgid "Height must be 120 pixels." msgstr "" @@ -3612,9 +3603,6 @@ msgstr "" msgid "Host Description" msgstr "" -msgid "Host Display Manager Settings" -msgstr "" - msgid "Host EFI Exit Type" msgstr "" @@ -3702,9 +3690,6 @@ msgstr "" msgid "Host Registration" msgstr "" -msgid "Host Screen Resolution" -msgstr "" - msgid "Host Snapin Associations" msgstr "" @@ -6817,9 +6802,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -msgid "Refresh" -msgstr "" - msgid "Refresh Settings Cache" msgstr "" @@ -7273,15 +7255,6 @@ msgstr "" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -msgid "Screen Refresh Rate" -msgstr "" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "" @@ -8929,7 +8902,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -9914,6 +9886,9 @@ msgstr "" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -10010,9 +9985,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -10566,18 +10538,12 @@ msgstr "" msgid "in" msgstr "" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" msgid "in minutes" msgstr "" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 77fe001f03..ce911ede1f 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -324,6 +324,9 @@ msgstr "Argumentos de kernel" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1 hora" @@ -2604,15 +2607,6 @@ msgstr "Padrão" msgid "Default Choice" msgstr "Item padrão:" -msgid "Default Height" -msgstr "padrão Altura" - -msgid "Default Refresh Rate" -msgstr "Padrão Refresh Rate" - -msgid "Default Width" -msgstr "Largura padrão" - #, fuzzy msgid "Default init, ARM64" msgstr "Largura padrão" @@ -4081,10 +4075,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "meia-noite" - msgid "Height must be 120 pixels." msgstr "" @@ -4195,10 +4185,6 @@ msgstr "Item padrão:" msgid "Host Description" msgstr "anfitrião Descrição" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "Configurações" - msgid "Host EFI Exit Type" msgstr "Hospedar EFI Tipo Exit" @@ -4303,10 +4289,6 @@ msgstr "Hospedar de Chave de Produto" msgid "Host Registration" msgstr "Registro de acolhimento" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "Registro de acolhimento" - #, fuzzy msgid "Host Snapin Associations" msgstr "No nó associado" @@ -7950,10 +7932,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "Padrão Refresh Rate" - #, fuzzy msgid "Refresh Settings Cache" msgstr "status do serviço" @@ -8476,16 +8454,6 @@ msgstr "Instalar / Atualizar sucesso!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "Padrão Refresh Rate" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "Pesquisa" @@ -10412,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11530,6 +11497,9 @@ msgstr "Wake on LAN?" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11631,9 +11601,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -12268,9 +12235,6 @@ msgstr "" msgid "in" msgstr "minutos" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12278,9 +12242,6 @@ msgstr "" msgid "in minutes" msgstr "minutos" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" @@ -13072,6 +13033,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "Nenhum de hash disponíveis" +#~ msgid "Default Height" +#~ msgstr "padrão Altura" + +#~ msgid "Default Refresh Rate" +#~ msgstr "Padrão Refresh Rate" + +#~ msgid "Default Width" +#~ msgstr "Largura padrão" + #, fuzzy #~ msgid "Delete All" #~ msgstr "Apagar dados de arquivo" @@ -13265,10 +13235,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "anfitrião Descrição" +#, fuzzy +#~ msgid "Height" +#~ msgstr "meia-noite" + #, fuzzy #~ msgid "History Report" #~ msgstr "ID de acolhimento" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "Configurações" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "Lista de Host" @@ -13285,6 +13263,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "atualização do utilizador falhou" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "Registro de acolhimento" + #, fuzzy #~ msgid "Host Site" #~ msgstr "Lista de Host" @@ -13519,6 +13501,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "Registros atuais" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "Padrão Refresh Rate" + #, fuzzy #~ msgid "Release Version" #~ msgstr "Última versão" @@ -13585,6 +13571,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "atualização da impressora falhou!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "Padrão Refresh Rate" + #, fuzzy #~ msgid "Serial" #~ msgstr "Serial sistema" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index c014fbef54..19f3d8f338 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -324,6 +324,9 @@ msgstr "内核参数" msgid "0 is a normal snapin, 1 a snapin pack the client extracts before running." msgstr "" +msgid "0 logs the user out with no warning" +msgstr "" + #, fuzzy msgid "1 Hour" msgstr "1小时" @@ -2604,15 +2607,6 @@ msgstr "默认" msgid "Default Choice" msgstr "默认项:" -msgid "Default Height" -msgstr "默认高度" - -msgid "Default Refresh Rate" -msgstr "默认刷新频率" - -msgid "Default Width" -msgstr "默认宽度" - #, fuzzy msgid "Default init, ARM64" msgstr "默认宽度" @@ -4081,10 +4075,6 @@ msgstr "" msgid "Header is missing the required \"%s\" column" msgstr "" -#, fuzzy -msgid "Height" -msgstr "午夜" - msgid "Height must be 120 pixels." msgstr "" @@ -4195,10 +4185,6 @@ msgstr "默认项:" msgid "Host Description" msgstr "主机描述" -#, fuzzy -msgid "Host Display Manager Settings" -msgstr "设置" - msgid "Host EFI Exit Type" msgstr "主持人EFI退出类型" @@ -4303,10 +4289,6 @@ msgstr "主机产品密钥" msgid "Host Registration" msgstr "主机注册" -#, fuzzy -msgid "Host Screen Resolution" -msgstr "主机注册" - #, fuzzy msgid "Host Snapin Associations" msgstr "无关联的节点" @@ -7950,10 +7932,6 @@ msgstr "" msgid "Redirect URI" msgstr "" -#, fuzzy -msgid "Refresh" -msgstr "默认刷新频率" - #, fuzzy msgid "Refresh Settings Cache" msgstr "服务状态" @@ -8476,16 +8454,6 @@ msgstr "安装/升级成功!" msgid "Scopes" msgstr "" -msgid "Screen Height" -msgstr "" - -#, fuzzy -msgid "Screen Refresh Rate" -msgstr "默认刷新频率" - -msgid "Screen Width" -msgstr "" - msgid "Search" msgstr "搜索" @@ -10412,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -11530,6 +11497,9 @@ msgstr "网络唤醒?" msgid "Wake Up" msgstr "" +msgid "Warning Before Log Out" +msgstr "" + msgid "Warnings" msgstr "" @@ -11631,9 +11601,6 @@ msgstr "" msgid "Why the caller can see it, most specific grant first. A filter reachable several ways is still returned once and reports only the most specific reason." msgstr "" -msgid "Width" -msgstr "" - msgid "Width must be 650 pixels." msgstr "" @@ -12268,9 +12235,6 @@ msgstr "" msgid "in" msgstr "分钟" -msgid "in Hz" -msgstr "" - msgid "in batch row" msgstr "" @@ -12278,9 +12242,6 @@ msgstr "" msgid "in minutes" msgstr "分钟" -msgid "in pixels" -msgstr "" - msgid "in seconds" msgstr "" @@ -13072,6 +13033,15 @@ msgstr "" #~ msgid "Database connection unavailable" #~ msgstr "没有可用的散列" +#~ msgid "Default Height" +#~ msgstr "默认高度" + +#~ msgid "Default Refresh Rate" +#~ msgstr "默认刷新频率" + +#~ msgid "Default Width" +#~ msgstr "默认宽度" + #, fuzzy #~ msgid "Delete All" #~ msgstr "删除的文件数据" @@ -13265,10 +13235,18 @@ msgstr "" #~ msgid "Group module settings" #~ msgstr "主机描述" +#, fuzzy +#~ msgid "Height" +#~ msgstr "午夜" + #, fuzzy #~ msgid "History Report" #~ msgstr "主机ID" +#, fuzzy +#~ msgid "Host Display Manager Settings" +#~ msgstr "设置" + #, fuzzy #~ msgid "Host Ext" #~ msgstr "主机列表" @@ -13285,6 +13263,10 @@ msgstr "" #~ msgid "Host Ext Variable" #~ msgstr "用户更新失败" +#, fuzzy +#~ msgid "Host Screen Resolution" +#~ msgstr "主机注册" + #, fuzzy #~ msgid "Host Site" #~ msgstr "主机列表" @@ -13519,6 +13501,10 @@ msgstr "" #~ msgid "Recorded." #~ msgstr "当前记录" +#, fuzzy +#~ msgid "Refresh" +#~ msgstr "默认刷新频率" + #, fuzzy #~ msgid "Release Version" #~ msgstr "最新版本" @@ -13585,6 +13571,10 @@ msgstr "" #~ msgid "Rule update failed!" #~ msgstr "打印机更新失败!" +#, fuzzy +#~ msgid "Screen Refresh Rate" +#~ msgstr "默认刷新频率" + #, fuzzy #~ msgid "Serial" #~ msgstr "系统序列" diff --git a/packages/web/service/displaymanager.php b/packages/web/service/displaymanager.php deleted file mode 100644 index c9d7ccb569..0000000000 --- a/packages/web/service/displaymanager.php +++ /dev/null @@ -1,41 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ - -use FOG\Client\DisplayManager; - -/** - * Display sender for the clients - * - * @category DisplayManager - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -/* - * A machine entry point: the caller is a booting NIC, FOS, the fog-client - * or a storage node, none of which can present a credential. Declared per - * file rather than inferred from the absence of one -- see - * Authorization::_hasNoPrincipal() for what it licenses and why the - * distinction matters. - */ -define('FOG_MACHINE_REQUEST', true); - -require '../commons/base.inc.php'; -new DisplayManager( - true, - false, - false, - false, - isset($_REQUEST['newService']) -); diff --git a/packages/web/src/Agent/State.php b/packages/web/src/Agent/State.php index 1fadd22588..43541c8a77 100644 --- a/packages/web/src/Agent/State.php +++ b/packages/web/src/Agent/State.php @@ -55,6 +55,10 @@ class State extends FOGBase 'snapin' => 'snapinclient', 'software' => 'software', 'power' => 'powermanagement', + // The one legacy client module the rebuild keeps rather than + // drops (design 0014). Same switch, same per-host time, same + // five-minute floor an admin already knows. + 'autologout' => 'autologout', // Both halves of the hostnamechanger module, kept apart on the wire // because they are different acts with different blast radii: a // rename touches this machine, a domain join touches somebody's @@ -294,6 +298,30 @@ public static function desired(Host $Host) 'ondemand' => $ondemand ]; } + if (in_array('autologout', $capabilities, true)) { + // Design 0014. getAlo() is the legacy accessor unchanged: the + // host's own hostAutoLogOut row falling back to the global + // FOG_CLIENT_AUTOLOGOFF_MIN, where 0 disables. + // + // The five-minute floor is applied here AND on the agent. That + // is not belt and braces for its own sake: a two-minute idle + // timer logs people off while they are reading the screen, and + // the client that used to enforce this refused it in + // FOG\Client\Autologout::json(). Sending nothing at all below + // the floor is what makes the agent forget a policy it had. + $minutes = (int)$Host->getAlo(); + if ($minutes >= 5) { + $state['autologout'] = [ + 'minutes' => $minutes, + 'warn_seconds' => (int)self::getSetting( + 'FOG_CLIENT_AUTOLOGOFF_WARN' + ) + ]; + } + // No 'message': the agent carries a sensible default, and a + // string set here would be one the server cannot translate into + // the language of whoever is sitting at the machine. + } if (count($capabilities) > 0) { // The policy every reboot obeys, whatever asked for it: // FOG_GRACE_TIMEOUT is the warning logged-in users get. diff --git a/packages/web/src/Auth/Authorization.php b/packages/web/src/Auth/Authorization.php index 5845a8bf8f..a77ff780e7 100644 --- a/packages/web/src/Auth/Authorization.php +++ b/packages/web/src/Auth/Authorization.php @@ -400,7 +400,6 @@ class Authorization extends FOGBase 'hookevent' => 'settings', 'host' => 'host', 'hostautologout' => 'host', - 'hostscreensetting' => 'host', 'image' => 'image', 'imageassociation' => 'image', 'imagepartitiontype' => 'image', diff --git a/packages/web/src/Base/FOGBase.php b/packages/web/src/Base/FOGBase.php index d2361409fc..5bf2eb69b5 100644 --- a/packages/web/src/Base/FOGBase.php +++ b/packages/web/src/Base/FOGBase.php @@ -1820,7 +1820,6 @@ protected static function getGlobalModuleStatus($names = false, $keys = false) // FOG_CLIENT__ENABLED in lowercase. $services = [ 'autologout' => 'autologoff', - 'displaymanager' => true, 'hostnamechanger' => true, 'hostregister' => true, 'powermanagement' => true, diff --git a/packages/web/src/Base/System.php b/packages/web/src/Base/System.php index 02630cd14b..862b6e9577 100644 --- a/packages/web/src/Base/System.php +++ b/packages/web/src/Base/System.php @@ -130,7 +130,7 @@ public function __construct() // 1.5.x carried count does, see SchemaReconciler's docstring -- is // permanently "up to date" from the updater's point of view and will // never run another indexed step, whatever this constant says. - define('FOG_SCHEMA', 430); + define('FOG_SCHEMA', 432); define('FOG_BCACHE_VER', 364); define('FOG_CLIENT_VERSION', '0.13.0'); // GH-959: iPXE lives in FOGProject/fog-ipxe and its binaries arrive as diff --git a/packages/web/src/Client/DisplayManager.php b/packages/web/src/Client/DisplayManager.php deleted file mode 100644 index 0f05ba9f9d..0000000000 --- a/packages/web/src/Client/DisplayManager.php +++ /dev/null @@ -1,40 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ - -namespace FOG\Client; - -/** - * Handles display manager - * - * @category DisplayManager - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -class DisplayManager extends FOGClient -{ - /** - * Function returns data that will be translated to json - * - * @return array - */ - public function json() - { - return [ - 'x' => self::$Host->getDispVals('width'), - 'y' => self::$Host->getDispVals('height'), - 'r' => self::$Host->getDispVals('refresh'), - ]; - } -} diff --git a/packages/web/src/Client/FOGClient.php b/packages/web/src/Client/FOGClient.php index a758c2483d..d27652d6f7 100644 --- a/packages/web/src/Client/FOGClient.php +++ b/packages/web/src/Client/FOGClient.php @@ -208,7 +208,6 @@ public function __construct( $this->{$method}(); $nonJsonEncode = [ 'autologout', - 'displaymanager', 'printerclient', 'servicemodule', ]; diff --git a/packages/web/src/Items/Group.php b/packages/web/src/Items/Group.php index ca073f35d7..9a264e7321 100644 --- a/packages/web/src/Items/Group.php +++ b/packages/web/src/Items/Group.php @@ -24,7 +24,6 @@ use FOG\Managers\GroupSoftwareAssociationManager; use FOG\Managers\HostAutoLogoutManager; use FOG\Managers\HostManager; -use FOG\Managers\HostScreenSettingManager; use FOG\Managers\MulticastSessionAssociationManager; use FOG\Managers\SnapinJobManager; use FOG\Managers\SnapinTaskManager; @@ -567,43 +566,6 @@ public function removeModule($removeArray) return $this; } - /** - * Set's the display for all hosts in group. - * - * @param mixed $x the width to set - * @param mixed $y the height to set - * @param mixed $r the refresh rate to set - * - * @return object - */ - public function setDisp( - $x, - $y, - $r - ) { - Route::deletemass( - 'hostscreensetting', - ['hostID' => $this->get('hosts')] - ); - $insert_fields = [ - 'hostID', - 'width', - 'height', - 'refresh', - ]; - $insert_items = []; - foreach ((array) $this->get('hosts') as &$hostID) { - $insert_items[] = [$hostID, $x, $y, $r]; - unset($hostID); - } - (new HostScreenSettingManager()) - ->insertBatch( - $insert_fields, - $insert_items - ); - - return $this; - } /** * Set's the auto logout time for all hosts. * diff --git a/packages/web/src/Items/Host.php b/packages/web/src/Items/Host.php index a960e9f06d..8f8dc6748b 100644 --- a/packages/web/src/Items/Host.php +++ b/packages/web/src/Items/Host.php @@ -160,7 +160,6 @@ class Host extends FOGController 'primac', 'imagename', 'groups', - 'hostscreen', 'hostalo', 'optimalStorageNode', 'printers', @@ -196,11 +195,6 @@ class Host extends FOGController 'imageID', 'imagename' ], - 'HostScreenSetting' => [ - 'hostID', - 'id', - 'hostscreen' - ], 'HostAutoLogout' => [ 'hostID', 'id', @@ -244,7 +238,6 @@ class Host extends FOGController * * @var array */ - private static $_hostscreen = []; /** * ALO time val * @@ -538,78 +531,6 @@ public function updateDefault($printerid) } return $this; } - /** - * Sets display vals for the host - * - * @return void - */ - private function _setDispVals() - { - if (count(self::$_hostscreen)) { - return; - } - $keys = [ - 'FOG_CLIENT_DISPLAYMANAGER_R', - 'FOG_CLIENT_DISPLAYMANAGER_X', - 'FOG_CLIENT_DISPLAYMANAGER_y' - ]; - list( - $refresh, - $width, - $height - ) = self::getSetting($keys); - $refresh = ( - $this->get('hostscreen')->get('refresh') ?: - $refresh - ); - $width = ( - $this->get('hostscreen')->get('width') ?: - $width - ); - $height = ( - $this->get('hostscreen')->get('height') ?: - $height - ); - self::$_hostscreen = [ - 'refresh' => $refresh, - 'width' => $width, - 'height' => $height - ]; - } - /** - * Gets the display values - * - * @param string $key the key to get - * - * @return mixed - */ - public function getDispVals($key = '') - { - $this->_setDispVals(); - return self::$_hostscreen[$key]; - } - /** - * Sets the display values - * - * @param mixed $x the width - * @param mixed $y the height - * @param mixed $r the refresh - * - * @return object - */ - public function setDisp($x, $y, $r) - { - if (!$this->get('hostscreen')->isValid()) { - $this->get('hostscreen') - ->set('hostID', $this->get('id')); - } - $this->get('hostscreen') - ->set('width', $x) - ->set('height', $y) - ->set('refresh', $r) - ->save(); - return $this; - } /** * Sets this hosts alo time (or default to global if needed * diff --git a/packages/web/src/Items/HostScreenSetting.php b/packages/web/src/Items/HostScreenSetting.php deleted file mode 100644 index 3a859d072c..0000000000 --- a/packages/web/src/Items/HostScreenSetting.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ - -namespace FOG\Items; - -use FOG\Base\FOGController; - -/** - * Host screen settings class. - * - * @category HostScreenSetting - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -class HostScreenSetting extends FOGController -{ - /** - * The host screen settings table name. - * - * @var string - */ - protected $databaseTable = 'hostScreenSettings'; - /** - * The host screen settings fields and common names. - * - * @var array - */ - protected $databaseFields = [ - 'id' => 'hssID', - 'hostID' => 'hssHostID', - 'width' => 'hssWidth', - 'height' => 'hssHeight', - 'refresh' => 'hssRefresh', - 'orientation' => 'hssOrientation', - 'other1' => 'hssOther1', - 'other2' => 'hssOther2' - ]; - /** - * The required fields - * - * @var array - */ - protected $databaseFieldsRequired = [ - 'hostID' - ]; - /** - * Gets the host object. - * - * @return object - */ - public function getHost() - { - return new Host($this->get('hostID')); - } -} diff --git a/packages/web/src/Items/Setting.php b/packages/web/src/Items/Setting.php index 6f2712dc1c..5fc6415ff8 100644 --- a/packages/web/src/Items/Setting.php +++ b/packages/web/src/Items/Setting.php @@ -52,30 +52,6 @@ class Setting extends FOGController protected $databaseFieldsRequired = [ 'name' ]; - /** - * Set the display settings. - * - * @param int $x The width of the screen. - * @param int $y The height of the screen. - * @param int $r The refresh rate. - * - * @return void - */ - public function setDisplay( - $x, - $y, - $r - ) { - $keySettings = [ - 'FOG_CLIENT_DISPLAYMANAGER_X' => $x, - 'FOG_CLIENT_DISPLAYMANAGER_Y' => $y, - 'FOG_CLIENT_DISPLAYMANAGER_R' => $r, - ]; - foreach ($keySettings as $name => &$value) { - self::setSetting($name, $value); - unset($value); - } - } /** * Builds the exit type selectors for us. * diff --git a/packages/web/src/Managers/HostScreenSettingManager.php b/packages/web/src/Managers/HostScreenSettingManager.php deleted file mode 100644 index fa49b0f556..0000000000 --- a/packages/web/src/Managers/HostScreenSettingManager.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ - -namespace FOG\Managers; - -use FOG\Base\FOGManagerController; - -/** - * Host screen settings manager class. - * - * @category HostScreenSettingManager - * @package FOGProject - * @author Tom Elliott - * @license http://opensource.org/licenses/gpl-3.0 GPLv3 - * @link https://fogproject.org - */ -class HostScreenSettingManager extends FOGManagerController -{ - /** - * The base table name. - * - * @var string - */ - public $tablename = 'hostScreenSettings'; -} diff --git a/packages/web/src/Pages/FOGConfigurationPage.php b/packages/web/src/Pages/FOGConfigurationPage.php index 3baa6a24fb..dbac7f005f 100644 --- a/packages/web/src/Pages/FOGConfigurationPage.php +++ b/packages/web/src/Pages/FOGConfigurationPage.php @@ -4415,7 +4415,6 @@ private function _settingsMeta() 'FOG_CLIENT_AUTOLOGOFF_ENABLED' => true, 'FOG_CLIENT_CLIENTUPDATER_ENABLED' => true, 'FOG_CLIENT_DIRECTORYCLEANER_ENABLED' => true, - 'FOG_CLIENT_DISPLAYMANAGER_ENABLED' => true, 'FOG_CLIENT_HOSTREGISTER_ENABLED' => true, 'FOG_CLIENT_HOSTNAMECHANGER_ENABLED' => true, 'FOG_CLIENT_POWERMANAGEMENT_ENABLED' => true, @@ -4508,10 +4507,7 @@ private function _settingsMeta() 'FOG_GRACE_TIMEOUT' => true, // FOG Service - Auto Log Off 'FOG_CLIENT_AUTOLOGOFF_MIN' => true, - // FOG Service - Display manager - 'FOG_CLIENT_DISPLAYMANAGER_X' => true, - 'FOG_CLIENT_DISPLAYMANAGER_Y' => true, - 'FOG_CLIENT_DISPLAYMANAGER_R' => true, + 'FOG_CLIENT_AUTOLOGOFF_WARN' => true, // FOG Service - Host Register 'FOG_QUICKREG_MAX_PENDING_MACS' => true, // FOG View Settings diff --git a/packages/web/src/Pages/HostManagement.php b/packages/web/src/Pages/HostManagement.php index ce5a2087be..acd6e35ca0 100644 --- a/packages/web/src/Pages/HostManagement.php +++ b/packages/web/src/Pages/HostManagement.php @@ -33,7 +33,6 @@ use FOG\Managers\ArchitectureManager; use FOG\Managers\HostAutoLogoutManager; use FOG\Managers\HostManager; -use FOG\Managers\HostScreenSettingManager; use FOG\Managers\ImageManager; use FOG\Managers\MACAddressAssociationManager; use FOG\Managers\PowerManagementManager; @@ -3177,123 +3176,6 @@ public function hostModules() . '" '; $labelClass = 'col-sm-3 col-form-label'; - // Display Manager area - $dispEnabled = self::getSetting('FOG_CLIENT_DISPLAYMANAGER_ENABLED'); - if ($dispEnabled) { - $buttons = self::makeButton( - 'host-displayman-send', - _('Update'), - 'btn btn-primary float-end', - $props - ); - // If the x, y, and/or r inputs are set. - $ix = filter_input(INPUT_POST, 'x'); - $iy = filter_input(INPUT_POST, 'y'); - $ir = filter_input(INPUT_POST, 'r'); - if (!$ix) { - // If x not set check hosts setting - $ix = $this->obj->getDispVals('width'); - } - if (!$iy) { - // If y not set check hosts setting - $iy = $this->obj->getDispVals('height'); - } - if (!$ir) { - // If r not set check hosts setting - $ir = $this->obj->getDispVals('refresh'); - } - $x = $ix; - $y = $iy; - $r = $ir; - $names = [ - 'x' => [ - 'width', - _('Screen Width') - . '
    (' - . _('in pixels') - . ')' - ], - 'y' => [ - 'height', - _('Screen Height') - . '
    (' - . _('in pixels') - . ')' - ], - 'r' => [ - 'refresh', - _('Screen Refresh Rate') - . '
    (' - . _('in Hz') - . ')' - ] - ]; - foreach ($names as $name => &$get) { - switch ($name) { - case 'r': - $val = $r; - break; - case 'x': - $val = $x; - break; - case 'y': - $val = $y; - } - $fields[ - self::makeLabel( - $labelClass, - $name, - $get[1] - ) - ] = self::makeInput( - 'form-control', - $name, - '', - 'number', - $name, - $val - ); - unset($get); - } - - self::$HookManager->processEvent( - 'HOST_DISPLAYMAN_FIELDS', - [ - 'fields' => &$fields, - 'buttons' => &$buttons, - 'Host' => &$this->obj - ] - ); - - $rendered = self::formFields($fields); - unset($fields); - echo '
    '; - echo '
    '; - echo '

    '; - echo _('Host Display Manager Settings'); - echo '

    '; - echo '
    '; - echo '
    '; - echo self::makeFormTag( - '', - 'host-displayman-form', - self::makeTabUpdateURL( - 'host-module', - $this->obj->get('id') - ), - 'post', - 'application/x-www-form-urlencoded', - true - ); - echo $rendered; - echo ''; - echo '
    '; - echo ''; - echo '
    '; - } - // Auto Log Out $aloEnabled = self::getSetting('FOG_CLIENT_AUTOLOGOFF_ENABLED'); if ($aloEnabled) { @@ -3418,12 +3300,6 @@ public function hostModulePost() } $this->obj->setModuleState([$moduleID], $states[(string)$state]); } - if (isset($_POST['confirmdisplaysend'])) { - $x = (int)filter_input(INPUT_POST, 'x'); - $y = (int)filter_input(INPUT_POST, 'y'); - $r = (int)filter_input(INPUT_POST, 'r'); - $this->obj->setDisp($x, $y, $r); - } if (isset($_POST['confirmalosend'])) { $tme = (int)filter_input(INPUT_POST, 'tme'); // HostAutoLogout::MIN_MINUTES, not the literal it was spelled as. @@ -4992,7 +4868,7 @@ private function massEditCoreFields() // association is not a value a mass edit sets. What is left // when you take the list away is a statement about how hard // the client works on printers at check-in, which is what the - // rest of this tab is: resolution, auto-logout, this. + // rest of this tab is: auto-logout, this. 'tab' => 'client' ], // The two booleans. A boolean has no meaningful "clear", so the @@ -5056,25 +4932,17 @@ private function massEditCoreFields() /** * The host settings a mass edit may change that are NOT `hosts` columns. * - * Auto-logout and screen resolution live one row per host in their own - * tables, and the group page writes each by deleting every member's row - * and inserting a fresh one (`Group::setAlo()`, `Group::setDisp()`). That - * shape maps onto the three states exactly: SET is delete-then-insert, - * CLEAR is the delete on its own -- no row IS the absence of an override - * -- and LEAVE touches nothing. - * - * `composite` marks the one field whose value is more than one number. - * A resolution is width, height and refresh written as a single row, so - * "set the width and leave the height" has no meaning at the storage - * layer; it is one instruction carrying three parts, resolved through - * MassEdit::resolveComposite(). See that method for why it is not a - * `1024x768@60` string. + * Auto-logout lives one row per host in its own table, and the group + * page writes it by deleting every member's row and inserting a fresh + * one (`Group::setAlo()`). That shape maps onto the three states + * exactly: SET is delete-then-insert, CLEAR is the delete on its own -- + * no row IS the absence of an override -- and LEAVE touches nothing. * * Deliberately separate from massEditCoreFields(): nothing here can ever * be a column update, and keeping the two lists apart is what stops one * from being handed to columnUpdates() by accident. * - * @return array key => ['label', 'kind', 'composite', 'tab'] + * @return array key => ['label', 'kind', 'tab'] */ private function massEditRowFields() { @@ -5084,12 +4952,6 @@ private function massEditRowFields() 'kind' => 'number', 'tab' => 'client' ], - 'resolution' => [ - 'label' => _('Host Screen Resolution'), - 'kind' => 'resolution', - 'composite' => true, - 'tab' => 'client' - ], ]; } @@ -5130,26 +4992,6 @@ private function massEditApplyRows(array $resolved, array $hostIDs) $wrote = count($hostIDs); } - $res = $resolved['resolution'] ?? null; - if (null !== $res && MassEdit::LEAVE !== $res['action']) { - Route::deletemass('hostscreensetting', ['hostID' => $hostIDs]); - if (MassEdit::SET === $res['action']) { - $x = (int)($res['value']['x'] ?? 0); - $y = (int)($res['value']['y'] ?? 0); - $r = (int)($res['value']['r'] ?? 0); - $rows = []; - foreach ($hostIDs as $hostID) { - $rows[] = [$hostID, $x, $y, $r]; - } - (new HostScreenSettingManager()) - ->insertBatch( - ['hostID', 'width', 'height', 'refresh'], - $rows - ); - } - $wrote = count($hostIDs); - } - return $wrote; } @@ -5424,33 +5266,6 @@ private function massEditValueControl($key, array $spec) -1, 'min="0"' ); - case 'resolution': - // One instruction, three parts. The names are the array HTTP - // already gives -- value[resolution][x] and friends -- so - // MassEdit::resolveComposite() reads them without parsing - // anything. See that method for why this is not one string. - $part = function ($sub, $placeholder) use ($name, $id) { - return '
    ' - . self::makeInput( - 'form-control', - $name . '[' . $sub . ']', - $placeholder, - 'number', - $id . '-' . $sub, - '', - false, - false, - -1, - -1, - 'min="0"' - ) - . '
    '; - }; - return '
    ' - . $part('x', _('Width')) - . $part('y', _('Height')) - . $part('r', _('Refresh')) - . '
    '; } return self::makeInput('form-control', $name, '', 'text', $id); @@ -5537,34 +5352,6 @@ private function massEditHints(array $hostIDs, array $core) ); $hints['autologout'] = SharedHostValues::hint($alo['autologout']); - // Three columns, one answer. The resolution is uniform only when - // every part agrees AND every selected host has a row -- which is - // what forHostRows() means by uniform -- so the parts are combined - // rather than reported one by one. Three hints reading "(all)", - // "(varies)", "(all)" would describe a resolution nobody has. - $disp = SharedHostValues::forHostRows( - $hostIDs, - 'hostScreenSettings', - 'hssHostID', - ['x' => 'hssWidth', 'y' => 'hssHeight', 'r' => 'hssRefresh'] - ); - $uniform = !empty($disp['x']['uniform']) - && !empty($disp['y']['uniform']) - && !empty($disp['r']['uniform']); - $hints['resolution'] = SharedHostValues::hint( - [ - 'uniform' => $uniform, - 'value' => $uniform - ? sprintf( - '%sx%s@%s', - $disp['x']['value'], - $disp['y']['value'], - $disp['r']['value'] - ) - : '' - ] - ); - return $hints; } @@ -5819,18 +5606,10 @@ public function massEditPost() ) ) ); - // The row-backed fields are resolved separately by shape, not - // merged into $keys: the composite one has an array value, and - // resolve()'s safety property is that an array is never a value. - $scalarRows = []; - $compositeRows = []; - foreach ($rowFields as $key => $rowSpec) { - if (!empty($rowSpec['composite'])) { - $compositeRows[] = $key; - } else { - $scalarRows[] = $key; - } - } + // The row-backed fields are resolved separately, not merged + // into $keys: they are written one row per host rather than as + // columns and must never reach columnUpdates(). + $scalarRows = array_keys($rowFields); $flags = ['flags' => FILTER_REQUIRE_ARRAY]; $posted = filter_input_array( @@ -5842,17 +5621,10 @@ public function massEditPost() $posted['action'] ?? null, $posted['value'] ?? null ); - $resolvedRows = array_merge( - MassEdit::resolve( - $scalarRows, - $posted['action'] ?? null, - $posted['value'] ?? null - ), - MassEdit::resolveComposite( - $compositeRows, - $posted['action'] ?? null, - $posted['value'] ?? null - ) + $resolvedRows = MassEdit::resolve( + $scalarRows, + $posted['action'] ?? null, + $posted['value'] ?? null ); $touched = array_merge( MassEdit::touched($resolved), @@ -7739,26 +7511,6 @@ public function getInstalledSoftware() json_encode($data, JSON_UNESCAPED_UNICODE) ); } - /** - * Get the hosts display man values - * - * @return void - */ - public function getHostDisplayManVals() - { - header('Content-type: application/json'); - parse_str( - file_get_contents('php://input'), - $pass_vars - ); - $this->jsonSend(HTTPResponseCodes::HTTP_SUCCESS, json_encode( - [ - 'x' => $this->obj->getDispVals('width'), - 'y' => $this->obj->getDispVals('height'), - 'r' => $this->obj->getDispVals('refresh') - ] - )); - } /** * Get the hosts display man values * diff --git a/packages/web/src/Pages/ServiceConfigurationPage.php b/packages/web/src/Pages/ServiceConfigurationPage.php index 19c367c6f5..c34e5e59ce 100644 --- a/packages/web/src/Pages/ServiceConfigurationPage.php +++ b/packages/web/src/Pages/ServiceConfigurationPage.php @@ -283,117 +283,23 @@ private function _saveModuleTab($key, $match, $hook, $extra = null) } } /** - * Presents the displaymanager page. + * Presents the autologout page. * * @return void */ - public function serviceDisplaymanager() + public function serviceAutologout() { - list( - $r, - $x, - $y - ) = self::getSetting( - [ - 'FOG_CLIENT_DISPLAYMANAGER_R', - 'FOG_CLIENT_DISPLAYMANAGER_X', - 'FOG_CLIENT_DISPLAYMANAGER_Y' - ] - ); - $labelClass = 'col-sm-3 col-form-label'; - $this->_renderModuleTab( - 'displaymanager', - 'display manager', - 'dm', - 'MODULE_DISPLAYMANAGER_FIELDS', + list( + $tme, + $warn + ) = self::getSetting( [ - self::makeLabel( - $labelClass, - 'width', - _('Default Width') - . '
    (' - . _('in pixels') - . ')' - ) => self::makeInput( - 'form-control', - 'width', - '1024', - 'number', - 'width', - $x - ), - self::makeLabel( - $labelClass, - 'height', - _('Default Height') - . '
    (' - . _('in pixels') - . ')' - ) => self::makeInput( - 'form-control', - 'height', - '768', - 'number', - 'height', - $y - ), - self::makeLabel( - $labelClass, - 'refresh', - _('Default Refresh Rate') - . '
    (' - . _('in Hz') - . ')' - ) => self::makeInput( - 'form-control', - 'refresh', - '60', - 'number', - 'refresh', - $r - ) + 'FOG_CLIENT_AUTOLOGOFF_MIN', + 'FOG_CLIENT_AUTOLOGOFF_WARN' ] ); - } - /** - * Updates the display manager elements. - * - * @return void - */ - public function serviceDisplaymanagerPost() - { - $this->_saveModuleTab( - 'displaymanager', - 'display manager', - 'MODULE_DISPLAYMANAGER_POST', - function () { - self::setSetting( - 'FOG_CLIENT_DISPLAYMANAGER_R', - (int)filter_input(INPUT_POST, 'refresh') - ); - self::setSetting( - 'FOG_CLIENT_DISPLAYMANAGER_X', - (int)filter_input(INPUT_POST, 'width') - ); - self::setSetting( - 'FOG_CLIENT_DISPLAYMANAGER_Y', - (int)filter_input(INPUT_POST, 'height') - ); - } - ); - } - /** - * Presents the autologout page. - * - * @return void - */ - public function serviceAutologout() - { - $labelClass = 'col-sm-3 col-form-label'; - - $tme = self::getSetting('FOG_CLIENT_AUTOLOGOFF_MIN'); $this->_renderModuleTab( 'autologout', @@ -417,6 +323,23 @@ public function serviceAutologout() 'number', 'updatetme', $tme + ), + self::makeLabel( + $labelClass, + 'updatewarn', + _('Warning Before Log Out') + . '
    (' + . _('in seconds') + . ')
    (' + . _('0 logs the user out with no warning') + . ')' + ) => self::makeInput( + 'form-control', + 'warn', + '60', + 'number', + 'updatewarn', + $warn ) ] ); @@ -438,6 +361,16 @@ function () { $tme = 0; } self::setSetting('FOG_CLIENT_AUTOLOGOFF_MIN', $tme); + // Clamped rather than refused, and never negative. A warning + // longer than the timeout is clamped again on the agent, to + // half the timeout -- the point of doing it in both places + // is that a policy which arrived wrong must not be able to + // log a fleet off the moment anybody stops typing. + $warn = (int)filter_input(INPUT_POST, 'warn'); + if ($warn < 0) { + $warn = 0; + } + self::setSetting('FOG_CLIENT_AUTOLOGOFF_WARN', $warn); } ); } @@ -711,9 +644,6 @@ public function editPost() case 'service-autologout': $this->serviceAutologoutPost(); break; - case 'service-displaymanager': - $this->serviceDisplaymanagerPost(); - break; case 'service-hostnamechanger': $this->serviceHostnamechangerPost(); break; diff --git a/packages/web/src/Router/Route.php b/packages/web/src/Router/Route.php index 94e452af88..4cfb522c76 100644 --- a/packages/web/src/Router/Route.php +++ b/packages/web/src/Router/Route.php @@ -665,7 +665,6 @@ class Route extends FOGBase 'host', 'hostautologout', 'hostfactstate', - 'hostscreensetting', 'hostsoftware', 'hostdirectory', 'hostnetwork', @@ -3689,7 +3688,7 @@ private static function _listExpandRows($listData, $class, $classname) // Inline ONLY the requested relations onto the flat grid // row. Merging the full getter() output here would drag in // every relation the entity's base serialization embeds - // (for Host: inventory/image/hostscreen/hostalo/macs), + // (for Host: inventory/image/hostalo/macs), // which defeats the selective contract of ?expand=token. $exp = self::expandRelations($classname, $robj, $row); $exp = self::enrichPluginItems($classname, $robj, $exp); @@ -7216,11 +7215,6 @@ public static function getter($classname, $class) $serialExtras = [ 'ADPass' => $pass, 'productKey' => $productKey, - 'hostscreen' => self::embed( - $classname, - 'hostscreen', - $class->get('hostscreen') - ), 'hostalo' => self::embed( $classname, 'hostalo', @@ -9277,7 +9271,6 @@ private static function _removeItemsFor($classname, $itemIDs) 'task' => $findWhere, 'scheduledtask' => $findWhere, 'hostautologout' => $findWhere, - 'hostscreensetting' => $findWhere, 'groupassociation' => $findWhere, 'snapinassociation' => $findWhere, 'printerassociation' => $findWhere, diff --git a/packages/web/src/Util/MassEdit.php b/packages/web/src/Util/MassEdit.php index 34fe128ffa..e0dacd6dee 100644 --- a/packages/web/src/Util/MassEdit.php +++ b/packages/web/src/Util/MassEdit.php @@ -147,84 +147,6 @@ public static function resolve(array $keys, $actions, $values) return $resolved; } - /** - * resolve() for fields whose value is more than one number. - * - * Screen resolution is one setting made of three values -- width, height - * and refresh -- written as one row. It cannot be three independent - * fields, because the row is deleted and re-inserted whole, so "set the - * width and leave the height" has no meaning at the storage layer. And - * it must not be one string like `1024x768@60`, because that is an - * in-band encoding, which is the exact shape ADR 0038 decision 11 threw - * out: undiscoverable, unescapable, and a second format to remember at - * every call site. - * - * So the value arrives the way HTTP already gives it -- `value[key][x]`, - * `value[key][y]`, `value[key][r]` -- and stays an array all the way to - * the arm that writes it. Nothing parses anything. - * - * This is a SEPARATE function rather than a flag on resolve() on - * purpose. resolve()'s safety property is "an array is never a value, - * because a field posting key[]=a&key[]=b is either a bug or somebody - * probing". Adding a parameter that suspends that rule for some keys - * would make the property conditional on the caller passing the right - * list. Here it stays literally true of resolve(), and asking for a - * composite is an explicit choice of which function to call. - * - * Fails closed the same way in both directions: a composite key posting - * a scalar resolves to LEAVE, and so does one posting an array with a - * non-scalar in it. - * - * @param array $keys the composite field keys the caller offers - * @param mixed $actions the posted action map, key => action - * @param mixed $values the posted value map, key => array of scalars - * - * @return array key => ['action' => ..., 'value' => array of strings]. - * EVERY key in $keys is present, as with resolve(). - */ - public static function resolveComposite(array $keys, $actions, $values) - { - $actions = is_array($actions) ? $actions : []; - $values = is_array($values) ? $values : []; - $allowed = [self::LEAVE, self::SET, self::CLEAR]; - $resolved = []; - foreach ($keys as $key) { - $key = (string)$key; - $action = $actions[$key] ?? self::LEAVE; - if (!is_string($action) - || !in_array($action, $allowed, true) - ) { - $action = self::LEAVE; - } - if (self::SET === $action && !array_key_exists($key, $values)) { - $action = self::LEAVE; - } - $value = []; - if (self::SET === $action) { - $raw = $values[$key]; - if (!is_array($raw)) { - $action = self::LEAVE; - } else { - foreach ($raw as $part => $sub) { - if (!is_scalar($sub) && null !== $sub) { - // One bad part discards the whole instruction. - // A composite half-written is a row half-right, - // and there is no way for the arm downstream to - // tell that from a deliberate blank. - $action = self::LEAVE; - $value = []; - break; - } - $value[(string)$part] = trim((string)$sub); - } - } - } - $resolved[$key] = ['action' => $action, 'value' => $value]; - } - - return $resolved; - } - /** * Turns resolved instructions into the column map an update takes. * @@ -252,11 +174,11 @@ public static function columnUpdates(array $resolved, array $spec) } $action = $instruction['action'] ?? self::LEAVE; if (self::SET === $action) { - // A composite instruction can never be a column update: its - // value is an array and a column takes one value. Guarded - // here rather than left to the spec, because the spec is - // partly written by plugins and a plugin naming `field` on a - // composite key would otherwise write "Array" into a column. + // An array value can never be a column update: a column + // takes one value. Guarded here rather than left to the + // spec, because the spec is partly written by plugins and a + // plugin naming `field` on a key whose posted value is an + // array would otherwise write the string "Array" into it. if (is_array($instruction['value'])) { continue; } diff --git a/packages/web/vendor/composer/autoload_classmap.php b/packages/web/vendor/composer/autoload_classmap.php index 5e972a2ff9..3e4c4ecd06 100644 --- a/packages/web/vendor/composer/autoload_classmap.php +++ b/packages/web/vendor/composer/autoload_classmap.php @@ -7,6 +7,24 @@ return array( 'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php', + 'FOG\\Agent\\DirectoryFacts' => $baseDir . '/src/Agent/DirectoryFacts.php', + 'FOG\\Agent\\DirectoryJoin' => $baseDir . '/src/Agent/DirectoryJoin.php', + 'FOG\\Agent\\DirectoryPlacement' => $baseDir . '/src/Agent/DirectoryPlacement.php', + 'FOG\\Agent\\Enrollment' => $baseDir . '/src/Agent/Enrollment.php', + 'FOG\\Agent\\InventoryFacts' => $baseDir . '/src/Agent/InventoryFacts.php', + 'FOG\\Agent\\NetworkFacts' => $baseDir . '/src/Agent/NetworkFacts.php', + 'FOG\\Agent\\Principal' => $baseDir . '/src/Agent/Principal.php', + 'FOG\\Agent\\PrinterFacts' => $baseDir . '/src/Agent/PrinterFacts.php', + 'FOG\\Agent\\PrinterSet' => $baseDir . '/src/Agent/PrinterSet.php', + 'FOG\\Agent\\SecureBootFacts' => $baseDir . '/src/Agent/SecureBootFacts.php', + 'FOG\\Agent\\Snapins' => $baseDir . '/src/Agent/Snapins.php', + 'FOG\\Agent\\SoftwareFacts' => $baseDir . '/src/Agent/SoftwareFacts.php', + 'FOG\\Agent\\SoftwareSet' => $baseDir . '/src/Agent/SoftwareSet.php', + 'FOG\\Agent\\State' => $baseDir . '/src/Agent/State.php', + 'FOG\\Agent\\Token' => $baseDir . '/src/Agent/Token.php', + 'FOG\\Agent\\UserSessions' => $baseDir . '/src/Agent/UserSessions.php', + 'FOG\\Agent\\WakeRelay' => $baseDir . '/src/Agent/WakeRelay.php', + 'FOG\\Assign\\Resolver' => $baseDir . '/src/Assign/Resolver.php', 'FOG\\Audit\\ActivityWindow' => $baseDir . '/src/Audit/ActivityWindow.php', 'FOG\\Audit\\Audit' => $baseDir . '/src/Audit/Audit.php', 'FOG\\Audit\\AuditStats' => $baseDir . '/src/Audit/AuditStats.php', @@ -40,6 +58,7 @@ 'FOG\\Base\\LoadGlobals' => $baseDir . '/src/Base/LoadGlobals.php', 'FOG\\Base\\Page' => $baseDir . '/src/Base/Page.php', 'FOG\\Base\\PluginTask' => $baseDir . '/src/Base/PluginTask.php', + 'FOG\\Base\\SmbiosIdentity' => $baseDir . '/src/Base/SmbiosIdentity.php', 'FOG\\Base\\StorageEpoch' => $baseDir . '/src/Base/StorageEpoch.php', 'FOG\\Base\\System' => $baseDir . '/src/Base/System.php', 'FOG\\Boot\\BootMenuBase' => $baseDir . '/src/Boot/BootMenuBase.php', @@ -47,9 +66,10 @@ 'FOG\\Boot\\Registration' => $baseDir . '/src/Boot/Registration.php', 'FOG\\Boot\\SecureBootState' => $baseDir . '/src/Boot/SecureBootState.php', 'FOG\\Boot\\UbootBootMenu' => $baseDir . '/src/Boot/UbootBootMenu.php', + 'FOG\\Boot\\UbootRenderHalted' => $baseDir . '/src/Boot/UbootRenderHalted.php', + 'FOG\\Boot\\UbootTftpSync' => $baseDir . '/src/Boot/UbootTftpSync.php', 'FOG\\Boot\\WakeOnLan' => $baseDir . '/src/Boot/WakeOnLan.php', 'FOG\\Client\\Autologout' => $baseDir . '/src/Client/Autologout.php', - 'FOG\\Client\\DisplayManager' => $baseDir . '/src/Client/DisplayManager.php', 'FOG\\Client\\FOGClient' => $baseDir . '/src/Client/FOGClient.php', 'FOG\\Client\\HostnameChanger' => $baseDir . '/src/Client/HostnameChanger.php', 'FOG\\Client\\Jobs' => $baseDir . '/src/Client/Jobs.php', @@ -78,18 +98,33 @@ 'FOG\\Hooks\\SubMenuData' => $baseDir . '/src/Hooks/SubMenuData.php', 'FOG\\Hooks\\Template' => $baseDir . '/src/Hooks/Template.php', 'FOG\\Items\\APIToken' => $baseDir . '/src/Items/APIToken.php', + 'FOG\\Items\\AgentEnrollToken' => $baseDir . '/src/Items/AgentEnrollToken.php', + 'FOG\\Items\\AgentEnrollment' => $baseDir . '/src/Items/AgentEnrollment.php', + 'FOG\\Items\\AgentWake' => $baseDir . '/src/Items/AgentWake.php', 'FOG\\Items\\Architecture' => $baseDir . '/src/Items/Architecture.php', 'FOG\\Items\\AuditChange' => $baseDir . '/src/Items/AuditChange.php', 'FOG\\Items\\AuditLog' => $baseDir . '/src/Items/AuditLog.php', + 'FOG\\Items\\BootFile' => $baseDir . '/src/Items/BootFile.php', 'FOG\\Items\\DMIKey' => $baseDir . '/src/Items/DMIKey.php', 'FOG\\Items\\FileDeleteQueue' => $baseDir . '/src/Items/FileDeleteQueue.php', 'FOG\\Items\\Group' => $baseDir . '/src/Items/Group.php', 'FOG\\Items\\GroupAssociation' => $baseDir . '/src/Items/GroupAssociation.php', + 'FOG\\Items\\GroupModuleAssociation' => $baseDir . '/src/Items/GroupModuleAssociation.php', + 'FOG\\Items\\GroupPowerManagement' => $baseDir . '/src/Items/GroupPowerManagement.php', + 'FOG\\Items\\GroupPrinterAssociation' => $baseDir . '/src/Items/GroupPrinterAssociation.php', + 'FOG\\Items\\GroupSnapinAssociation' => $baseDir . '/src/Items/GroupSnapinAssociation.php', + 'FOG\\Items\\GroupSoftwareAssociation' => $baseDir . '/src/Items/GroupSoftwareAssociation.php', 'FOG\\Items\\History' => $baseDir . '/src/Items/History.php', 'FOG\\Items\\HookEvent' => $baseDir . '/src/Items/HookEvent.php', 'FOG\\Items\\Host' => $baseDir . '/src/Items/Host.php', 'FOG\\Items\\HostAutoLogout' => $baseDir . '/src/Items/HostAutoLogout.php', - 'FOG\\Items\\HostScreenSetting' => $baseDir . '/src/Items/HostScreenSetting.php', + 'FOG\\Items\\HostDirectory' => $baseDir . '/src/Items/HostDirectory.php', + 'FOG\\Items\\HostFactState' => $baseDir . '/src/Items/HostFactState.php', + 'FOG\\Items\\HostNetwork' => $baseDir . '/src/Items/HostNetwork.php', + 'FOG\\Items\\HostPrinter' => $baseDir . '/src/Items/HostPrinter.php', + 'FOG\\Items\\HostSoftware' => $baseDir . '/src/Items/HostSoftware.php', + 'FOG\\Items\\HostSpooler' => $baseDir . '/src/Items/HostSpooler.php', + 'FOG\\Items\\HostUserSession' => $baseDir . '/src/Items/HostUserSession.php', 'FOG\\Items\\Image' => $baseDir . '/src/Items/Image.php', 'FOG\\Items\\ImageAssociation' => $baseDir . '/src/Items/ImageAssociation.php', 'FOG\\Items\\ImagePartitionType' => $baseDir . '/src/Items/ImagePartitionType.php', @@ -132,6 +167,9 @@ 'FOG\\Items\\SnapinGroupAssociation' => $baseDir . '/src/Items/SnapinGroupAssociation.php', 'FOG\\Items\\SnapinJob' => $baseDir . '/src/Items/SnapinJob.php', 'FOG\\Items\\SnapinTask' => $baseDir . '/src/Items/SnapinTask.php', + 'FOG\\Items\\Software' => $baseDir . '/src/Items/Software.php', + 'FOG\\Items\\SoftwareAssociation' => $baseDir . '/src/Items/SoftwareAssociation.php', + 'FOG\\Items\\SoftwareStatus' => $baseDir . '/src/Items/SoftwareStatus.php', 'FOG\\Items\\StorageGroup' => $baseDir . '/src/Items/StorageGroup.php', 'FOG\\Items\\StorageNode' => $baseDir . '/src/Items/StorageNode.php', 'FOG\\Items\\Task' => $baseDir . '/src/Items/Task.php', @@ -145,18 +183,33 @@ 'FOG\\Items\\UserPref' => $baseDir . '/src/Items/UserPref.php', 'FOG\\Items\\UserTracking' => $baseDir . '/src/Items/UserTracking.php', 'FOG\\Managers\\APITokenManager' => $baseDir . '/src/Managers/APITokenManager.php', + 'FOG\\Managers\\AgentEnrollTokenManager' => $baseDir . '/src/Managers/AgentEnrollTokenManager.php', + 'FOG\\Managers\\AgentEnrollmentManager' => $baseDir . '/src/Managers/AgentEnrollmentManager.php', + 'FOG\\Managers\\AgentWakeManager' => $baseDir . '/src/Managers/AgentWakeManager.php', 'FOG\\Managers\\ArchitectureManager' => $baseDir . '/src/Managers/ArchitectureManager.php', 'FOG\\Managers\\AuditChangeManager' => $baseDir . '/src/Managers/AuditChangeManager.php', 'FOG\\Managers\\AuditLogManager' => $baseDir . '/src/Managers/AuditLogManager.php', + 'FOG\\Managers\\BootFileManager' => $baseDir . '/src/Managers/BootFileManager.php', 'FOG\\Managers\\DMIKeyManager' => $baseDir . '/src/Managers/DMIKeyManager.php', 'FOG\\Managers\\FileDeleteQueueManager' => $baseDir . '/src/Managers/FileDeleteQueueManager.php', 'FOG\\Managers\\GroupAssociationManager' => $baseDir . '/src/Managers/GroupAssociationManager.php', 'FOG\\Managers\\GroupManager' => $baseDir . '/src/Managers/GroupManager.php', + 'FOG\\Managers\\GroupModuleAssociationManager' => $baseDir . '/src/Managers/GroupModuleAssociationManager.php', + 'FOG\\Managers\\GroupPowerManagementManager' => $baseDir . '/src/Managers/GroupPowerManagementManager.php', + 'FOG\\Managers\\GroupPrinterAssociationManager' => $baseDir . '/src/Managers/GroupPrinterAssociationManager.php', + 'FOG\\Managers\\GroupSnapinAssociationManager' => $baseDir . '/src/Managers/GroupSnapinAssociationManager.php', + 'FOG\\Managers\\GroupSoftwareAssociationManager' => $baseDir . '/src/Managers/GroupSoftwareAssociationManager.php', 'FOG\\Managers\\HistoryManager' => $baseDir . '/src/Managers/HistoryManager.php', 'FOG\\Managers\\HookEventManager' => $baseDir . '/src/Managers/HookEventManager.php', 'FOG\\Managers\\HostAutoLogoutManager' => $baseDir . '/src/Managers/HostAutoLogoutManager.php', + 'FOG\\Managers\\HostDirectoryManager' => $baseDir . '/src/Managers/HostDirectoryManager.php', + 'FOG\\Managers\\HostFactStateManager' => $baseDir . '/src/Managers/HostFactStateManager.php', 'FOG\\Managers\\HostManager' => $baseDir . '/src/Managers/HostManager.php', - 'FOG\\Managers\\HostScreenSettingManager' => $baseDir . '/src/Managers/HostScreenSettingManager.php', + 'FOG\\Managers\\HostNetworkManager' => $baseDir . '/src/Managers/HostNetworkManager.php', + 'FOG\\Managers\\HostPrinterManager' => $baseDir . '/src/Managers/HostPrinterManager.php', + 'FOG\\Managers\\HostSoftwareManager' => $baseDir . '/src/Managers/HostSoftwareManager.php', + 'FOG\\Managers\\HostSpoolerManager' => $baseDir . '/src/Managers/HostSpoolerManager.php', + 'FOG\\Managers\\HostUserSessionManager' => $baseDir . '/src/Managers/HostUserSessionManager.php', 'FOG\\Managers\\ImageAssociationManager' => $baseDir . '/src/Managers/ImageAssociationManager.php', 'FOG\\Managers\\ImageManager' => $baseDir . '/src/Managers/ImageManager.php', 'FOG\\Managers\\ImagePartitionTypeManager' => $baseDir . '/src/Managers/ImagePartitionTypeManager.php', @@ -198,6 +251,9 @@ 'FOG\\Managers\\SnapinJobManager' => $baseDir . '/src/Managers/SnapinJobManager.php', 'FOG\\Managers\\SnapinManager' => $baseDir . '/src/Managers/SnapinManager.php', 'FOG\\Managers\\SnapinTaskManager' => $baseDir . '/src/Managers/SnapinTaskManager.php', + 'FOG\\Managers\\SoftwareAssociationManager' => $baseDir . '/src/Managers/SoftwareAssociationManager.php', + 'FOG\\Managers\\SoftwareManager' => $baseDir . '/src/Managers/SoftwareManager.php', + 'FOG\\Managers\\SoftwareStatusManager' => $baseDir . '/src/Managers/SoftwareStatusManager.php', 'FOG\\Managers\\StorageGroupManager' => $baseDir . '/src/Managers/StorageGroupManager.php', 'FOG\\Managers\\StorageNodeManager' => $baseDir . '/src/Managers/StorageNodeManager.php', 'FOG\\Managers\\TaskLogManager' => $baseDir . '/src/Managers/TaskLogManager.php', @@ -211,11 +267,13 @@ 'FOG\\Managers\\UserPrefManager' => $baseDir . '/src/Managers/UserPrefManager.php', 'FOG\\Managers\\UserTrackingManager' => $baseDir . '/src/Managers/UserTrackingManager.php', 'FOG\\Net\\FOGFTP' => $baseDir . '/src/Net/FOGFTP.php', + 'FOG\\Net\\FOGLdap' => $baseDir . '/src/Net/FOGLdap.php', 'FOG\\Net\\FOGRollingURL' => $baseDir . '/src/Net/FOGRollingURL.php', 'FOG\\Net\\FOGSSH' => $baseDir . '/src/Net/FOGSSH.php', 'FOG\\Net\\FOGURLRequests' => $baseDir . '/src/Net/FOGURLRequests.php', 'FOG\\Net\\Ping' => $baseDir . '/src/Net/Ping.php', 'FOG\\Pages\\ActivityManagement' => $baseDir . '/src/Pages/ActivityManagement.php', + 'FOG\\Pages\\AgentActivityManagement' => $baseDir . '/src/Pages/AgentActivityManagement.php', 'FOG\\Pages\\ApiDocumentation' => $baseDir . '/src/Pages/ApiDocumentation.php', 'FOG\\Pages\\AuditManagement' => $baseDir . '/src/Pages/AuditManagement.php', 'FOG\\Pages\\ClientManagement' => $baseDir . '/src/Pages/ClientManagement.php', @@ -238,25 +296,32 @@ 'FOG\\Pages\\ServiceConfigurationPage' => $baseDir . '/src/Pages/ServiceConfigurationPage.php', 'FOG\\Pages\\SiteManagement' => $baseDir . '/src/Pages/SiteManagement.php', 'FOG\\Pages\\SnapinManagement' => $baseDir . '/src/Pages/SnapinManagement.php', + 'FOG\\Pages\\SoftwareManagement' => $baseDir . '/src/Pages/SoftwareManagement.php', 'FOG\\Pages\\StorageGroupManagement' => $baseDir . '/src/Pages/StorageGroupManagement.php', 'FOG\\Pages\\StorageNodeManagement' => $baseDir . '/src/Pages/StorageNodeManagement.php', 'FOG\\Pages\\TaskManagement' => $baseDir . '/src/Pages/TaskManagement.php', 'FOG\\Pages\\UserGroupManagement' => $baseDir . '/src/Pages/UserGroupManagement.php', 'FOG\\Pages\\UserManagement' => $baseDir . '/src/Pages/UserManagement.php', 'FOG\\Reports\\Audit_Report' => $baseDir . '/src/Reports/Audit_Report.php', + 'FOG\\Reports\\Directory_Membership' => $baseDir . '/src/Reports/Directory_Membership.php', 'FOG\\Reports\\File_Deleter' => $baseDir . '/src/Reports/File_Deleter.php', 'FOG\\Reports\\Fleet_Report' => $baseDir . '/src/Reports/Fleet_Report.php', 'FOG\\Reports\\Hardware_Report' => $baseDir . '/src/Reports/Hardware_Report.php', 'FOG\\Reports\\History_Report' => $baseDir . '/src/Reports/History_Report.php', 'FOG\\Reports\\Hosts_And_Users' => $baseDir . '/src/Reports/Hosts_And_Users.php', 'FOG\\Reports\\Imaging_Report' => $baseDir . '/src/Reports/Imaging_Report.php', + 'FOG\\Reports\\Installed_Software' => $baseDir . '/src/Reports/Installed_Software.php', 'FOG\\Reports\\Pending_MAC_List' => $baseDir . '/src/Reports/Pending_MAC_List.php', + 'FOG\\Reports\\Printer_Deployment' => $baseDir . '/src/Reports/Printer_Deployment.php', 'FOG\\Reports\\Product_Keys' => $baseDir . '/src/Reports/Product_Keys.php', 'FOG\\Reports\\Run_History' => $baseDir . '/src/Reports/Run_History.php', 'FOG\\Reports\\Snapin_List' => $baseDir . '/src/Reports/Snapin_List.php', 'FOG\\Reports\\Snapin_Report' => $baseDir . '/src/Reports/Snapin_Report.php', + 'FOG\\Reports\\Software_Report' => $baseDir . '/src/Reports/Software_Report.php', 'FOG\\Reports\\Storage_Report' => $baseDir . '/src/Reports/Storage_Report.php', + 'FOG\\Reports\\User_Sessions' => $baseDir . '/src/Reports/User_Sessions.php', 'FOG\\Router\\HTTPResponseCodes' => $baseDir . '/src/Router/HTTPResponseCodes.php', + 'FOG\\Router\\LongestFirstRouteParser' => $baseDir . '/src/Router/LongestFirstRouteParser.php', 'FOG\\Router\\OpenAPI' => $baseDir . '/src/Router/OpenAPI.php', 'FOG\\Router\\Route' => $baseDir . '/src/Router/Route.php', 'FOG\\Service\\FOGItemScanner' => $baseDir . '/src/Service/FOGItemScanner.php', @@ -278,6 +343,8 @@ 'FOG\\TaskHandling\\TaskingElement' => $baseDir . '/src/TaskHandling/TaskingElement.php', 'FOG\\Util\\FOGCron' => $baseDir . '/src/Util/FOGCron.php', 'FOG\\Util\\FOGLogPaths' => $baseDir . '/src/Util/FOGLogPaths.php', + 'FOG\\Util\\MassEdit' => $baseDir . '/src/Util/MassEdit.php', + 'FOG\\Util\\SharedHostValues' => $baseDir . '/src/Util/SharedHostValues.php', 'FOG\\Util\\Timer' => $baseDir . '/src/Util/Timer.php', 'FastRoute\\BadRouteException' => $vendorDir . '/nikic/fast-route/src/BadRouteException.php', 'FastRoute\\DataGenerator' => $vendorDir . '/nikic/fast-route/src/DataGenerator.php', diff --git a/packages/web/vendor/composer/autoload_static.php b/packages/web/vendor/composer/autoload_static.php index 31295c8750..75869a919b 100644 --- a/packages/web/vendor/composer/autoload_static.php +++ b/packages/web/vendor/composer/autoload_static.php @@ -44,6 +44,24 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 public static $classMap = array ( 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', + 'FOG\\Agent\\DirectoryFacts' => __DIR__ . '/../..' . '/src/Agent/DirectoryFacts.php', + 'FOG\\Agent\\DirectoryJoin' => __DIR__ . '/../..' . '/src/Agent/DirectoryJoin.php', + 'FOG\\Agent\\DirectoryPlacement' => __DIR__ . '/../..' . '/src/Agent/DirectoryPlacement.php', + 'FOG\\Agent\\Enrollment' => __DIR__ . '/../..' . '/src/Agent/Enrollment.php', + 'FOG\\Agent\\InventoryFacts' => __DIR__ . '/../..' . '/src/Agent/InventoryFacts.php', + 'FOG\\Agent\\NetworkFacts' => __DIR__ . '/../..' . '/src/Agent/NetworkFacts.php', + 'FOG\\Agent\\Principal' => __DIR__ . '/../..' . '/src/Agent/Principal.php', + 'FOG\\Agent\\PrinterFacts' => __DIR__ . '/../..' . '/src/Agent/PrinterFacts.php', + 'FOG\\Agent\\PrinterSet' => __DIR__ . '/../..' . '/src/Agent/PrinterSet.php', + 'FOG\\Agent\\SecureBootFacts' => __DIR__ . '/../..' . '/src/Agent/SecureBootFacts.php', + 'FOG\\Agent\\Snapins' => __DIR__ . '/../..' . '/src/Agent/Snapins.php', + 'FOG\\Agent\\SoftwareFacts' => __DIR__ . '/../..' . '/src/Agent/SoftwareFacts.php', + 'FOG\\Agent\\SoftwareSet' => __DIR__ . '/../..' . '/src/Agent/SoftwareSet.php', + 'FOG\\Agent\\State' => __DIR__ . '/../..' . '/src/Agent/State.php', + 'FOG\\Agent\\Token' => __DIR__ . '/../..' . '/src/Agent/Token.php', + 'FOG\\Agent\\UserSessions' => __DIR__ . '/../..' . '/src/Agent/UserSessions.php', + 'FOG\\Agent\\WakeRelay' => __DIR__ . '/../..' . '/src/Agent/WakeRelay.php', + 'FOG\\Assign\\Resolver' => __DIR__ . '/../..' . '/src/Assign/Resolver.php', 'FOG\\Audit\\ActivityWindow' => __DIR__ . '/../..' . '/src/Audit/ActivityWindow.php', 'FOG\\Audit\\Audit' => __DIR__ . '/../..' . '/src/Audit/Audit.php', 'FOG\\Audit\\AuditStats' => __DIR__ . '/../..' . '/src/Audit/AuditStats.php', @@ -77,6 +95,7 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Base\\LoadGlobals' => __DIR__ . '/../..' . '/src/Base/LoadGlobals.php', 'FOG\\Base\\Page' => __DIR__ . '/../..' . '/src/Base/Page.php', 'FOG\\Base\\PluginTask' => __DIR__ . '/../..' . '/src/Base/PluginTask.php', + 'FOG\\Base\\SmbiosIdentity' => __DIR__ . '/../..' . '/src/Base/SmbiosIdentity.php', 'FOG\\Base\\StorageEpoch' => __DIR__ . '/../..' . '/src/Base/StorageEpoch.php', 'FOG\\Base\\System' => __DIR__ . '/../..' . '/src/Base/System.php', 'FOG\\Boot\\BootMenuBase' => __DIR__ . '/../..' . '/src/Boot/BootMenuBase.php', @@ -84,9 +103,10 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Boot\\Registration' => __DIR__ . '/../..' . '/src/Boot/Registration.php', 'FOG\\Boot\\SecureBootState' => __DIR__ . '/../..' . '/src/Boot/SecureBootState.php', 'FOG\\Boot\\UbootBootMenu' => __DIR__ . '/../..' . '/src/Boot/UbootBootMenu.php', + 'FOG\\Boot\\UbootRenderHalted' => __DIR__ . '/../..' . '/src/Boot/UbootRenderHalted.php', + 'FOG\\Boot\\UbootTftpSync' => __DIR__ . '/../..' . '/src/Boot/UbootTftpSync.php', 'FOG\\Boot\\WakeOnLan' => __DIR__ . '/../..' . '/src/Boot/WakeOnLan.php', 'FOG\\Client\\Autologout' => __DIR__ . '/../..' . '/src/Client/Autologout.php', - 'FOG\\Client\\DisplayManager' => __DIR__ . '/../..' . '/src/Client/DisplayManager.php', 'FOG\\Client\\FOGClient' => __DIR__ . '/../..' . '/src/Client/FOGClient.php', 'FOG\\Client\\HostnameChanger' => __DIR__ . '/../..' . '/src/Client/HostnameChanger.php', 'FOG\\Client\\Jobs' => __DIR__ . '/../..' . '/src/Client/Jobs.php', @@ -115,18 +135,33 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Hooks\\SubMenuData' => __DIR__ . '/../..' . '/src/Hooks/SubMenuData.php', 'FOG\\Hooks\\Template' => __DIR__ . '/../..' . '/src/Hooks/Template.php', 'FOG\\Items\\APIToken' => __DIR__ . '/../..' . '/src/Items/APIToken.php', + 'FOG\\Items\\AgentEnrollToken' => __DIR__ . '/../..' . '/src/Items/AgentEnrollToken.php', + 'FOG\\Items\\AgentEnrollment' => __DIR__ . '/../..' . '/src/Items/AgentEnrollment.php', + 'FOG\\Items\\AgentWake' => __DIR__ . '/../..' . '/src/Items/AgentWake.php', 'FOG\\Items\\Architecture' => __DIR__ . '/../..' . '/src/Items/Architecture.php', 'FOG\\Items\\AuditChange' => __DIR__ . '/../..' . '/src/Items/AuditChange.php', 'FOG\\Items\\AuditLog' => __DIR__ . '/../..' . '/src/Items/AuditLog.php', + 'FOG\\Items\\BootFile' => __DIR__ . '/../..' . '/src/Items/BootFile.php', 'FOG\\Items\\DMIKey' => __DIR__ . '/../..' . '/src/Items/DMIKey.php', 'FOG\\Items\\FileDeleteQueue' => __DIR__ . '/../..' . '/src/Items/FileDeleteQueue.php', 'FOG\\Items\\Group' => __DIR__ . '/../..' . '/src/Items/Group.php', 'FOG\\Items\\GroupAssociation' => __DIR__ . '/../..' . '/src/Items/GroupAssociation.php', + 'FOG\\Items\\GroupModuleAssociation' => __DIR__ . '/../..' . '/src/Items/GroupModuleAssociation.php', + 'FOG\\Items\\GroupPowerManagement' => __DIR__ . '/../..' . '/src/Items/GroupPowerManagement.php', + 'FOG\\Items\\GroupPrinterAssociation' => __DIR__ . '/../..' . '/src/Items/GroupPrinterAssociation.php', + 'FOG\\Items\\GroupSnapinAssociation' => __DIR__ . '/../..' . '/src/Items/GroupSnapinAssociation.php', + 'FOG\\Items\\GroupSoftwareAssociation' => __DIR__ . '/../..' . '/src/Items/GroupSoftwareAssociation.php', 'FOG\\Items\\History' => __DIR__ . '/../..' . '/src/Items/History.php', 'FOG\\Items\\HookEvent' => __DIR__ . '/../..' . '/src/Items/HookEvent.php', 'FOG\\Items\\Host' => __DIR__ . '/../..' . '/src/Items/Host.php', 'FOG\\Items\\HostAutoLogout' => __DIR__ . '/../..' . '/src/Items/HostAutoLogout.php', - 'FOG\\Items\\HostScreenSetting' => __DIR__ . '/../..' . '/src/Items/HostScreenSetting.php', + 'FOG\\Items\\HostDirectory' => __DIR__ . '/../..' . '/src/Items/HostDirectory.php', + 'FOG\\Items\\HostFactState' => __DIR__ . '/../..' . '/src/Items/HostFactState.php', + 'FOG\\Items\\HostNetwork' => __DIR__ . '/../..' . '/src/Items/HostNetwork.php', + 'FOG\\Items\\HostPrinter' => __DIR__ . '/../..' . '/src/Items/HostPrinter.php', + 'FOG\\Items\\HostSoftware' => __DIR__ . '/../..' . '/src/Items/HostSoftware.php', + 'FOG\\Items\\HostSpooler' => __DIR__ . '/../..' . '/src/Items/HostSpooler.php', + 'FOG\\Items\\HostUserSession' => __DIR__ . '/../..' . '/src/Items/HostUserSession.php', 'FOG\\Items\\Image' => __DIR__ . '/../..' . '/src/Items/Image.php', 'FOG\\Items\\ImageAssociation' => __DIR__ . '/../..' . '/src/Items/ImageAssociation.php', 'FOG\\Items\\ImagePartitionType' => __DIR__ . '/../..' . '/src/Items/ImagePartitionType.php', @@ -169,6 +204,9 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Items\\SnapinGroupAssociation' => __DIR__ . '/../..' . '/src/Items/SnapinGroupAssociation.php', 'FOG\\Items\\SnapinJob' => __DIR__ . '/../..' . '/src/Items/SnapinJob.php', 'FOG\\Items\\SnapinTask' => __DIR__ . '/../..' . '/src/Items/SnapinTask.php', + 'FOG\\Items\\Software' => __DIR__ . '/../..' . '/src/Items/Software.php', + 'FOG\\Items\\SoftwareAssociation' => __DIR__ . '/../..' . '/src/Items/SoftwareAssociation.php', + 'FOG\\Items\\SoftwareStatus' => __DIR__ . '/../..' . '/src/Items/SoftwareStatus.php', 'FOG\\Items\\StorageGroup' => __DIR__ . '/../..' . '/src/Items/StorageGroup.php', 'FOG\\Items\\StorageNode' => __DIR__ . '/../..' . '/src/Items/StorageNode.php', 'FOG\\Items\\Task' => __DIR__ . '/../..' . '/src/Items/Task.php', @@ -182,18 +220,33 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Items\\UserPref' => __DIR__ . '/../..' . '/src/Items/UserPref.php', 'FOG\\Items\\UserTracking' => __DIR__ . '/../..' . '/src/Items/UserTracking.php', 'FOG\\Managers\\APITokenManager' => __DIR__ . '/../..' . '/src/Managers/APITokenManager.php', + 'FOG\\Managers\\AgentEnrollTokenManager' => __DIR__ . '/../..' . '/src/Managers/AgentEnrollTokenManager.php', + 'FOG\\Managers\\AgentEnrollmentManager' => __DIR__ . '/../..' . '/src/Managers/AgentEnrollmentManager.php', + 'FOG\\Managers\\AgentWakeManager' => __DIR__ . '/../..' . '/src/Managers/AgentWakeManager.php', 'FOG\\Managers\\ArchitectureManager' => __DIR__ . '/../..' . '/src/Managers/ArchitectureManager.php', 'FOG\\Managers\\AuditChangeManager' => __DIR__ . '/../..' . '/src/Managers/AuditChangeManager.php', 'FOG\\Managers\\AuditLogManager' => __DIR__ . '/../..' . '/src/Managers/AuditLogManager.php', + 'FOG\\Managers\\BootFileManager' => __DIR__ . '/../..' . '/src/Managers/BootFileManager.php', 'FOG\\Managers\\DMIKeyManager' => __DIR__ . '/../..' . '/src/Managers/DMIKeyManager.php', 'FOG\\Managers\\FileDeleteQueueManager' => __DIR__ . '/../..' . '/src/Managers/FileDeleteQueueManager.php', 'FOG\\Managers\\GroupAssociationManager' => __DIR__ . '/../..' . '/src/Managers/GroupAssociationManager.php', 'FOG\\Managers\\GroupManager' => __DIR__ . '/../..' . '/src/Managers/GroupManager.php', + 'FOG\\Managers\\GroupModuleAssociationManager' => __DIR__ . '/../..' . '/src/Managers/GroupModuleAssociationManager.php', + 'FOG\\Managers\\GroupPowerManagementManager' => __DIR__ . '/../..' . '/src/Managers/GroupPowerManagementManager.php', + 'FOG\\Managers\\GroupPrinterAssociationManager' => __DIR__ . '/../..' . '/src/Managers/GroupPrinterAssociationManager.php', + 'FOG\\Managers\\GroupSnapinAssociationManager' => __DIR__ . '/../..' . '/src/Managers/GroupSnapinAssociationManager.php', + 'FOG\\Managers\\GroupSoftwareAssociationManager' => __DIR__ . '/../..' . '/src/Managers/GroupSoftwareAssociationManager.php', 'FOG\\Managers\\HistoryManager' => __DIR__ . '/../..' . '/src/Managers/HistoryManager.php', 'FOG\\Managers\\HookEventManager' => __DIR__ . '/../..' . '/src/Managers/HookEventManager.php', 'FOG\\Managers\\HostAutoLogoutManager' => __DIR__ . '/../..' . '/src/Managers/HostAutoLogoutManager.php', + 'FOG\\Managers\\HostDirectoryManager' => __DIR__ . '/../..' . '/src/Managers/HostDirectoryManager.php', + 'FOG\\Managers\\HostFactStateManager' => __DIR__ . '/../..' . '/src/Managers/HostFactStateManager.php', 'FOG\\Managers\\HostManager' => __DIR__ . '/../..' . '/src/Managers/HostManager.php', - 'FOG\\Managers\\HostScreenSettingManager' => __DIR__ . '/../..' . '/src/Managers/HostScreenSettingManager.php', + 'FOG\\Managers\\HostNetworkManager' => __DIR__ . '/../..' . '/src/Managers/HostNetworkManager.php', + 'FOG\\Managers\\HostPrinterManager' => __DIR__ . '/../..' . '/src/Managers/HostPrinterManager.php', + 'FOG\\Managers\\HostSoftwareManager' => __DIR__ . '/../..' . '/src/Managers/HostSoftwareManager.php', + 'FOG\\Managers\\HostSpoolerManager' => __DIR__ . '/../..' . '/src/Managers/HostSpoolerManager.php', + 'FOG\\Managers\\HostUserSessionManager' => __DIR__ . '/../..' . '/src/Managers/HostUserSessionManager.php', 'FOG\\Managers\\ImageAssociationManager' => __DIR__ . '/../..' . '/src/Managers/ImageAssociationManager.php', 'FOG\\Managers\\ImageManager' => __DIR__ . '/../..' . '/src/Managers/ImageManager.php', 'FOG\\Managers\\ImagePartitionTypeManager' => __DIR__ . '/../..' . '/src/Managers/ImagePartitionTypeManager.php', @@ -235,6 +288,9 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Managers\\SnapinJobManager' => __DIR__ . '/../..' . '/src/Managers/SnapinJobManager.php', 'FOG\\Managers\\SnapinManager' => __DIR__ . '/../..' . '/src/Managers/SnapinManager.php', 'FOG\\Managers\\SnapinTaskManager' => __DIR__ . '/../..' . '/src/Managers/SnapinTaskManager.php', + 'FOG\\Managers\\SoftwareAssociationManager' => __DIR__ . '/../..' . '/src/Managers/SoftwareAssociationManager.php', + 'FOG\\Managers\\SoftwareManager' => __DIR__ . '/../..' . '/src/Managers/SoftwareManager.php', + 'FOG\\Managers\\SoftwareStatusManager' => __DIR__ . '/../..' . '/src/Managers/SoftwareStatusManager.php', 'FOG\\Managers\\StorageGroupManager' => __DIR__ . '/../..' . '/src/Managers/StorageGroupManager.php', 'FOG\\Managers\\StorageNodeManager' => __DIR__ . '/../..' . '/src/Managers/StorageNodeManager.php', 'FOG\\Managers\\TaskLogManager' => __DIR__ . '/../..' . '/src/Managers/TaskLogManager.php', @@ -248,11 +304,13 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Managers\\UserPrefManager' => __DIR__ . '/../..' . '/src/Managers/UserPrefManager.php', 'FOG\\Managers\\UserTrackingManager' => __DIR__ . '/../..' . '/src/Managers/UserTrackingManager.php', 'FOG\\Net\\FOGFTP' => __DIR__ . '/../..' . '/src/Net/FOGFTP.php', + 'FOG\\Net\\FOGLdap' => __DIR__ . '/../..' . '/src/Net/FOGLdap.php', 'FOG\\Net\\FOGRollingURL' => __DIR__ . '/../..' . '/src/Net/FOGRollingURL.php', 'FOG\\Net\\FOGSSH' => __DIR__ . '/../..' . '/src/Net/FOGSSH.php', 'FOG\\Net\\FOGURLRequests' => __DIR__ . '/../..' . '/src/Net/FOGURLRequests.php', 'FOG\\Net\\Ping' => __DIR__ . '/../..' . '/src/Net/Ping.php', 'FOG\\Pages\\ActivityManagement' => __DIR__ . '/../..' . '/src/Pages/ActivityManagement.php', + 'FOG\\Pages\\AgentActivityManagement' => __DIR__ . '/../..' . '/src/Pages/AgentActivityManagement.php', 'FOG\\Pages\\ApiDocumentation' => __DIR__ . '/../..' . '/src/Pages/ApiDocumentation.php', 'FOG\\Pages\\AuditManagement' => __DIR__ . '/../..' . '/src/Pages/AuditManagement.php', 'FOG\\Pages\\ClientManagement' => __DIR__ . '/../..' . '/src/Pages/ClientManagement.php', @@ -275,25 +333,32 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\Pages\\ServiceConfigurationPage' => __DIR__ . '/../..' . '/src/Pages/ServiceConfigurationPage.php', 'FOG\\Pages\\SiteManagement' => __DIR__ . '/../..' . '/src/Pages/SiteManagement.php', 'FOG\\Pages\\SnapinManagement' => __DIR__ . '/../..' . '/src/Pages/SnapinManagement.php', + 'FOG\\Pages\\SoftwareManagement' => __DIR__ . '/../..' . '/src/Pages/SoftwareManagement.php', 'FOG\\Pages\\StorageGroupManagement' => __DIR__ . '/../..' . '/src/Pages/StorageGroupManagement.php', 'FOG\\Pages\\StorageNodeManagement' => __DIR__ . '/../..' . '/src/Pages/StorageNodeManagement.php', 'FOG\\Pages\\TaskManagement' => __DIR__ . '/../..' . '/src/Pages/TaskManagement.php', 'FOG\\Pages\\UserGroupManagement' => __DIR__ . '/../..' . '/src/Pages/UserGroupManagement.php', 'FOG\\Pages\\UserManagement' => __DIR__ . '/../..' . '/src/Pages/UserManagement.php', 'FOG\\Reports\\Audit_Report' => __DIR__ . '/../..' . '/src/Reports/Audit_Report.php', + 'FOG\\Reports\\Directory_Membership' => __DIR__ . '/../..' . '/src/Reports/Directory_Membership.php', 'FOG\\Reports\\File_Deleter' => __DIR__ . '/../..' . '/src/Reports/File_Deleter.php', 'FOG\\Reports\\Fleet_Report' => __DIR__ . '/../..' . '/src/Reports/Fleet_Report.php', 'FOG\\Reports\\Hardware_Report' => __DIR__ . '/../..' . '/src/Reports/Hardware_Report.php', 'FOG\\Reports\\History_Report' => __DIR__ . '/../..' . '/src/Reports/History_Report.php', 'FOG\\Reports\\Hosts_And_Users' => __DIR__ . '/../..' . '/src/Reports/Hosts_And_Users.php', 'FOG\\Reports\\Imaging_Report' => __DIR__ . '/../..' . '/src/Reports/Imaging_Report.php', + 'FOG\\Reports\\Installed_Software' => __DIR__ . '/../..' . '/src/Reports/Installed_Software.php', 'FOG\\Reports\\Pending_MAC_List' => __DIR__ . '/../..' . '/src/Reports/Pending_MAC_List.php', + 'FOG\\Reports\\Printer_Deployment' => __DIR__ . '/../..' . '/src/Reports/Printer_Deployment.php', 'FOG\\Reports\\Product_Keys' => __DIR__ . '/../..' . '/src/Reports/Product_Keys.php', 'FOG\\Reports\\Run_History' => __DIR__ . '/../..' . '/src/Reports/Run_History.php', 'FOG\\Reports\\Snapin_List' => __DIR__ . '/../..' . '/src/Reports/Snapin_List.php', 'FOG\\Reports\\Snapin_Report' => __DIR__ . '/../..' . '/src/Reports/Snapin_Report.php', + 'FOG\\Reports\\Software_Report' => __DIR__ . '/../..' . '/src/Reports/Software_Report.php', 'FOG\\Reports\\Storage_Report' => __DIR__ . '/../..' . '/src/Reports/Storage_Report.php', + 'FOG\\Reports\\User_Sessions' => __DIR__ . '/../..' . '/src/Reports/User_Sessions.php', 'FOG\\Router\\HTTPResponseCodes' => __DIR__ . '/../..' . '/src/Router/HTTPResponseCodes.php', + 'FOG\\Router\\LongestFirstRouteParser' => __DIR__ . '/../..' . '/src/Router/LongestFirstRouteParser.php', 'FOG\\Router\\OpenAPI' => __DIR__ . '/../..' . '/src/Router/OpenAPI.php', 'FOG\\Router\\Route' => __DIR__ . '/../..' . '/src/Router/Route.php', 'FOG\\Service\\FOGItemScanner' => __DIR__ . '/../..' . '/src/Service/FOGItemScanner.php', @@ -315,6 +380,8 @@ class ComposerStaticInitf1e42438574b3ce50c0c9ee957f57472 'FOG\\TaskHandling\\TaskingElement' => __DIR__ . '/../..' . '/src/TaskHandling/TaskingElement.php', 'FOG\\Util\\FOGCron' => __DIR__ . '/../..' . '/src/Util/FOGCron.php', 'FOG\\Util\\FOGLogPaths' => __DIR__ . '/../..' . '/src/Util/FOGLogPaths.php', + 'FOG\\Util\\MassEdit' => __DIR__ . '/../..' . '/src/Util/MassEdit.php', + 'FOG\\Util\\SharedHostValues' => __DIR__ . '/../..' . '/src/Util/SharedHostValues.php', 'FOG\\Util\\Timer' => __DIR__ . '/../..' . '/src/Util/Timer.php', 'FastRoute\\BadRouteException' => __DIR__ . '/..' . '/nikic/fast-route/src/BadRouteException.php', 'FastRoute\\DataGenerator' => __DIR__ . '/..' . '/nikic/fast-route/src/DataGenerator.php', diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index f2ec80572e..02340771cd 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -603,7 +603,7 @@ parameters: path: packages/web/src/Base/FOGBase.php - - message: '#^Parameter \#1 \$array \(array\{''autologout'', ''displaymanager'', ''hostnamechanger'', ''hostregister'', ''powermanagement'', ''printermanager'', ''snapinclient'', ''software'', \.\.\.\}\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' + message: '#^Parameter \#1 \$array \(array\{''autologout'', ''hostnamechanger'', ''hostregister'', ''powermanagement'', ''printermanager'', ''snapinclient'', ''software'', ''taskreboot'', \.\.\.\}\) to function array_filter does not contain falsy values, the array will always stay the same\.$#' identifier: arrayFilter.same count: 1 path: packages/web/src/Base/FOGBase.php @@ -3860,6 +3860,12 @@ parameters: count: 4 path: packages/web/src/Pages/ServiceConfigurationPage.php + - + message: '#^Parameter \#2 \$value of static method FOG\\Base\\FOGBase\:\:setSetting\(\) expects string, int\<0, max\> given\.$#' + identifier: argument.type + count: 1 + path: packages/web/src/Pages/ServiceConfigurationPage.php + - message: '#^Variable \$Module might not be defined\.$#' identifier: variable.undefined diff --git a/phpstan-tests-baseline.neon b/phpstan-tests-baseline.neon index 25dcb06ecd..3c01a82261 100644 --- a/phpstan-tests-baseline.neon +++ b/phpstan-tests-baseline.neon @@ -85,7 +85,7 @@ parameters: path: tests/agent-printer-facts.test.php - - message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', printers\: ''printermanager''\} on left side of \?\? always exists and is not nullable\.$#' + message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', autologout\: ''autologout'', directory\: ''hostnamechanger'', printers\: ''printermanager'', \.\.\.\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset count: 1 path: tests/agent-printer-facts.test.php @@ -139,7 +139,7 @@ parameters: path: tests/agent-printer-facts.test.php - - message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', directory\: ''hostnamechanger'', printers\: ''printermanager'', wake\: ''powermanagement''\} on left side of \?\? always exists and is not nullable\.$#' + message: '#^Offset ''printers'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', autologout\: ''autologout'', directory\: ''hostnamechanger'', printers\: ''printermanager'', \.\.\.\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset count: 1 path: tests/agent-printer-facts.test.php @@ -235,7 +235,7 @@ parameters: path: tests/agent-wake-relay.test.php - - message: '#^Offset ''wake'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', directory\: ''hostnamechanger'', printers\: ''printermanager'', wake\: ''powermanagement''\} on left side of \?\? always exists and is not nullable\.$#' + message: '#^Offset ''wake'' on array\{hostname\: ''hostnamechanger'', taskreboot\: ''taskreboot'', snapin\: ''snapinclient'', software\: ''software'', power\: ''powermanagement'', autologout\: ''autologout'', directory\: ''hostnamechanger'', printers\: ''printermanager'', \.\.\.\} on left side of \?\? always exists and is not nullable\.$#' identifier: nullCoalesce.offset count: 2 path: tests/agent-wake-relay.test.php diff --git a/tests/fixtures/route-cascade-contract.txt b/tests/fixtures/route-cascade-contract.txt index 3429706408..956fc8d175 100644 --- a/tests/fixtures/route-cascade-contract.txt +++ b/tests/fixtures/route-cascade-contract.txt @@ -9,7 +9,6 @@ history (nothing) hookevent (nothing) host groupassociation hostID host hostautologout hostID -host hostscreensetting hostID host inventory hostID host macaddressassociation hostID host moduleassociation hostID @@ -27,7 +26,6 @@ hostdirectory (nothing) hostfactstate (nothing) hostnetwork (nothing) hostprinter (nothing) -hostscreensetting (nothing) hostsoftware (nothing) hostspooler (nothing) hostusersession (nothing) diff --git a/tests/fixtures/route-column-contract.txt b/tests/fixtures/route-column-contract.txt index 022978d5f5..bb93c5c738 100644 --- a/tests/fixtures/route-column-contract.txt +++ b/tests/fixtures/route-column-contract.txt @@ -172,16 +172,6 @@ hostprinter 7 hpDriver driver - - hostprinter 8 hpDefault isDefault - - hostprinter 9 hpShared shared - - hostprinter 10 hpObservedAt observedAt - - -hostscreensetting 0 hssID id - - -hostscreensetting 1 hssID DT_RowId f - -hostscreensetting 2 hssHostID hostID - - -hostscreensetting 3 hssHostID hostLink f:classname host -hostscreensetting 4 hssWidth width - - -hostscreensetting 5 hssHeight height - - -hostscreensetting 6 hssRefresh refresh - - -hostscreensetting 7 hssOrientation orientation - - -hostscreensetting 8 hssOther1 other1 - - -hostscreensetting 9 hssOther2 other2 - - hostsoftware 0 hsID id - - hostsoftware 1 hsID DT_RowId f - hostsoftware 2 hsHostID hostID - - diff --git a/tests/foreign-key-map.test.php b/tests/foreign-key-map.test.php index 4525c3ea5e..fffd44a5e1 100644 --- a/tests/foreign-key-map.test.php +++ b/tests/foreign-key-map.test.php @@ -262,7 +262,6 @@ 'moduleStatusByHost.msHostID', 'moduleStatusByHost.msModuleID', 'inventory.iHostID', - 'hostScreenSettings.hssHostID', 'hostAutoLogOut.haloHostID', 'powerManagement.pmHostID', 'greenFog.gfHostID', diff --git a/tests/group-grants-are-owned.test.php b/tests/group-grants-are-owned.test.php index d50cd4df8f..91a7aedd52 100644 --- a/tests/group-grants-are-owned.test.php +++ b/tests/group-grants-are-owned.test.php @@ -473,7 +473,7 @@ function ($sql) { $pushes = [ 'groupGeneralPost' => ['HostManager', 'productKey', 'bootTypeExit'], 'groupPrinterPost' => ['printerLevel', 'confirmlevelup'], - 'groupModulePost' => ['setDisp', 'setAlo', 'confirmenforcesend'], + 'groupModulePost' => ['setAlo', 'confirmenforcesend'], ]; foreach ($pushes as $method => $needles) { $body = codeOnly(methodBody($pageSrc, $method)); diff --git a/tests/mass-edit-endpoint-is-gated.test.php b/tests/mass-edit-endpoint-is-gated.test.php index 301570a793..d91d406cbb 100644 --- a/tests/mass-edit-endpoint-is-gated.test.php +++ b/tests/mass-edit-endpoint-is-gated.test.php @@ -275,35 +275,22 @@ // --- The row-backed half -------------------------------------------------- // -// Auto-logout and screen resolution are not `hosts` columns; they are one row -// per host in their own tables, written delete-then-insert. Two properties -// have to hold and neither is visible from a passing request: the row fields -// must be resolved SEPARATELY from the column fields (so a row key can never -// reach columnUpdates()), and the composite one must go through -// resolveComposite() rather than resolve(), whose safety rule is that an -// array is never a value. +// Auto-logout is not a `hosts` column; it is one row per host in its own +// table, written delete-then-insert. The row fields must be resolved +// SEPARATELY from the column fields, so a row key can never reach +// columnUpdates(). $rows = $methodBody($source, 'private function massEditRowFields()'); $check('massEditRowFields() is still findable', null !== $rows); $rows = (string)$rows; $check( - 'the row-backed fields are auto-logout and resolution', + 'the row-backed fields include auto-logout', false !== strpos($rows, "'autologout' => [") - && false !== strpos($rows, "'resolution' => [") -); -$check( - 'the resolution is declared composite', - 1 === preg_match( - "/'resolution' => \[[^\]]*'composite' => true/s", - $rows - ) ); $rowKeys = strpos($body, 'massEditRowFields()'); $colUpdates = strpos($body, 'columnUpdates('); -$composite = strpos($body, 'resolveComposite('); $applyRows = strpos($body, 'massEditApplyRows('); $check('the endpoint asks for the row-backed fields', false !== $rowKeys); -$check('the endpoint resolves the composite one', false !== $composite); $check('the endpoint applies the row-backed half', false !== $applyRows); // The load-bearing one: columnUpdates() is handed $coreFields, and the row @@ -327,7 +314,7 @@ ); // A row-backed-only submission must still count as work. Without this the -// "nothing was set to change" refusal fires on a resolution-only edit and +// "nothing was set to change" refusal fires on a row-backed-only edit and // the operator is told their submission was empty when it was not. $check( 'the touched list includes the row-backed instructions', @@ -344,24 +331,23 @@ // CLEAR is the delete with no insert -- no row IS the absence of an override. // The tell that this is wrong is an insert that runs unconditionally, which -// would turn CLEAR into "set to zero" for auto-logout and "set to 0x0" for -// the resolution. +// would turn CLEAR into "set to zero" for auto-logout. $check( - 'the row arms insert only on SET', - 2 === substr_count($apply, 'MassEdit::SET === ') - && 2 === substr_count($apply, 'insertBatch(') + 'the row arm inserts only on SET', + 1 === substr_count($apply, 'MassEdit::SET === ') + && 1 === substr_count($apply, 'insertBatch(') ); $check( - 'the row arms delete on both SET and CLEAR', - 2 === substr_count($apply, 'Route::deletemass(') - && 2 === substr_count($apply, 'MassEdit::LEAVE !== ') + 'the row arm deletes on both SET and CLEAR', + 1 === substr_count($apply, 'Route::deletemass(') + && 1 === substr_count($apply, 'MassEdit::LEAVE !== ') ); // One statement per field regardless of selection size, same reason the // column half is one UPDATE. A per-host loop here is the shape ADR 0038 // decision 4 is about. $check( - 'the row arms do not write one host at a time', + 'the row arm does not write one host at a time', false === strpos($apply, '->save()') && false === strpos($apply, 'foreach ($resolved') ); diff --git a/tests/mass-edit-fails-closed.test.php b/tests/mass-edit-fails-closed.test.php index b57319e3c5..624dfdf6db 100644 --- a/tests/mass-edit-fails-closed.test.php +++ b/tests/mass-edit-fails-closed.test.php @@ -239,80 +239,6 @@ && ['kernel' => ''] === MassEdit::columnUpdates($resolved, $spec) ); -// --- Composite values ----------------------------------------------------- -// -// resolveComposite() exists because a screen resolution is three numbers -// written as one row, and "set the width, leave the height" has no meaning at -// the storage layer. It must fail closed in BOTH directions: a composite key -// posting a scalar, and a composite key posting an array with something -// unusable in it. - -$resolved = MassEdit::resolveComposite( - ['resolution'], - ['resolution' => MassEdit::SET], - ['resolution' => ['x' => ' 1024 ', 'y' => '768', 'r' => '60']] -); -$check( - 'a composite SET keeps its parts, trimmed', - MassEdit::SET === $resolved['resolution']['action'] - && ['x' => '1024', 'y' => '768', 'r' => '60'] - === $resolved['resolution']['value'] -); - -$resolved = MassEdit::resolveComposite( - ['resolution'], - ['resolution' => MassEdit::SET], - ['resolution' => '1024x768@60'] -); -$check( - 'a composite posting a scalar falls back to LEAVE', - MassEdit::LEAVE === $resolved['resolution']['action'] -); - -$resolved = MassEdit::resolveComposite( - ['resolution'], - ['resolution' => MassEdit::SET], - ['resolution' => ['x' => '1024', 'y' => ['nested'], 'r' => '60']] -); -$check( - 'one unusable part discards the whole composite instruction', - MassEdit::LEAVE === $resolved['resolution']['action'] - && [] === $resolved['resolution']['value'] -); - -$resolved = MassEdit::resolveComposite( - ['resolution'], - ['resolution' => MassEdit::CLEAR], - [] -); -$check( - 'a composite CLEAR needs no value', - MassEdit::CLEAR === $resolved['resolution']['action'] -); - -$resolved = MassEdit::resolveComposite(['resolution'], null, null); -$check( - 'a composite with nothing posted is LEAVE and still present', - array_key_exists('resolution', $resolved) - && MassEdit::LEAVE === $resolved['resolution']['action'] -); - -// A composite can never become a column update, even if a spec names a field -// for it. The spec is partly written by plugins, and writing the string -// "Array" into a column is the failure this guard prevents. -$resolved = MassEdit::resolveComposite( - ['resolution'], - ['resolution' => MassEdit::SET], - ['resolution' => ['x' => '1024', 'y' => '768', 'r' => '60']] -); -$check( - 'a composite is never turned into a column update', - [] === MassEdit::columnUpdates( - $resolved, - ['resolution' => ['field' => 'kernel', 'empty' => '']] - ) -); - if (count($failures)) { fwrite(STDERR, "FAIL: the mass edit does not fail closed:\n"); foreach ($failures as $f) { diff --git a/tests/mass-edit-form.test.php b/tests/mass-edit-form.test.php index 554c90f250..c6400d60c8 100644 --- a/tests/mass-edit-form.test.php +++ b/tests/mass-edit-form.test.php @@ -227,21 +227,6 @@ function () use ($tmp) { && false !== strpos($level, 'value="2"') ); -// The composite. Its parts must arrive as an ARRAY under one key, which is -// what lets MassEdit::resolveComposite() read them without parsing anything -// -- and is why there is no 1024x768@60 string anywhere in this form. -$res = $call('massEditValueControl', ['resolution', $rows['resolution']]); -$check( - 'the resolution posts three parts under one key', - false !== strpos($res, 'name="value[resolution][x]"') - && false !== strpos($res, 'name="value[resolution][y]"') - && false !== strpos($res, 'name="value[resolution][r]"') -); -$check( - 'the resolution is not encoded into one string field', - false === strpos($res, 'name="value[resolution]"') -); - // --- Control ids ---------------------------------------------------------- $check( @@ -348,28 +333,12 @@ function () use ($tmp) { ) ); -// The row-backed hints ask their own tables, and the resolution's three -// columns are combined into one answer rather than reported separately. +// The row-backed hint asks its own table. $check( 'the auto-logout hint reads its own table', null !== $hintBody && false !== strpos((string)$hintBody, "'hostAutoLogOut'") ); -$check( - 'the resolution hint reads its own table', - null !== $hintBody - && false !== strpos((string)$hintBody, "'hostScreenSettings'") -); -$check( - 'the resolution is uniform only when all three parts agree', - null !== $hintBody - && 1 === preg_match( - '/\$uniform = !empty\(\$disp\[.x.\]\[.uniform.\]\)\s*' - . '&& !empty\(\$disp\[.y.\]\[.uniform.\]\)\s*' - . '&& !empty\(\$disp\[.r.\]\[.uniform.\]\)/s', - (string)$hintBody - ) -); // --- Where the button sits ------------------------------------------------ // @@ -582,7 +551,6 @@ function () use ($tmp) { 'useAD' => 'ad', 'ADPass' => 'ad', 'autologout' => 'client', - 'resolution' => 'client' ]; $wrong = []; foreach ($where as $key => $tab) { From f8ed9fdbafab85dd76a29a8124f27a80dfeea134 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 18:03:02 +0000 Subject: [PATCH 106/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 65cdd68371..1d2410eba7 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10384,6 +10384,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 68678da903..c74d09adea 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10393,6 +10393,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cd30d0ba82..10c6d2b974 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10551,6 +10551,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index db17348d4e..de80c0822f 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index df040c4a15..0aa4009094 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10377,6 +10377,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 381b625ed6..27e3a7186b 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10100,6 +10100,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 241ae46a64..35c522f171 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10057,6 +10057,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 045e80fba0..e2460120d6 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8902,6 +8902,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index ce911ede1f..1634484193 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 19f3d8f338..3b8d0863be 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From beda933c0df538dc325e68fb15b89c2f6a8ed9a0 Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 13:11:52 -0500 Subject: [PATCH 107/117] Rehearsal baseline: one fewer foreign key, because the table it pointed at is gone Missed in the Display Manager removal. Dropping hostScreenSettings drops its hssHostID -> hosts.hostID constraint, so the rehearsal's decade profile now declares 98 applicable constraints and finds 96, not 99 and 97. MISSING stays 2: both are the seed-induced refusals the block below already documents, and neither is this table. The rehearsal itself replayed steps 431 and 432 against MariaDB 11.8 without complaint -- "every seeded row landed (no REFUSED)" passed and only the count line differed. That is the database execution the parent commit recorded as NOT VERIFIED HERE. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- tests/fixtures/upgrade-rehearsal-baseline.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/fixtures/upgrade-rehearsal-baseline.txt b/tests/fixtures/upgrade-rehearsal-baseline.txt index 8fcbc01e98..41d3b8b233 100644 --- a/tests/fixtures/upgrade-rehearsal-baseline.txt +++ b/tests/fixtures/upgrade-rehearsal-baseline.txt @@ -1,5 +1,5 @@ - constraints declared and applicable here: 99 - constraints actually present: 97 + constraints declared and applicable here: 98 + constraints actually present: 96 MISSING: 2 fk_hostMAC_hmHostID CASCADE orphan rows: 0 fk_nfsGroupMembers_ngmGroupID RESTRICT orphan rows: 1 From 9d8cc1db7d8c0a4f700f68b97d7da79011cb5c9f Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 13:23:23 -0500 Subject: [PATCH 108/117] Host::getAlo() cached the first host's answer and gave it to every host after Found while proving the new autologout desired-state block on the lab. I set host 239's auto logout to 10, read the block, set it to 3, and read again -- and got 10 both times. Then I set it back to 0 and getAlo() still said 10, while the database said 0. self::$_hostalo was declared `= []` but assigned a scalar, and the guard was `!empty()`. So the first host whose getAlo() runs in a request wins, and every host read after it in that same request gets that host's number, silently and with no error. The `!empty()` guard has a second face: a host whose auto logout is legitimately 0 never caches at all, so it re-queries on every read -- which is why single-host requests looked fine and hid this. It is now keyed by host id, guarded with array_key_exists so a real 0 caches, and setAlo() drops this host's entry so a save and a read in the same request agree. This matters more than it did yesterday. Until now the only readers were one host per request -- the legacy client endpoint and the host edit page -- so the bug was reachable but rarely reached. State::desired() reads getAlo() for the agent, and anything that walks a list of hosts (a group operation, the host mass edit, a REST list with the field serialized) hits it directly: the whole page would report one host's auto logout time. $_hostscreen had the same shape and went with Display Manager in the parent commit. $_hostalo was the last one; there are no other `!empty(self::$_...)` cache guards left in src/Items. VERIFIED: same round trip re-run against the deployed fix now reports 10, then withheld below the floor, then 0. sh tests/run-all.sh 335 passed, 1 failed (certificate-table, inherited). Both phpstan passes clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../de_DE.UTF-8/LC_MESSAGES/messages.po | 1 - .../en_US.UTF-8/LC_MESSAGES/messages.po | 1 - .../es_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 - .../fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 - .../it_IT.UTF-8/LC_MESSAGES/messages.po | 1 - .../ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/management/languages/messages.pot | 1 - .../pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 - .../zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 - packages/web/src/Items/Host.php | 16 +++++++++++----- 11 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 1d2410eba7..65cdd68371 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10384,7 +10384,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index c74d09adea..68678da903 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10393,7 +10393,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 10c6d2b974..cd30d0ba82 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10551,7 +10551,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index de80c0822f..db17348d4e 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 0aa4009094..df040c4a15 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10377,7 +10377,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 27e3a7186b..381b625ed6 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10100,7 +10100,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 35c522f171..241ae46a64 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10057,7 +10057,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e2460120d6..045e80fba0 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8902,7 +8902,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 1634484193..ce911ede1f 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10380,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 3b8d0863be..19f3d8f338 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10380,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/src/Items/Host.php b/packages/web/src/Items/Host.php index 8f8dc6748b..4da69c21c6 100644 --- a/packages/web/src/Items/Host.php +++ b/packages/web/src/Items/Host.php @@ -239,9 +239,9 @@ class Host extends FOGController * @var array */ /** - * ALO time val + * ALO time val, keyed by host id * - * @var int + * @var array */ private static $_hostalo = []; /** @@ -538,10 +538,13 @@ public function updateDefault($printerid) */ private function _setAlo() { - if (!empty(self::$_hostalo)) { + $id = (int)$this->get('id'); + // array_key_exists, not !empty: a host whose auto logout is legitimately + // 0 must still cache, or every read re-queries. + if (array_key_exists($id, self::$_hostalo)) { return; } - self::$_hostalo = ( + self::$_hostalo[$id] = ( $this->get('hostalo')->get('time') ?: self::getSetting('FOG_CLIENT_AUTOLOGOFF_MIN') ); @@ -554,7 +557,7 @@ private function _setAlo() public function getAlo() { $this->_setAlo(); - return self::$_hostalo; + return self::$_hostalo[(int)$this->get('id')]; } /** * Sets the auto logout time @@ -565,6 +568,9 @@ public function getAlo() */ public function setAlo($time) { + // The write invalidates this host's cached read, so a save and a + // getAlo() in the same request agree. + unset(self::$_hostalo[(int)$this->get('id')]); return $this->get('hostalo') ->set('hostID', $this->get('id')) ->set('time', $time) From 014635c5d8858edb8b60e726817caed2a2c342c5 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 18:24:51 +0000 Subject: [PATCH 109/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 65cdd68371..1d2410eba7 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10384,6 +10384,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 68678da903..c74d09adea 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10393,6 +10393,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index cd30d0ba82..10c6d2b974 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10551,6 +10551,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index db17348d4e..de80c0822f 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index df040c4a15..0aa4009094 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10377,6 +10377,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 381b625ed6..27e3a7186b 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10100,6 +10100,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 241ae46a64..35c522f171 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10057,6 +10057,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 045e80fba0..e2460120d6 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8902,6 +8902,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index ce911ede1f..1634484193 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 19f3d8f338..3b8d0863be 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 0cc989aed8eeca5daefc6aa2a5067c4eb362299a Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 13:33:16 -0500 Subject: [PATCH 110/117] One module row with no tab took down the whole Global Module Settings page Found deploying: https:///fog/management/index.php?node=service returns 500 with "Call to undefined method ServiceConfigurationPage::serviceSoftware()". Not from this branch's Auto Log Out work -- php-fpm logged the same fatal at 08:25 this morning, hours before any of today's deploys, and the failing name has nothing to do with Display Manager. The cause is the dispatcher in edit(): it walks the `modules` table, builds `service` . ucfirst($shortName) and calls it with no check. Schema seeds a `software` module row for the inventory work; its tab has not been written yet. So one row in a database table fatals the page that configures every other module -- Auto Log Out, Snapins, Printer Manager and the rest all became unreachable because of a module unrelated to any of them. It now renders "This module has no settings to configure." instead of fataling. That is honest about the state -- the module exists, it has no settings surface -- and it is not a stub anyone has to remove: the moment serviceSoftware() is written, method_exists() finds it and the real tab renders with no change here. Deliberately NOT added to the $notWhere exclusion list next to clientupdater, dircleanup and usercleanup. Those three are excluded because they are settled -- they will never have a tab. `software` is mid-build, and putting it there would hide the module from an admin and have to be undone by whoever finishes it. editPost() needed nothing: its dispatch is an explicit switch with no default, so an unhandled tab is already a no-op rather than a fatal. VERIFIED: the page renders after the fix (it 500'd before). php -l, phpstan clean, sh tests/run-all.sh 335 passed 1 failed (certificate-table, inherited). Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Ft1sYpi27EW7g798fkkWR7 --- .../languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/en_US.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 8 ++++---- packages/web/management/languages/messages.pot | 4 +++- .../languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 4 +++- .../languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 4 +++- packages/web/src/Pages/ServiceConfigurationPage.php | 10 ++++++++++ 11 files changed, 41 insertions(+), 13 deletions(-) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 1d2410eba7..260bba4364 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10384,7 +10384,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10624,6 +10623,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Bereits registriert als" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index c74d09adea..0222a31aea 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10393,7 +10393,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10632,6 +10631,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Already registered as" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index 10c6d2b974..b5326a27f8 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10551,7 +10551,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10790,6 +10789,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Impresora ya existe" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index de80c0822f..d765c42388 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10385,7 +10385,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10625,6 +10624,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Bereits registriert als" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 0aa4009094..8903e85f5a 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10377,7 +10377,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10616,6 +10615,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Déjà inscrit comme" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 27e3a7186b..55783b84d0 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10100,7 +10100,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10334,6 +10333,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Già registrato come" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index 35c522f171..d596ea04e6 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10057,7 +10057,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10290,6 +10289,10 @@ msgstr "" msgid "This machine already registered as %s" msgstr "既に次の名前で登録されています:" +#, fuzzy +msgid "This module has no settings to configure." +msgstr "このモジュールは旧クライアントでのみ使用されます。" + msgid "This name is not a recognized file in the boot directory." msgstr "" @@ -14540,9 +14543,6 @@ msgstr "" #~ msgid "This module is only used on the old client" #~ msgstr "このモジュールは旧クライアントでのみ使用されます" -#~ msgid "This module is only used on the old client." -#~ msgstr "このモジュールは旧クライアントでのみ使用されます。" - #~ msgid "This section allows you to customize or alter" #~ msgstr "このセクションでは各種設定をカスタマイズできます" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index e2460120d6..0207fd098d 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8902,7 +8902,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -9128,6 +9127,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index 1634484193..a3790f3e61 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10380,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10619,6 +10618,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "Já está registado como" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 3b8d0863be..558769379f 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10380,7 +10380,6 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" -#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" @@ -10619,6 +10618,9 @@ msgstr "" msgid "This machine already registered as %s" msgstr "已注册为" +msgid "This module has no settings to configure." +msgstr "" + msgid "This name is not a recognized file in the boot directory." msgstr "" diff --git a/packages/web/src/Pages/ServiceConfigurationPage.php b/packages/web/src/Pages/ServiceConfigurationPage.php index c34e5e59ce..ac7d765738 100644 --- a/packages/web/src/Pages/ServiceConfigurationPage.php +++ b/packages/web/src/Pages/ServiceConfigurationPage.php @@ -618,6 +618,16 @@ public function edit() 'id' => 'service-' . $Module->shortName, 'generator' => function () use ($Module) { $func = 'service' . ucfirst($Module->shortName); + // A module row whose tab has not been written must not + // take the whole page down with it. `software` is one + // today. The moment the method exists this renders it. + if (!method_exists($this, $func)) { + printf( + '

    %s

    ', + _('This module has no settings to configure.') + ); + return; + } $this->{$func}(); } ]; From 2ba68a50883b5568c11728130493713d647ace30 Mon Sep 17 00:00:00 2001 From: "fog-workflows[bot]" Date: Sat, 5 Sep 2026 18:34:31 +0000 Subject: [PATCH 111/117] Generated Files Sync: regenerate translations and PSR2 formatting (#1707) --- .../web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po | 1 + packages/web/management/languages/messages.pot | 1 + .../web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po | 1 + .../web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po | 1 + 10 files changed, 10 insertions(+) diff --git a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po index 260bba4364..b4a4fc8d2b 100644 --- a/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/de_DE.UTF-8/LC_MESSAGES/messages.po @@ -10384,6 +10384,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po index 0222a31aea..1f739a8dd2 100644 --- a/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/en_US.UTF-8/LC_MESSAGES/messages.po @@ -10393,6 +10393,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po index b5326a27f8..97aee6a9f3 100644 --- a/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/es_ES.UTF-8/LC_MESSAGES/messages.po @@ -10551,6 +10551,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po index d765c42388..da6a88ac8b 100644 --- a/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/eu_ES.UTF-8/LC_MESSAGES/messages.po @@ -10385,6 +10385,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po index 8903e85f5a..c76e17b8a3 100644 --- a/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/fr_FR.UTF-8/LC_MESSAGES/messages.po @@ -10377,6 +10377,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po index 55783b84d0..2192e3d415 100644 --- a/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/it_IT.UTF-8/LC_MESSAGES/messages.po @@ -10100,6 +10100,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po index d596ea04e6..fd84afaacf 100644 --- a/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/ja_JP.UTF-8/LC_MESSAGES/messages.po @@ -10057,6 +10057,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/messages.pot b/packages/web/management/languages/messages.pot index 0207fd098d..d95f2e7dd6 100644 --- a/packages/web/management/languages/messages.pot +++ b/packages/web/management/languages/messages.pot @@ -8902,6 +8902,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po index a3790f3e61..0282ec6467 100644 --- a/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/pt_BR.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" diff --git a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po index 558769379f..9fdfdc1dbe 100644 --- a/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po +++ b/packages/web/management/languages/zh_CN.UTF-8/LC_MESSAGES/messages.po @@ -10380,6 +10380,7 @@ msgstr "" msgid "The term goes in q. limit caps results PER CLASS, not overall, and this route is not paged -- there is no nextUrl to follow. Within a class, names that start with the term sort first. Both fields may also be sent as POST body fields. Also reachable as /search." msgstr "" +#, php-format msgid "The term. Because it is a path segment, a term containing / ? # or % cannot travel this way; use /unisearch?q= instead." msgstr "" From 68ad677105a35b0020fef387ab24292397da902f Mon Sep 17 00:00:00 2001 From: Tom Elliott Date: Sat, 5 Sep 2026 14:05:54 -0500 Subject: [PATCH 112/117] The Software module gets the settings tab schema 419 promised it Finishing my own gap, not somebody else's. Schema 419 seeded the `software` module row and Agent\SoftwareSet reads three global settings from it, but ServiceConfigurationPage never got a serviceSoftware() -- so the page that configures every module 500'd on a call to a method that was never written, and the only way to change a software setting was the raw FOG Configuration page. The tab carries the three settings SoftwareSet actually sends: Re-check Interval (FOG_SOFTWARE_DRIFT_INTERVAL). Seconds between a host's re-checks of a set that has not changed. 0 is meaningful and is labeled as such: the agent treats DriftInterval <= 0 as "only check when the assigned set changes" (cmd/fog-agent/main.go). The POST clamps negatives to 0 rather than refusing them, so the stored value and the agent's reading can never disagree. Chocolatey Install Script (FOG_SOFTWARE_CHOCO_BOOTSTRAP_URL) and Chocolatey Package Source (FOG_SOFTWARE_CHOCO_NUPKG_URL). Empty bootstrap means never install Chocolatey, which is what SoftwareSet already documents; the second is for a mirrored or air-gapped install. Both are trimmed on save because SoftwareSet trims them on send, so a pasted trailing space cannot change the value out from under the admin. FOG_SOFTWARE_DRIFT_INTERVAL is registered numeric in _settingsMeta() as well -- that map is the shared source of truth for validating and rendering the same setting on the FOG Configuration page, and it was missing. TWO THINGS FOUND WHILE DOING IT. The Update button on a new tab does nothing until fog.service.list.js names it. That file holds an explicit button/form registry and there is no fallback, so the first version of this tab rendered perfectly and silently discarded every save -- I only caught it by reading the row back out of the database instead of trusting the success toast. Printer Manager and Power Management both passed 'pm' as their id prefix. Every tab renders into one document, so both emitted id="ispmEnabled", and a