Skip to content

Commit 7566132

Browse files
Vitexusclaude
andcommitted
feat: add smart credential prototype PHP classes and UI package
Introduce CredentialProtoType/MServer.php with all six Pohoda fields and Ui/CredentialType/MServer.php that tests mServer /status via HTTP Basic Auth and displays company name, status and processing count. Split Debian packaging into multiflexi-mserver (core) and multiflexi-mserver-ui (web form) packages; add composer.json with PSR-4 autoload mapping. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 4ce3189 commit 7566132

6 files changed

Lines changed: 325 additions & 0 deletions

File tree

composer.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "vitexsoftware/multiflexi-mserver",
3+
"description": "MultiFlexi support for Stormware Pohoda mServer API",
4+
"type": "library",
5+
"license": "MIT",
6+
"authors": [
7+
{
8+
"name": "Vítězslav Dvořák",
9+
"email": "info@vitexsoftware.cz"
10+
}
11+
],
12+
"require": {
13+
"vitexsoftware/multiflexi-core": ">=1.0"
14+
},
15+
"minimum-stability": "dev",
16+
"autoload": {
17+
"psr-4": {
18+
"MultiFlexi\\CredentialProtoType\\": "src/MultiFlexi/CredentialProtoType/",
19+
"MultiFlexi\\Ui\\CredentialType\\": "src/MultiFlexi/Ui/CredentialType/"
20+
}
21+
}
22+
}

debian/control

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,15 @@ Description: MultiFlexi mServer Credential Prototype
2323
This credential prototype defines the configuration fields required to connect
2424
to the Pohoda mServer API, including support for a secondary account for
2525
year-end (December/January) data entry.
26+
27+
Package: multiflexi-mserver-ui
28+
Architecture: all
29+
Multi-Arch: foreign
30+
Depends:
31+
multiflexi-mserver (= ${binary:Version}),
32+
multiflexi-web,
33+
${misc:Depends}
34+
Description: MultiFlexi mServer UI components
35+
Web UI form for the Stormware Pohoda mServer credential type.
36+
Tests mServer API connectivity via /status endpoint and displays
37+
server name, status and processing count.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src/MultiFlexi/Ui/CredentialType/MServer.php usr/lib/multiflexi/MultiFlexi/Ui/CredentialType/

debian/multiflexi-mserver.install

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ multiflexi/*.json usr/lib/multiflexi-mserver/multiflexi/
22
multiflexi/*.svg usr/share/multiflexi/images/
33
multiflexi-mserver.svg usr/share/icons/hicolor/scalable/apps/
44
debian/io.github.spoje_net.multiflexi_mserver.metainfo.xml usr/share/metainfo/
5+
src/MultiFlexi/CredentialProtoType/MServer.php usr/share/php/MultiFlexi/CredentialProtoType/
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the MultiFlexi package
7+
*
8+
* https://multiflexi.eu/
9+
*
10+
* (c) Vítězslav Dvořák <http://vitexsoftware.com>
11+
*
12+
* For the full copyright and license information, please view the LICENSE
13+
* file that was distributed with this source code.
14+
*/
15+
16+
namespace MultiFlexi\CredentialProtoType;
17+
18+
/**
19+
* Stormware Pohoda mServer API credential prototype.
20+
*
21+
* @no-named-arguments
22+
*/
23+
class MServer extends \MultiFlexi\CredentialProtoType implements \MultiFlexi\credentialTypeInterface
24+
{
25+
public static string $logo = 'mServer.svg';
26+
27+
public function __construct()
28+
{
29+
parent::__construct();
30+
31+
$icoField = new \MultiFlexi\ConfigField('POHODA_ICO', 'string', _('Organization Number'), _('Organization Number for Pohoda'));
32+
$icoField->setHint('123245678')->setValue('');
33+
34+
$urlField = new \MultiFlexi\ConfigField('POHODA_URL', 'string', _('mServer API Endpoint'), _('URL of the mServer API'));
35+
$urlField->setHint('http://pohoda:40000')->setValue('');
36+
37+
$usernameField = new \MultiFlexi\ConfigField('POHODA_USERNAME', 'string', _('mServer API Username'), _('Username for the mServer API'));
38+
$usernameField->setHint('winstrom')->setValue('');
39+
40+
$passwordField = new \MultiFlexi\ConfigField('POHODA_PASSWORD', 'password', _('mServer API Password'), _('Password for the mServer API'));
41+
$passwordField->setHint('pohoda')->setValue('');
42+
43+
$secUsernameField = new \MultiFlexi\ConfigField('POHODA_SECONDARY_USERNAME', 'string', _('Secondary Account Username'), _('Username for writing December data in January (previous year)'));
44+
$secUsernameField->setHint('winstrom2')->setValue('');
45+
46+
$secPasswordField = new \MultiFlexi\ConfigField('POHODA_SECONDARY_PASSWORD', 'password', _('Secondary Account Password'), _('Password for writing December data in January (previous year)'));
47+
$secPasswordField->setHint('pohoda2')->setValue('');
48+
49+
$this->configFieldsInternal->addField($icoField);
50+
$this->configFieldsInternal->addField($urlField);
51+
$this->configFieldsInternal->addField($usernameField);
52+
$this->configFieldsInternal->addField($passwordField);
53+
$this->configFieldsInternal->addField($secUsernameField);
54+
$this->configFieldsInternal->addField($secPasswordField);
55+
}
56+
57+
public function load(int $credTypeId)
58+
{
59+
$loaded = parent::load($credTypeId);
60+
61+
foreach ($this->configFieldsInternal->getFields() as $field) {
62+
$this->configFieldsProvided->addField($field);
63+
}
64+
65+
return $loaded;
66+
}
67+
68+
#[\Override]
69+
public function prepareConfigForm(): void
70+
{
71+
}
72+
73+
public static function name(): string
74+
{
75+
return _('Stormware Pohoda mServer');
76+
}
77+
78+
public static function description(): string
79+
{
80+
return _('Credential type for connecting to Stormware Pohoda mServer API');
81+
}
82+
83+
public static function uuid(): string
84+
{
85+
return '6ba7b814-9dad-11d1-80b4-00c04fd430c8';
86+
}
87+
88+
#[\Override]
89+
public static function logo(): string
90+
{
91+
return self::$logo;
92+
}
93+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* This file is part of the MultiFlexi package
7+
*
8+
* https://multiflexi.eu/
9+
*
10+
* (c) Vítězslav Dvořák <http://vitexsoftware.com>
11+
*
12+
* For the full copyright and license information, please view the LICENSE
13+
* file that was distributed with this source code.
14+
*/
15+
16+
namespace MultiFlexi\Ui\CredentialType;
17+
18+
/**
19+
* Stormware Pohoda mServer credential UI form with live connection test.
20+
*
21+
* @author Vitex <info@vitexsoftware.cz>
22+
*/
23+
class MServer extends \MultiFlexi\Ui\CredentialFormHelperPrototype
24+
{
25+
public function finalize(): void
26+
{
27+
$urlField = $this->credential->getFields()->getFieldByCode('POHODA_URL');
28+
$icoField = $this->credential->getFields()->getFieldByCode('POHODA_ICO');
29+
$usernameField = $this->credential->getFields()->getFieldByCode('POHODA_USERNAME');
30+
$passwordField = $this->credential->getFields()->getFieldByCode('POHODA_PASSWORD');
31+
32+
$url = $urlField ? $urlField->getValue() : '';
33+
$ico = $icoField ? $icoField->getValue() : '';
34+
$username = $usernameField ? $usernameField->getValue() : '';
35+
$password = $passwordField ? $passwordField->getValue() : '';
36+
37+
if (empty($url) || empty($username) || empty($password)) {
38+
$missing = [];
39+
40+
if (empty($url)) {
41+
$missing[] = 'POHODA_URL';
42+
}
43+
44+
if (empty($username)) {
45+
$missing[] = 'POHODA_USERNAME';
46+
}
47+
48+
if (empty($password)) {
49+
$missing[] = 'POHODA_PASSWORD';
50+
}
51+
52+
$this->addItem(new \Ease\TWB4\Alert('warning', sprintf(
53+
_('Required fields not set: %s'),
54+
implode(', ', $missing),
55+
)));
56+
parent::finalize();
57+
58+
return;
59+
}
60+
61+
$statusUrl = rtrim($url, '/').'/status';
62+
63+
$infoPanel = new \Ease\TWB4\Panel(_('mServer Connection'), 'default');
64+
$infoList = new \Ease\Html\DlTag(null, ['class' => 'row']);
65+
66+
$infoList->addItem(new \Ease\Html\DtTag(_('Endpoint'), ['class' => 'col-sm-4']));
67+
$infoList->addItem(new \Ease\Html\DdTag(
68+
new \Ease\Html\SpanTag($statusUrl, ['class' => 'font-monospace']),
69+
['class' => 'col-sm-8'],
70+
));
71+
72+
$infoList->addItem(new \Ease\Html\DtTag(_('Username'), ['class' => 'col-sm-4']));
73+
$infoList->addItem(new \Ease\Html\DdTag($username, ['class' => 'col-sm-8']));
74+
75+
if (!empty($ico)) {
76+
$infoList->addItem(new \Ease\Html\DtTag(_('Organization Number'), ['class' => 'col-sm-4']));
77+
$infoList->addItem(new \Ease\Html\DdTag($ico, ['class' => 'col-sm-8']));
78+
}
79+
80+
$infoPanel->addItem($infoList);
81+
$this->addItem($infoPanel);
82+
83+
$result = self::testConnection($statusUrl, $username, $password);
84+
85+
if ($result['success']) {
86+
$this->addItem(new \Ease\TWB4\Alert('success', sprintf(
87+
_('mServer connection to %s successful'),
88+
$url,
89+
)));
90+
91+
if (!empty($result['info'])) {
92+
$serverPanel = new \Ease\TWB4\Panel(_('Server Status'), 'info');
93+
$serverList = new \Ease\Html\DlTag(null, ['class' => 'row']);
94+
95+
foreach ($result['info'] as $key => $value) {
96+
$serverList->addItem(new \Ease\Html\DtTag($key, ['class' => 'col-sm-4']));
97+
$serverList->addItem(new \Ease\Html\DdTag($value, ['class' => 'col-sm-8']));
98+
}
99+
100+
$serverPanel->addItem($serverList);
101+
$this->addItem($serverPanel);
102+
}
103+
} else {
104+
$this->addItem(new \Ease\TWB4\Alert('danger', sprintf(
105+
_('mServer connection to %s failed: %s'),
106+
$url,
107+
$result['message'],
108+
)));
109+
}
110+
111+
parent::finalize();
112+
}
113+
114+
/**
115+
* Test mServer /status endpoint with HTTP Basic Auth.
116+
*
117+
* @return array{success: bool, message: string, info: array<string, string>}
118+
*/
119+
private static function testConnection(string $statusUrl, string $username, string $password): array
120+
{
121+
$context = stream_context_create([
122+
'http' => [
123+
'method' => 'GET',
124+
'header' => 'Authorization: Basic '.base64_encode($username.':'.$password)."\r\n",
125+
'timeout' => 10,
126+
'ignore_errors' => true,
127+
],
128+
]);
129+
130+
$response = @file_get_contents($statusUrl, false, $context);
131+
132+
if ($response === false) {
133+
return [
134+
'success' => false,
135+
'message' => error_get_last()['message'] ?? _('Connection failed'),
136+
'info' => [],
137+
];
138+
}
139+
140+
$httpStatus = 0;
141+
142+
if (isset($http_response_header) && \is_array($http_response_header)) {
143+
if (preg_match('#HTTP/\S+\s+(\d+)#', $http_response_header[0], $m)) {
144+
$httpStatus = (int) $m[1];
145+
}
146+
}
147+
148+
if ($httpStatus === 401) {
149+
return [
150+
'success' => false,
151+
'message' => _('Authentication failed — check POHODA_USERNAME and POHODA_PASSWORD'),
152+
'info' => [],
153+
];
154+
}
155+
156+
if ($httpStatus !== 200) {
157+
return [
158+
'success' => false,
159+
'message' => sprintf(_('Unexpected HTTP status: %d'), $httpStatus),
160+
'info' => [],
161+
];
162+
}
163+
164+
$info = [];
165+
166+
$xml = @simplexml_load_string($response);
167+
168+
if ($xml !== false && $xml->getName() === 'mServer') {
169+
if (isset($xml->name)) {
170+
$info[_('Company')] = (string) $xml->name;
171+
}
172+
173+
if (isset($xml->status)) {
174+
$info[_('Status')] = (string) $xml->status;
175+
}
176+
177+
if (isset($xml->processing)) {
178+
$info[_('Processing')] = (string) $xml->processing;
179+
}
180+
181+
if (isset($xml->server)) {
182+
$info[_('Server')] = (string) $xml->server;
183+
}
184+
185+
if (isset($xml->message)) {
186+
$info[_('Message')] = (string) $xml->message;
187+
}
188+
}
189+
190+
return [
191+
'success' => true,
192+
'message' => '',
193+
'info' => $info,
194+
];
195+
}
196+
}

0 commit comments

Comments
 (0)