2026-SEP-09
This commit is contained in:
@@ -147,7 +147,7 @@ table inet filter {
|
||||
oifname $IF_SERVER ip saddr $HOSTS4_CONSOLE ip daddr { $HOSTS4_BK, $HOSTS4_INFRA, $HOSTS4_APP } tcp dport $PORTS_WEB accept comment "allow ipv4 web connection: CONSOLE > FW > SERVER BK/INFRA/APP"
|
||||
oifname $IF_SERVER ip6 saddr $HOSTS6_CONSOLE ip6 daddr { $HOSTS6_BK, $HOSTS6_INFRA, $HOSTS6_APP } tcp dport $PORTS_WEB accept comment "allow ipv6 web connection: CONSOLE > FW > SERVER BK/INFRA/APP"
|
||||
# Printer
|
||||
oifname $IF_CLIENT ip saddr $HOSTS4_CONSOLE ip daddr $HOSTS4_PRINTER accept comment "allow ipv4 printer connection: CONSOLE > FW > PRINTER"
|
||||
ip saddr $HOSTS4_CONSOLE ip daddr $HOSTS4_PRINTER accept comment "allow ipv4 printer connection: CONSOLE > FW > PRINTER"
|
||||
|
||||
iifname $IF_WAN jump wan comment "set WAN interface rules"
|
||||
iifname $IF_CLIENT jump client comment "set CLIENT interface rules"
|
||||
|
||||
@@ -139,6 +139,8 @@ postgresql:
|
||||
mastodon: ENC[AES256_GCM,data:hq1ETBK9iUI7kWFO+9pZwMQMbmyLExGk7/VzBwDdLFbzptXL2KEi6oYRCS0=,iv:hF1C8lsnP2i3LxDIuOA6op4qKXQKjJ7gqznDst7hVAU=,tag:maR7Jvsv3DJGrO77EzmBDA==,type:str]
|
||||
tandoor: ENC[AES256_GCM,data:+5g/jzRMQoxocZntoE+1BU0MS00yEp0tyVNxriEIJ0pW2V1bsLhUAkwQ748=,iv:feNbpOhkdjmKS7Ovy0KGioOTyjQ4sflfH+50niP7Y8Y=,tag:51/u+EE3Pp5o7jnlj0AWIw==,type:str]
|
||||
linkding: ENC[AES256_GCM,data:+diOqVjK5RkDOpxCrb0M0sLWEufXEwhTNo0x5qH4FkteN8zaUAZfO8lD13I=,iv:jHQANXC1tamiphm33RpTKpEB3niLFx6IKzcdq8/KNCg=,tag:NwkkmUPOS9K0IKtUBVRWqg==,type:str]
|
||||
openwebui: ENC[AES256_GCM,data:QNeBJjdLIzoI2h9e3me6A/xZ03YpBeLpKK0Yya5Wi5vryv1ZwnHHZwOjmuA=,iv:qLpsxlwpE+V+qACkfOee/ExFIyVV+KsXzuT3VDiJ/w4=,tag:YsmBhmEkqYcSmIIX7R2nTw==,type:str]
|
||||
kaneo: ENC[AES256_GCM,data:Q2rg4/0wAgWDzB8Aj5B6i851FZN3eGR7BbX2l3Y5+Ij47//TtKGzdq6cxNg=,iv:Mbp/S7Lc0tLJAT+QFMVB3a86S+mqoQmZDZQ7Agf8rU8=,tag:BkerNmfeeOicsQAwUo4dtQ==,type:str]
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
@@ -148,6 +150,7 @@ mariadb:
|
||||
crt: ENC[AES256_GCM,data:aCrAXf5n6vIyEIbpwyrmVvjPa6gmVrAU213SnqHO3/Mem4MgfDjmh9Q1tQbvXKSJT4tcecp08sCY1Y+AKN26lvqFd1Y8b50dNVLXuBRd0saLDwyvi2/w7a9B5sinKLCtKfvOc2JOdoYVHQKcyQXG6uHxSpZPf2+j8s7KUrgTNAjKM6aOQaUYydbxcD0pQ9BgJxCCM88+que+HtX0FdLYr/ImF+dvpMRE5RlvWlWB4bae6A/xPRWH8ZHj4ZwO4uMSQARu5w3kWDngcE/CnEhC1JQvKLJ1gKzkaUF3/Vn8kkxIITGddw0+2kQyGCJ9a5ZXpzr+AJX1OuFE83YDZRRXbhmEuSZiNQmLaw7ImSjNI5uZ4MNyKUuq9t+xqSgBj64dhn4egHaAb9fzZo9lV9/2kAchnidWBzn9EtAacYsO0lf7i7OYuSwCjKaTBvO8DwPudSsWi3CYYx2NlQXP5PvElJi/7aasWcF3ohmhMUew0ATBdgxMKvl3MENzi66WuTb2uzcryMQf5M6lIceXYa3RLQQzJXkBUe4vBg5aQBiKAYqURvxmvgb+j/YMLHrXGj8bkkV/iTdiI5oyeE9+si6Tw+f6Gj/E++KUWSR8AIrNbAHgJ1dtPd/oHt8CW8X9USJd56L3tOAA0r5SDnHhnZ/sfumuyz3H34RYjPxfKu0FCQhsEofDrDc4DnypD6s1RgzJM+esvHRLb2k4+X1CN5Lf04pTis+GshRtWxRClycrwLjcyaCZ/JN5+4sAHazr2nEQ8C+RQ9NuVRK2enFVcfTCf+2Sk6pN8ltHcmrYH1Xx1B88gsAPL8eWmeGLiTbzCqRAkkZ2EvbOAu4qZteLC0qRan1jYQs3C8zjwfQKf8QpEY1EYqNYaNkqDPpZcxeRMoNX5vlS9n5W3qoX+qyBxUKKNl6O3WH6JNnODkJMwnFhaSE8HSTr7rbBjqc1XaM0,iv:lCxYy8LkovdX1dIIoS0NXe5YbUT/ctPK1eS1pSU1z9c=,tag:Pads/Okb8YVZH19ODUThHw==,type:str]
|
||||
password:
|
||||
bookstack: ENC[AES256_GCM,data:kFQ47qbE+D89Pjr4B6WMAcCHQ8deCyzaPmNspI5TvqwvN5RIaKOWU1uT4jU=,iv:3OiYzBod2xxKMt/5Q9uazalld1NkUNun+DJipC3Ts+w=,tag:orCNWYMAZ/5yi7c6apXVJg==,type:str]
|
||||
groupoffice: ENC[AES256_GCM,data:s4jO9jrn15tPcStDYUTNLgvCJvtlfp641nU3EzYmPvIzk4sQIQZRuGsODaU=,iv:OBpzPkW0bMWL9da2/vL7GgOTqaKSvYamapaK5AMZeZE=,tag:CYuoBHO5Wt0VBL32YKrd5A==,type:str]
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
@@ -352,6 +355,33 @@ linkding:
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
#ENC[AES256_GCM,data:aFaqLiw+m7M5KEBCH06w2Cgasw==,iv:Yq1tY7lNxW6UuY+77OQEmWwzW9vnJpaZL/bI650wg18=,tag:e9Fozat8Pf3h/BXUNZ0fHQ==,type:comment]
|
||||
openwebui:
|
||||
secret_key: ENC[AES256_GCM,data:dAUmbxfOGo684M7Mk7VAv+mZTLT2qi83Nvf6fOR6M/Ale/2n1D4Q8jIl0C4=,iv:uNNQmXTIUACVyguvPBhLzv9ir3WXlPFacgqMhrNScKM=,tag:h3plKpikh7UqHIUO9fyZiQ==,type:str]
|
||||
oidc:
|
||||
secret: ENC[AES256_GCM,data:1PZgSzT28FMuClh7W21j9zjHn/YAPiY7iBF8Hid2FCDMzC2jHaObQdXM5H4=,iv:mhNxP0So9JjG1Hgtvt8OpdzIcfp7SZZGPiX0wj0ZP20=,tag:xvKiHjeVJDz0uSkZXpuKcA==,type:str]
|
||||
hash: ENC[AES256_GCM,data:XIHS9JJVGy3qKFiBIPETou+79vMV4wjYXW9Grv4Hx1zqjqZ4onJ/JiMVcYmtBm5CAp2hsrouIdw9gjW344pnjVg5DORMz6P+/95Yu3OlYmOEj+Cl1iLqgN/q5qc3XAXzQaOylwdbp6UMR+z7PmqJzY+z1h/qMqlSIYGtRAVo4BgNBng=,iv:sK8HTn0z/kzYB8Db+zrKmlk2DxcH3DcoQkPRJtlIvSY=,tag:5hmbItxT4bRlGaM5z2kQkg==,type:str]
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
#ENC[AES256_GCM,data:AzckMweK6MUIX9VYy96juNpiGfU=,iv:4/j/5QgJy9OsxZ1dNN16TKR93/9mCmOuaqLrab3ymLo=,tag:4wrv96l2BI09Afc7HaQiRA==,type:comment]
|
||||
groupoffice:
|
||||
admin:
|
||||
id: ENC[AES256_GCM,data:WafJHpvJ3MmpfA8=,iv:i9KbnMoMG8EnaryPAQ+3xYvUwFYKQDnzGmMkSOKllTs=,tag:uH/ksrBdK1M++rVyMvxcRQ==,type:str]
|
||||
email: ENC[AES256_GCM,data:AfAQUHankHwMGutLM/LZWJEimCiDFzyDp+GsPg==,iv:atCWo/TTshDGGwtu42ZddILK45eMIg5oYy82m4TAzUU=,tag:0X+bhJ05/1b7DS9wb22A9w==,type:str]
|
||||
password: ENC[AES256_GCM,data:V0CJ8KAoHguWrW9+/j12xjbqqn/5nKAqbYYHZ1UxqAubpD0fQDLTjrZRTuQ=,iv:MJinVDkLBaSNAwM8qCj0WaWl26vTYDoHQqcmdlyNjR4=,tag:7P1UQZd197aJlFREiJP2ow==,type:str]
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
#ENC[AES256_GCM,data:zEb2WvNmwVIqKcKesJk=,iv:G/CP+ZrjF9uJNTRC0CVY1cRM1gPL8Ud2GHAmHkkSM78=,tag:lXIH+YF2dCdIzI/ImhQRig==,type:comment]
|
||||
kaneo:
|
||||
secret_key: ENC[AES256_GCM,data:SeMvcW+SZABzQKnTsd1dOFbKklfqqTEDM6q9pqd1SS9hig/XuW4lDzS8q0k=,iv:3Djzn0VGasdxyoZo8zMx4G8sIOGBhRt/1cq3LuabUzA=,tag:FCbSB7A/PowGPz8URFm/eA==,type:str]
|
||||
oidc:
|
||||
secret: ENC[AES256_GCM,data:AFjQeEv3DMFZCpObgOR69i/Tavp3lcYUsaUqZ/X4pQYcXCNAE1gRHwkk//8=,iv:nEsQOT5AZnHHHMfrfcCIClw3Dwg+O2ykPFjIlcl8Wrc=,tag:J+E50bmw6pEXSVkAOwfuWQ==,type:str]
|
||||
hash: ENC[AES256_GCM,data:47WzIRYnXn/Jz01BwSVjtff2cTl16vVWh1YypJLmg/GJjXNj+XcghAnw4j7hFf8YhIyyCgWHHm0wkgqxpjPXN03wDIP8Djxg3K5ubUPXZJZWYVTdwcc1Ebdi/3x/DijMLqSJxbRATYSXijBF1o6t1uv0GfGHXTt7M50vdsB8MNjBkds=,iv:MRj+G3gpr8tQdwAxxZlojknUeQ67WY17OAbu04v2sxY=,tag:avz6caNLAi/bGvkfheGJCw==,type:str]
|
||||
#ENC[AES256_GCM,data:ODXFUxxxdQ==,iv:s9zJVx6wo6x517tbNvC+FZ0dFzqbjqeLI6rXBq72hQA=,tag:bXoV2I3LbpmQyddJrtS3Qg==,type:comment]
|
||||
#
|
||||
#
|
||||
#ENC[AES256_GCM,data:T4Wtn49AAxPd2QUFTR+q,iv:bH5goGWBDqumAat9dUv2OwfCUJUpuVqncTMqMBZUXhI=,tag:G+W6hHA+yftQ+4RJpXrxHg==,type:comment]
|
||||
switch:
|
||||
admin:
|
||||
@@ -368,7 +398,7 @@ sops:
|
||||
k+uyWPPi+enFkVaceSwMFrA66uaWWrwAj11sXEB7yzvGFPrnAGezjQ==
|
||||
-----END AGE ENCRYPTED FILE-----
|
||||
recipient: age120wuwcmsm845ztsvsz46pswj5je53uc2n35vadklrfqudu6cxuusxetk7y
|
||||
lastmodified: "2026-08-27T14:09:38Z"
|
||||
mac: ENC[AES256_GCM,data:K+SwIrdo62opVGnsrMH7FysC8GtMN1RGPGmtUxRKLoNy/Yi/tKxjVH1xiu1ePCWXWqpGjuC7btJdel3SZmwDi4jMhN87DadOh1Kf0sIGDe8+Ff1LrLLl0I4+NWpu/RJicXwxOXkZUwdEZ0sqTW2Qm1S7aknPpSoD9vc44epv1AA=,iv:npBPZiCeb3+7s+xHd/XhOhEnxvAJzeZWGFzW98uFSK0=,tag:KhQcf6/SVDtbgpiZO/7VNA==,type:str]
|
||||
lastmodified: "2026-09-08T03:23:38Z"
|
||||
mac: ENC[AES256_GCM,data:+mTAe1NwSbQHsmaVxKrtIDeRyyKbDVHSRCkji5+hAByS/GS4OzK+SwdyGYa5Ihv+g2J/7Pechf44GY8mv0lirWcOTGxqzvMDiRSXNty+aQVzOn9sXbfCvWXBUqDLNOiNISN0RfF1eVlG12D8wNfFEXFbs6lv81IMIN9ctdvTN5c=,iv:CG6Px0p/EomUafLody2kJOR8AzdmPTqe1ry1DZUhNSc=,tag:ClSWU401Jt6PzhcSCNNvbg==,type:str]
|
||||
unencrypted_suffix: _unencrypted
|
||||
version: 3.13.2
|
||||
|
||||
@@ -13,6 +13,7 @@ PublishPort={{ services['collabora']['ports']['http'] }}:9980/tcp
|
||||
|
||||
Environment="TZ={{ timezone }}"
|
||||
Environment="aliasgroup1=https://{{ services['nextcloud']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
Environment="aliasgroup2=https://{{ services['groupoffice']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
# Environment="aliasgroup2=other_server_FQDN"
|
||||
Environment="extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:server_name={{ services['collabora']['domain']['public'] }}.{{ domain['public'] }} --o:admin_console.enable=false"
|
||||
|
||||
|
||||
@@ -0,0 +1,519 @@
|
||||
<?php
|
||||
|
||||
namespace go\modules\community\imapauthenticator\model;
|
||||
|
||||
use Exception;
|
||||
use go\core\auth\PrimaryAuthenticator;
|
||||
use go\core\imap\Connection;
|
||||
use go\core\model\User;
|
||||
use GO\Email\Model\Account;
|
||||
use function GO;
|
||||
|
||||
class Authenticator extends PrimaryAuthenticator
|
||||
{
|
||||
public static function id(): string
|
||||
{
|
||||
return "imap";
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a login name into a Group-Office username and e-mail address.
|
||||
*
|
||||
* Full e-mail:
|
||||
* foo@example.com
|
||||
* -> username = foo
|
||||
* -> email = foo@example.com
|
||||
*
|
||||
* Short username:
|
||||
* foo
|
||||
* -> allowed only when exactly one concrete IMAP domain is configured
|
||||
* -> username = foo
|
||||
* -> email = foo@configured-domain
|
||||
*
|
||||
* Wildcard (*) domains are not used for short-name inference.
|
||||
*
|
||||
* @param string $login
|
||||
* @return array{username:string,email:string}|null
|
||||
*/
|
||||
private static function resolveIdentity(string $login): ?array
|
||||
{
|
||||
$login = trim($login);
|
||||
|
||||
if($login === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$atPos = strpos($login, '@');
|
||||
|
||||
if($atPos !== false) {
|
||||
if($atPos === 0 || $atPos === strlen($login) - 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => substr($login, 0, $atPos),
|
||||
'email' => $login
|
||||
];
|
||||
}
|
||||
|
||||
/*
|
||||
* For a short username, infer the mail domain from the
|
||||
* configured IMAP authenticator domains.
|
||||
*
|
||||
* This is only unambiguous when exactly one concrete
|
||||
* domain is configured.
|
||||
*/
|
||||
$stmt = go()->getDbConnection()->query(
|
||||
"SELECT DISTINCT `name`
|
||||
FROM `imapauth_server_domain`
|
||||
WHERE `name` <> '*'
|
||||
AND `name` <> ''
|
||||
ORDER BY `name`"
|
||||
);
|
||||
|
||||
$domains = $stmt->fetchAll(\PDO::FETCH_COLUMN);
|
||||
|
||||
if(count($domains) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'username' => $login,
|
||||
'email' => $login . '@' . $domains[0]
|
||||
];
|
||||
}
|
||||
|
||||
public static function isAvailableFor(string $username): bool
|
||||
{
|
||||
$identity = static::resolveIdentity($username);
|
||||
|
||||
if(!$identity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Existing Group-Office users with a local password must be
|
||||
* authenticated locally.
|
||||
*
|
||||
* This keeps local/break-glass administrator accounts such as
|
||||
* "local_admin" out of the IMAP authentication flow.
|
||||
*
|
||||
* IMAP-managed users have their local password cleared after
|
||||
* successful IMAP authentication, so they continue to use IMAP.
|
||||
*/
|
||||
$localUser = User::find()
|
||||
->where([
|
||||
'username' => $identity['username']
|
||||
])
|
||||
->single();
|
||||
|
||||
if($localUser && $localUser->hasPassword()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return static::findServer($identity['email']) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the IMAP authentication server for an e-mail address.
|
||||
*
|
||||
* @param string $email
|
||||
* @return Server|boolean
|
||||
*/
|
||||
private static function findServer($email)
|
||||
{
|
||||
$atPos = strpos($email, '@');
|
||||
|
||||
if(!$atPos) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$domain = substr($email, $atPos + 1);
|
||||
|
||||
return Server::find()
|
||||
->join(
|
||||
'imapauth_server_domain',
|
||||
'd',
|
||||
's.id = d.serverId'
|
||||
)
|
||||
->where([
|
||||
'd.name' => $domain
|
||||
])
|
||||
->orWhere([
|
||||
'd.name' => '*'
|
||||
])
|
||||
->single();
|
||||
}
|
||||
|
||||
public function authenticate(
|
||||
string $username,
|
||||
string $password
|
||||
): bool|User
|
||||
{
|
||||
$this->fireEvent(
|
||||
'beforeauthenticate',
|
||||
[$username]
|
||||
);
|
||||
|
||||
$identity = static::resolveIdentity($username);
|
||||
|
||||
if(!$identity) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$localUsername = $identity['username'];
|
||||
$email = $identity['email'];
|
||||
|
||||
$server = static::findServer($email);
|
||||
|
||||
if(!$server) {
|
||||
return false;
|
||||
}
|
||||
|
||||
go()->debug(
|
||||
"Attempting IMAP authentication on " .
|
||||
$server->imapHostname
|
||||
);
|
||||
|
||||
$connection = new Connection();
|
||||
|
||||
if(!$connection->connect(
|
||||
$server->imapHostname,
|
||||
$server->imapPort,
|
||||
$server->imapEncryption == 'ssl',
|
||||
10,
|
||||
!$server->imapValidateCertificate
|
||||
)) {
|
||||
throw new Exception(
|
||||
"Could not connect to IMAP server"
|
||||
);
|
||||
}
|
||||
|
||||
if($server->imapEncryption == 'tls') {
|
||||
if(!$connection->startTLS()) {
|
||||
throw new Exception(
|
||||
"Could not enable TLS on " .
|
||||
$server->imapHostname .
|
||||
" at port " .
|
||||
$server->imapPort
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Username sent to the IMAP server.
|
||||
*
|
||||
* Remove domain from username = No
|
||||
* -> foo@example.com
|
||||
*
|
||||
* Remove domain from username = Yes
|
||||
* -> foo
|
||||
*/
|
||||
$imapUsername = $server->removeDomainFromUsername
|
||||
? $localUsername
|
||||
: $email;
|
||||
|
||||
if(!$connection->authenticate(
|
||||
$imapUsername,
|
||||
$password
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/*
|
||||
* Group-Office username is the local-part.
|
||||
*/
|
||||
$user = User::find()
|
||||
->where([
|
||||
'username' => $localUsername
|
||||
])
|
||||
->single();
|
||||
|
||||
/*
|
||||
* Prevent the same short username from being associated
|
||||
* with two different e-mail addresses.
|
||||
*/
|
||||
if(
|
||||
$user &&
|
||||
strcasecmp(
|
||||
(string) $user->email,
|
||||
$email
|
||||
) !== 0
|
||||
) {
|
||||
throw new Exception(
|
||||
"Username '" .
|
||||
$localUsername .
|
||||
"' already belongs to another e-mail address"
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Migrate an account previously created by the stock
|
||||
* IMAP authenticator:
|
||||
*
|
||||
* username = foo@example.com
|
||||
* email = foo@example.com
|
||||
*
|
||||
* to:
|
||||
*
|
||||
* username = foo
|
||||
* email = foo@example.com
|
||||
*
|
||||
* The existing User ID is preserved.
|
||||
*/
|
||||
if(!$user) {
|
||||
$user = User::find()
|
||||
->where([
|
||||
'username' => $email
|
||||
])
|
||||
->single();
|
||||
|
||||
if(!$user) {
|
||||
$user = User::find()
|
||||
->where([
|
||||
'email' => $email
|
||||
])
|
||||
->single();
|
||||
}
|
||||
|
||||
if($user) {
|
||||
$user->username = $localUsername;
|
||||
}
|
||||
}
|
||||
|
||||
if(!$user) {
|
||||
$user = $this->createUser(
|
||||
$localUsername,
|
||||
$email
|
||||
);
|
||||
} elseif($user->hasPassword()) {
|
||||
$user->clearPassword();
|
||||
}
|
||||
|
||||
foreach($server->groups as $group) {
|
||||
$user->addGroup($group->groupId);
|
||||
}
|
||||
|
||||
if($user->isModified()) {
|
||||
if(!$user->save()) {
|
||||
throw new Exception(
|
||||
"Could not save user"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Synchronize the user's actual Email Account with the
|
||||
* global IMAP Authenticator server profile on every
|
||||
* successful login.
|
||||
*/
|
||||
$this->setEmailAccount(
|
||||
$imapUsername,
|
||||
$password,
|
||||
$email,
|
||||
$server,
|
||||
$user
|
||||
);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new Group-Office user.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function createUser(
|
||||
string $username,
|
||||
string $email
|
||||
): User
|
||||
{
|
||||
$user = new User();
|
||||
|
||||
$user->displayName = $username;
|
||||
$user->username = $username;
|
||||
$user->email = $email;
|
||||
$user->recoveryEmail = $email;
|
||||
|
||||
if(!$user->save()) {
|
||||
throw new Exception(
|
||||
"Could not save user after successful IMAP login"
|
||||
);
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize the user's Email Account with the global
|
||||
* IMAP Authenticator server profile on every successful login.
|
||||
*
|
||||
* The following settings are authoritative:
|
||||
*
|
||||
* - IMAP host
|
||||
* - IMAP port
|
||||
* - IMAP encryption
|
||||
* - IMAP certificate validation
|
||||
* - IMAP username
|
||||
* - IMAP password
|
||||
* - SMTP host
|
||||
* - SMTP port
|
||||
* - SMTP encryption
|
||||
* - SMTP certificate validation
|
||||
* - SMTP authentication credentials
|
||||
*
|
||||
* User-specific mailbox, alias and other unrelated settings
|
||||
* are preserved.
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function setEmailAccount(
|
||||
$username,
|
||||
$password,
|
||||
$email,
|
||||
Server $server,
|
||||
User $user
|
||||
) {
|
||||
if(!$user->hasModule('legacy', 'email')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* Find all existing e-mail accounts belonging to this user.
|
||||
*
|
||||
* Do not use the configured IMAP hostname as an identifier.
|
||||
* The administrator may change the global hostname later,
|
||||
* and in that case we want to update the existing account
|
||||
* instead of creating a duplicate.
|
||||
*/
|
||||
$accounts = Account::model()
|
||||
->findByAttributes([
|
||||
'user_id' => $user->id
|
||||
])
|
||||
->fetchAll();
|
||||
|
||||
$account = null;
|
||||
|
||||
/*
|
||||
* Prefer an account whose IMAP username matches either
|
||||
* the current IMAP login username or the full e-mail
|
||||
* address.
|
||||
*/
|
||||
foreach($accounts as $candidate) {
|
||||
if(
|
||||
$candidate->username === $username ||
|
||||
$candidate->username === $email
|
||||
) {
|
||||
$account = $candidate;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* If this user has exactly one e-mail account, treat that
|
||||
* account as the one managed by the IMAP authenticator.
|
||||
*
|
||||
* This allows changes to username/hostname configuration
|
||||
* without accidentally creating a duplicate account.
|
||||
*/
|
||||
if(!$account && count($accounts) === 1) {
|
||||
$account = $accounts[0];
|
||||
}
|
||||
|
||||
$wasNew = false;
|
||||
|
||||
if(!$account) {
|
||||
$account = new Account();
|
||||
$account->user_id = $user->id;
|
||||
$account->mbroot = "";
|
||||
|
||||
$wasNew = true;
|
||||
}
|
||||
|
||||
/*
|
||||
* IMAP Authenticator server profile is authoritative.
|
||||
*
|
||||
* Refresh IMAP configuration on every successful login.
|
||||
*/
|
||||
$account->host =
|
||||
$server->imapHostname;
|
||||
|
||||
$account->port =
|
||||
$server->imapPort;
|
||||
|
||||
$account->username =
|
||||
$username;
|
||||
|
||||
$account->password =
|
||||
$password;
|
||||
|
||||
$account->imap_encryption =
|
||||
$server->imapEncryption ?? "";
|
||||
|
||||
$account->imap_allow_self_signed =
|
||||
!$server->imapValidateCertificate;
|
||||
|
||||
/*
|
||||
* Refresh SMTP configuration from the same global
|
||||
* authentication server profile.
|
||||
*/
|
||||
$account->smtp_host =
|
||||
$server->smtpHostname;
|
||||
|
||||
$account->smtp_port =
|
||||
$server->smtpPort;
|
||||
|
||||
$account->smtp_encryption =
|
||||
$server->smtpEncryption ?? "";
|
||||
|
||||
$account->smtp_allow_self_signed =
|
||||
!$server->smtpValidateCertificate;
|
||||
|
||||
/*
|
||||
* SMTP authentication.
|
||||
*/
|
||||
if($server->smtpUseUserCredentials) {
|
||||
/*
|
||||
* Use the same credentials that successfully
|
||||
* authenticated against IMAP.
|
||||
*/
|
||||
$account->smtp_username =
|
||||
$username;
|
||||
|
||||
$account->smtp_password =
|
||||
$password;
|
||||
} else {
|
||||
/*
|
||||
* Use static SMTP credentials configured in the
|
||||
* global IMAP Authenticator server profile.
|
||||
*/
|
||||
$account->smtp_username =
|
||||
$server->smtpUsername;
|
||||
|
||||
$account->smtp_password =
|
||||
$server->getSmtpPassword();
|
||||
}
|
||||
|
||||
/*
|
||||
* Verify the resulting IMAP configuration while saving.
|
||||
*/
|
||||
$account->checkImapConnectionOnSave = true;
|
||||
|
||||
if(!$account->save(true)) {
|
||||
throw new Exception(
|
||||
"Could not save e-mail account: " .
|
||||
implode(
|
||||
"\n",
|
||||
$account->getValidationErrors()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if($wasNew) {
|
||||
$account->addAlias(
|
||||
$email,
|
||||
$user->displayName
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,736 @@
|
||||
<?php
|
||||
|
||||
namespace go\core\db;
|
||||
|
||||
use Exception;
|
||||
use go\core\App;
|
||||
use go\core\orm\Property;
|
||||
use LogicException;
|
||||
use PDO;
|
||||
use PDOException;
|
||||
use PDOStatement;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* The database connection object. It uses PDO to connect to the database.
|
||||
*
|
||||
* The app instance of this connection is available by calling:
|
||||
*
|
||||
* ````````````````````````````````````````````````````````````````````````````
|
||||
* \go\core\App::get()->getDbConnection();
|
||||
* ````````````````````````````````````````````````````````````````````````````
|
||||
*
|
||||
* @copyright (c) 2014, Intermesh BV http://www.intermesh.nl
|
||||
* @author Merijn Schering <mschering@intermesh.nl>
|
||||
* @license http://www.gnu.org/licenses/agpl-3.0.html AGPLv3
|
||||
*/
|
||||
class Connection {
|
||||
|
||||
private $dsn;
|
||||
private $username;
|
||||
private $password;
|
||||
private $options;
|
||||
|
||||
/**
|
||||
*
|
||||
* @var PDO
|
||||
*/
|
||||
private $pdo;
|
||||
|
||||
/**
|
||||
* Output all SQL to the debugger
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $debug = false;
|
||||
|
||||
public function __construct($dsn, $username, $password) {
|
||||
|
||||
$initCmd = "SET character_set_client = utf8mb4,
|
||||
character_set_connection = utf8mb4,
|
||||
character_set_results = utf8mb4,
|
||||
sql_mode = 'STRICT_ALL_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION',
|
||||
time_zone = '+00:00',
|
||||
lc_messages = 'en_US'";
|
||||
|
||||
$this->dsn = $dsn;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
$this->options = [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_PERSISTENT => false, // Unit test on closing DB connection fails. We need this to work for long running processes
|
||||
PDO::ATTR_EMULATE_PREPARES => false, //for native data types int, bool etc.
|
||||
PDO::ATTR_STRINGIFY_FETCHES => false,
|
||||
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
|
||||
];
|
||||
|
||||
if(version_compare(PHP_VERSION, '8.5.0') >= 0) {
|
||||
$this->options[PDO\Mysql::ATTR_USE_BUFFERED_QUERY] = true;
|
||||
$this->options[PDO\Mysql::ATTR_INIT_COMMAND] = $initCmd;
|
||||
} else {
|
||||
$this->options[PDO::MYSQL_ATTR_USE_BUFFERED_QUERY] = true;
|
||||
$this->options[PDO::MYSQL_ATTR_INIT_COMMAND] = $initCmd;
|
||||
}
|
||||
|
||||
/*
|
||||
* Database TLS
|
||||
*
|
||||
* SSL attributes must be passed when PDO is constructed.
|
||||
*/
|
||||
if (defined('GO_DB_SSL_CA') && GO_DB_SSL_CA !== '') {
|
||||
|
||||
$verifyServerCert =
|
||||
defined('GO_DB_SSL_VERIFY_SERVER_CERT')
|
||||
? GO_DB_SSL_VERIFY_SERVER_CERT
|
||||
: true;
|
||||
|
||||
if(version_compare(PHP_VERSION, '8.5.0') >= 0) {
|
||||
$this->options[PDO\Mysql::ATTR_SSL_CA] =
|
||||
GO_DB_SSL_CA;
|
||||
|
||||
$this->options[PDO\Mysql::ATTR_SSL_VERIFY_SERVER_CERT] =
|
||||
$verifyServerCert;
|
||||
} else {
|
||||
$this->options[PDO::MYSQL_ATTR_SSL_CA] =
|
||||
GO_DB_SSL_CA;
|
||||
|
||||
$this->options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] =
|
||||
$verifyServerCert;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getDsn() {
|
||||
return $this->dsn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global database connection object.
|
||||
*
|
||||
* {@link http://php.net/manual/en/pdo.construct.php}
|
||||
*
|
||||
* @return PDO Database connection object
|
||||
*/
|
||||
public function getPDO(): PDO
|
||||
{
|
||||
if (!isset($this->pdo)) {
|
||||
$this->setPDO();
|
||||
}
|
||||
return $this->pdo;
|
||||
}
|
||||
|
||||
private $database;
|
||||
|
||||
/**
|
||||
* Get the database instance
|
||||
*
|
||||
* @return Database
|
||||
*/
|
||||
public function getDatabase(): Database
|
||||
{
|
||||
if(!isset($this->database)) {
|
||||
$this->database = new Database($this);
|
||||
}
|
||||
|
||||
return $this->database;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the database connection. Beware that all active PDO statements must be set to null too
|
||||
* in the current scope.
|
||||
*
|
||||
* Weird things happen when using fsockopen. This test case leaves the conneciton open. When removing the fputs call it seems to work.
|
||||
*
|
||||
*
|
||||
|
||||
$settings = \GO\Sync\Model\Settings::model()->findForUser(\go\core\App::get()->user());
|
||||
$account = \GO\Email\Model\Account::model()->findByPk($settings->account_id);
|
||||
|
||||
|
||||
$handle = stream_socket_client("tcp://localhost:143");
|
||||
$login = 'A1 LOGIN "admin@intermesh.dev" "admin"'."\r\n";
|
||||
fputs($handle, $login);
|
||||
fclose($handle);
|
||||
$handle=null;
|
||||
|
||||
echo "Test\n";
|
||||
|
||||
\go\core\App::get()->unsetDbConnection();
|
||||
sleep(10);
|
||||
*/
|
||||
public function disconnect(): void
|
||||
{
|
||||
$this->pdo = null;
|
||||
|
||||
Property::clearCachedRelationStmts();
|
||||
Property::$lastDeleteStmt = null;
|
||||
self::$cachedStatements = [];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Checks if connection still exists
|
||||
*
|
||||
* @param int $id
|
||||
* @return bool
|
||||
* @throws DbException
|
||||
*/
|
||||
public static function exists(int $id) : bool {
|
||||
|
||||
$dsn = go()->getDbConnection()->getDsn();
|
||||
$config = go()->getConfig();
|
||||
|
||||
$watchConn = new Connection(
|
||||
$dsn, $config['db_user'], $config['db_pass']
|
||||
);
|
||||
$processes = $watchConn->query("SHOW PROCESSLIST")->fetchAll(\PDO::FETCH_ASSOC);
|
||||
|
||||
foreach($processes as $process) {
|
||||
if($process['Id'] == $id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static array $cachedStatements = [];
|
||||
|
||||
/**
|
||||
* Register a cached statement that will be unset when we want to disconnect from the database.
|
||||
* If you don't unset these statement the connection will be kept open.
|
||||
* @param string $name
|
||||
* @param Statement $statement
|
||||
* @return void
|
||||
*/
|
||||
public function cacheStatement(string $name, Statement $statement): void
|
||||
{
|
||||
self::$cachedStatements[$name] = $statement;
|
||||
}
|
||||
|
||||
public function getCachedStatment(string $name) : ?Statement {
|
||||
return self::$cachedStatements[$name] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set's a new PDO object base on the current connection settings
|
||||
*/
|
||||
private function setPDO() {
|
||||
$this->pdo = null;
|
||||
$this->pdo = new PDO($this->dsn, $this->username, $this->password, $this->options);
|
||||
// go()->debug("PDO Driver: " . $this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION));
|
||||
// if (strpos($this->pdo->getAttribute(PDO::ATTR_CLIENT_VERSION), 'mysqlnd') !== false) {
|
||||
// echo 'PDO MySQLnd enabled!';
|
||||
// }
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get MySQL connection ID
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public function getId() : int {
|
||||
return (int) $this->query("select connection_id()")->fetch(PDO::FETCH_COLUMN, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL string
|
||||
*
|
||||
* Should be properly escaped!
|
||||
* {@link http://php.net/manual/en/pdo.query.php}
|
||||
*
|
||||
* @param string $sql
|
||||
* @return PDOStatement
|
||||
* @throws DbException
|
||||
*/
|
||||
public function query(string $sql): PDOStatement
|
||||
{
|
||||
if($this->debug) {
|
||||
go()->getDebugger()->debug($sql, 1 ,false);
|
||||
}
|
||||
try {
|
||||
return $this->getPdo()->query($sql);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
throw new DbException($e, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute an SQL statement and return the number of affected rows
|
||||
* <p><b>PDO::exec()</b> executes an SQL statement in a single function call, returning the number of rows affected by the statement.</p><p><b>PDO::exec()</b> does not return results from a SELECT statement. For a SELECT statement that you only need to issue once during your program, consider issuing <code>PDO::query()</code>. For a statement that you need to issue multiple times, prepare a PDOStatement object with <code>PDO::prepare()</code> and issue the statement with <code>PDOStatement::execute()</code>.</p>
|
||||
* @param string $sql <p>The SQL statement to prepare and execute.</p> <p>Data inside the query should be properly escaped.</p>
|
||||
* @return int <p><b>PDO::exec()</b> returns the number of rows that were modified or deleted by the SQL statement you issued. If no rows were affected, <b>PDO::exec()</b> returns <i>0</i>.</p><p><b>Warning</b></p><p>This function may return Boolean <b><code>FALSE</code></b>, but may also return a non-Boolean value which evaluates to <b><code>FALSE</code></b>. Please read the section on Booleans for more information. Use the === operator for testing the return value of this function.</p><p>The following example incorrectly relies on the return value of <b>PDO::exec()</b>, wherein a statement that affected 0 rows results in a call to <code>die()</code>:</p> <code> <?php<br>$db->exec() or die(print_r($db->errorInfo(), true)); // incorrect<br>?> </code>
|
||||
* @link http://php.net/manual/en/pdo.exec.php
|
||||
*
|
||||
* @throws DbException
|
||||
*/
|
||||
public function exec(string $sql): int
|
||||
{
|
||||
if($this->debug) {
|
||||
go()->getDebugger()->debug($sql, 1, true);
|
||||
}
|
||||
try {
|
||||
return $this->getPdo()->exec($sql);
|
||||
}
|
||||
catch(PDOException $e) {
|
||||
throw new DbException($e, $sql);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* UNLOCK TABLES explicitly releases any table locks held by the current session
|
||||
*/
|
||||
public function unlockTables(): bool
|
||||
{
|
||||
return $this->getPdo()->exec("UNLOCK TABLES") !== false;
|
||||
}
|
||||
|
||||
private $transactionSavePointLevel = 0;
|
||||
|
||||
/**
|
||||
* Start a database transation
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function beginTransaction(): bool
|
||||
{
|
||||
if($this->transactionSavePointLevel == 0) {
|
||||
$ret = $this->getPdo()->beginTransaction();
|
||||
|
||||
}else
|
||||
{
|
||||
$ret = true;
|
||||
}
|
||||
|
||||
$this->transactionSavePointLevel++;
|
||||
|
||||
if($this->debug) {
|
||||
go()->debug("beginTransaction " . $this->transactionSavePointLevel, 1);
|
||||
}
|
||||
|
||||
return $ret;
|
||||
}
|
||||
|
||||
private $resumeLevels = 0;
|
||||
|
||||
public function isPaused(): bool
|
||||
{
|
||||
return $this->resumeLevels > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Commits the transaction but remembers the nesting of transaction nesting level. Must be resumed with resumeTransactions().
|
||||
* This is used by custom fields that do database structure changes. MySQL commits transactions automatically when the database
|
||||
* structure changes.
|
||||
*
|
||||
*/
|
||||
public function pauseTransactions() {
|
||||
if($this->isPaused()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->resumeLevels = $this->transactionSavePointLevel;
|
||||
while($this->transactionSavePointLevel > 0) {
|
||||
$this->commit();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @see pauseTransactions()
|
||||
*/
|
||||
public function resumeTransactions() {
|
||||
while($this->resumeLevels > 0) {
|
||||
$this->beginTransaction();
|
||||
$this->resumeLevels--;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rollback the DB transaction
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function rollBack(): bool
|
||||
{
|
||||
if($this->transactionSavePointLevel == 0) {
|
||||
throw new LogicException("Not in transaction!");
|
||||
}
|
||||
|
||||
$this->transactionSavePointLevel--;
|
||||
|
||||
if($this->debug) {
|
||||
go()->warn("ROLLBACK DB TRANSACTION " . $this->transactionSavePointLevel, 1);
|
||||
}
|
||||
|
||||
if($this->transactionSavePointLevel == 0) {
|
||||
return $this->getPdo()->rollBack();
|
||||
}else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Commit the database transaction
|
||||
*
|
||||
* @return boolean
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function commit(): bool
|
||||
{
|
||||
if($this->transactionSavePointLevel == 0) {
|
||||
throw new PDOException("Not in transaction!");
|
||||
}
|
||||
|
||||
$this->transactionSavePointLevel--;
|
||||
|
||||
if($this->debug) {
|
||||
go()->debug("COMMIT DB TRANSACTION " . $this->transactionSavePointLevel, 1);
|
||||
}
|
||||
|
||||
|
||||
if($this->transactionSavePointLevel == 0) {
|
||||
return $this->getPdo()->commit();
|
||||
}else
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a transaction is active
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public function inTransaction(): bool
|
||||
{
|
||||
return $this->getPdo()->inTransaction();
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock the table of this model and also for the 't' alias.
|
||||
*
|
||||
* The locks array should be indexed by model name and the value is an array with two optional values.
|
||||
* THe first is a boolean that enables a write-lock and the second is a table alias.
|
||||
*
|
||||
* @param array $locks eg. [go\cores\Users\Model\User::tableName() => [true, 't']]
|
||||
*
|
||||
* @throws PDOException
|
||||
*/
|
||||
public function lock(array $locks)
|
||||
{
|
||||
$sql = "LOCK TABLES ";
|
||||
|
||||
foreach ($locks as $tableName => $lockInfo) {
|
||||
$sql .= $tableName . " ";
|
||||
|
||||
if (isset($lockInfo[1])) {
|
||||
$sql .= ' AS ' . $lockInfo[1] . ' ';
|
||||
}
|
||||
$sql .= empty($lockInfo[0]) ? 'READ' : 'WRITE';
|
||||
|
||||
$sql .= ', ';
|
||||
}
|
||||
|
||||
$sql = rtrim($sql, ', ');
|
||||
|
||||
App::get()->getDbConnection()->exec($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a delete statement
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param Query|array|string|null $query
|
||||
* @return Statement
|
||||
* @example
|
||||
* ```
|
||||
* $success = App::get()
|
||||
* ->getDbConnection()
|
||||
* ->delete("test_a", ['id' => 1])
|
||||
* ->execute();
|
||||
* ```
|
||||
*/
|
||||
public function delete(string $tableName, $query = null): Statement
|
||||
{
|
||||
$query = Query::normalize($query);
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildDelete($tableName, $query);
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an insert statement
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param array|Query $data Key value array, a numeric array with key value arrays or select query
|
||||
* @param string[] $columns If $data is a query object then you can supply the
|
||||
* selected columns with this parameter. If not given all columns must be
|
||||
* selected in the correct order.
|
||||
*
|
||||
* @return Statement
|
||||
* @throws PDOException
|
||||
* @example Single record
|
||||
* ```
|
||||
* $data = [
|
||||
* "propA" => "string 1",
|
||||
* "createdAt" => new \DateTime(),
|
||||
* "modifiedAt" => new \DateTime()
|
||||
* ];
|
||||
*
|
||||
* $result = App::get()
|
||||
* ->getDbConnection()
|
||||
* ->insert("test_a", $data)
|
||||
* ->execute();
|
||||
* ```
|
||||
*
|
||||
* Get the ID if it has an auto increment column:
|
||||
* ```
|
||||
* $id = App::get()->getDbConnection()->getPDO()->lastInsertId();
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @example Multiple records
|
||||
* ```
|
||||
* $data = [[
|
||||
* "propA" => "string 1",
|
||||
* "createdAt" => new \DateTime(),
|
||||
* "modifiedAt" => new \DateTime()
|
||||
* ],[
|
||||
* "propA" => "string 2",
|
||||
* "createdAt" => new \DateTime(),
|
||||
* "modifiedAt" => new \DateTime()
|
||||
* ]];
|
||||
*
|
||||
* $result = App::get()
|
||||
* ->getDbConnection()
|
||||
* ->insert("test_a", $data)
|
||||
* ->execute();
|
||||
* ```
|
||||
*
|
||||
* Or with an expression:
|
||||
*
|
||||
* ```
|
||||
* App::get()->getDbConnection()
|
||||
* ->update("core_state", new \go\core\db\Expression("highestModSeq = highestModSeq + 1"), $query);
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
public function insert(string $tableName, $data, array $columns = []): Statement
|
||||
{
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildInsert($tableName, $data, $columns);
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert data in the database and ignore if the records exist
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param array|Query $data Key value array or select query
|
||||
* @param string[] $columns If $data is a query object then you can supply the
|
||||
* selected columns with this parameter. If not given all columns must be
|
||||
* selected in the correct order.
|
||||
*
|
||||
* @return Statement
|
||||
* @throws DbException
|
||||
* @see insert()
|
||||
*/
|
||||
public function insertIgnore(string $tableName, Query|array $data, array $columns = []): Statement
|
||||
{
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildInsert($tableName, $data, $columns, "INSERT IGNORE");
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace data in the database
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param array|Query $data Key value array, array of key value arrays for multi insert or select query
|
||||
* @param string[] $columns If $data is a query object then you can supply the
|
||||
* selected columns with this parameter. If not given all columns must be
|
||||
* selected in the correct order.
|
||||
*
|
||||
* @return Statement
|
||||
* @throws Throwable
|
||||
* @see insert()
|
||||
*/
|
||||
public function replace(string $tableName, Query|array $data, array $columns = []): Statement
|
||||
{
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildInsert($tableName, $data, $columns, "REPLACE");
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an update statement
|
||||
*
|
||||
* @param string $tableName
|
||||
* @param $data
|
||||
* @param Query|string|array|null $query {@see Query::normalize()}
|
||||
* @return Statement
|
||||
* @throws PDOException
|
||||
* @example with join
|
||||
* ```
|
||||
* go()->getDbConnection()->update(
|
||||
* 'core_acl',
|
||||
* [
|
||||
* 'acl.entityTypeId' => $entityTypeId,
|
||||
* 'acl.entityId' => new Expression('ab.id')], // Use go\core\db\Expression for references to tables
|
||||
* (new Query())
|
||||
* ->tableAlias('acl') // set alias for core_acl table
|
||||
* ->join('addressbook_addressbook, 'ab', 'ab.aclId = acl.id'))
|
||||
* ->execute();
|
||||
* ```
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```
|
||||
* $data = [
|
||||
* "propA" => "string 3",
|
||||
* "count" => new \go\core\db\Expression('count + 1'), //Example for expression
|
||||
* ];
|
||||
*
|
||||
* $stmt = App::get()->getDbConnection()->update("test_a", $data, ['id' => 1]);
|
||||
* $stmt->execute();
|
||||
* ```
|
||||
*
|
||||
*/
|
||||
public function update(string $tableName, $data, Criteria|array|string|null $query = null): Statement
|
||||
{
|
||||
$query = Query::normalize($query);
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildUpdate($tableName, $data, $query);
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update but with ignore
|
||||
*
|
||||
* @param $tableName
|
||||
* @param $data
|
||||
* @param Criteria|array|string|null $query
|
||||
* @return Statement
|
||||
* @throws Exception
|
||||
* @see update()
|
||||
*/
|
||||
public function updateIgnore($tableName, $data, Criteria|array|string|null $query = null): Statement
|
||||
{
|
||||
$query = Query::normalize($query);
|
||||
|
||||
$queryBuilder = new QueryBuilder($this);
|
||||
$build = $queryBuilder->buildUpdate($tableName, $data, $query, "UPDATE IGNORE");
|
||||
|
||||
return $this->createStatement($build);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a select statement.
|
||||
*
|
||||
* @param string|string[] $select
|
||||
* @return Query
|
||||
* @example
|
||||
* ```
|
||||
* $query = go()->getDbConnection()
|
||||
* ->select('*')
|
||||
* ->from('test_a')
|
||||
* ->where('id', '=', 1);
|
||||
*
|
||||
* $stmt = $query->execute();
|
||||
*
|
||||
* ```
|
||||
*
|
||||
* @see Query
|
||||
*/
|
||||
public function select(array|string $select = "*"): Query
|
||||
{
|
||||
$query = new Query();
|
||||
return $query->setDbConnection($this)->select($select);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a single column or count(*) for example.
|
||||
*
|
||||
* Shortcut for:
|
||||
* $query->fetchMode(\PDO::FETCH_COLUMN,0)->select($select)
|
||||
*
|
||||
* @param string $select
|
||||
* @return Query
|
||||
*/
|
||||
public function selectSingleValue(string $select): Query
|
||||
{
|
||||
$query = new Query();
|
||||
return $query->setDbConnection($this)->selectSingleValue($select);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a statement from a QueryBuilder result.
|
||||
*
|
||||
* For internal use of the API.
|
||||
*
|
||||
* @param array $build
|
||||
* @return Statement
|
||||
* @throws Exception
|
||||
*/
|
||||
public function createStatement(array &$build): Statement
|
||||
{
|
||||
try {
|
||||
if($this->debug) {
|
||||
$build['start'] = go()->getDebugger()->getMicroTime();
|
||||
}
|
||||
|
||||
$pdoStmt = $this->getPDO()->prepare($build['sql']);
|
||||
|
||||
$stmt = new Statement($pdoStmt);
|
||||
$stmt->setBuild($build);
|
||||
|
||||
for ($i =0, $l = count($build['params']); $i < $l; $i++) {
|
||||
$stmt->bindValue($i + 1, $build['params'][$i]['value'], $build['params'][$i]['pdoType']);
|
||||
}
|
||||
|
||||
return $stmt;
|
||||
} catch(PDOException $e) {
|
||||
throw new DbException($e, go()->getDebugger()->enabled ? QueryBuilder::debugBuild($build) : null);
|
||||
} catch(Exception $e) {
|
||||
if(go()->getDebugger()->enabled) {
|
||||
go()->debug(QueryBuilder::debugBuild($build));
|
||||
}
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get or set foreign key checks
|
||||
*
|
||||
* @param bool|null $value Provide to set new value
|
||||
* @return bool The current or old value
|
||||
* @throws DbException
|
||||
*/
|
||||
public function foreignKeyChecks(bool|null $value = null): bool
|
||||
{
|
||||
$stmt = $this->query("SELECT @@SESSION.foreign_key_checks");
|
||||
$current = !!$stmt->fetch(PDO::FETCH_COLUMN, 0);
|
||||
|
||||
if(isset($value) && $value != $current) {
|
||||
$this->exec("set foreign_key_checks = " . ($value ? "1" : "0") . ";");
|
||||
}
|
||||
|
||||
return $current;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
require "docker-config.php";
|
||||
|
||||
$config['db_port'] = 3306;
|
||||
|
||||
/*
|
||||
* Database TLS
|
||||
*/
|
||||
$config['db_ssl_ca'] = "/etc/ssl/groupoffice/{{ root_cert_filename }}";
|
||||
$config['db_ssl_verify_server_cert'] = true;
|
||||
|
||||
/*
|
||||
* Connection.php is instantiated while the application is being initialized.
|
||||
* Export DB TLS settings as constants to avoid calling App::get() recursively
|
||||
* from the Connection constructor.
|
||||
*/
|
||||
if (!defined('GO_DB_SSL_CA')) {
|
||||
define('GO_DB_SSL_CA', $config['db_ssl_ca']);
|
||||
}
|
||||
|
||||
if (!defined('GO_DB_SSL_VERIFY_SERVER_CERT')) {
|
||||
define(
|
||||
'GO_DB_SSL_VERIFY_SERVER_CERT',
|
||||
$config['db_ssl_verify_server_cert']
|
||||
);
|
||||
}
|
||||
|
||||
$config['file_storage_path'] = "/var/lib/groupoffice";
|
||||
$config['tmpdir'] = "/tmp/groupoffice";
|
||||
$config['debug'] = false;
|
||||
$config['business'] = array (
|
||||
'studio' =>
|
||||
array (
|
||||
'package' => 'studio',
|
||||
)
|
||||
);
|
||||
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
require('../vendor/autoload.php');
|
||||
use go\core\App;
|
||||
use go\core\fs\Folder;
|
||||
use go\core\util\IniFile;
|
||||
|
||||
|
||||
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
|
||||
header("Cache-Control: post-check=0, pre-check=0", false);
|
||||
header("Pragma: no-cache");
|
||||
|
||||
|
||||
$dbConnectError = null;
|
||||
$pdo = null;
|
||||
/**
|
||||
*
|
||||
* @global PDO $pdo
|
||||
* @param type $config
|
||||
* @return PDO
|
||||
*/
|
||||
function dbConnect($config){
|
||||
global $pdo, $dbConnectError;
|
||||
if(isset($pdo)) {
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
$dsn = go()->createDsn($config['db_name'] ?? "groupoffice", $config);
|
||||
/*
|
||||
* Database TLS
|
||||
*/
|
||||
$options = [];
|
||||
|
||||
if (!empty($config['db_ssl_ca'])) {
|
||||
$verifyServerCert =
|
||||
$config['db_ssl_verify_server_cert'] ?? true;
|
||||
|
||||
if (version_compare(PHP_VERSION, '8.5.0') >= 0) {
|
||||
$options[PDO\Mysql::ATTR_SSL_CA] =
|
||||
$config['db_ssl_ca'];
|
||||
|
||||
$options[PDO\Mysql::ATTR_SSL_VERIFY_SERVER_CERT] =
|
||||
$verifyServerCert;
|
||||
} else {
|
||||
$options[PDO::MYSQL_ATTR_SSL_CA] =
|
||||
$config['db_ssl_ca'];
|
||||
|
||||
$options[PDO::MYSQL_ATTR_SSL_VERIFY_SERVER_CERT] =
|
||||
$verifyServerCert;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
$pdo = new PDO(
|
||||
$dsn,
|
||||
$config['db_user'],
|
||||
$config['db_pass'],
|
||||
$options
|
||||
);
|
||||
}
|
||||
/*
|
||||
* Database TLS
|
||||
*/
|
||||
catch(Exception $e){
|
||||
$dbConnectError = "Could not connect to the database with DSN = '" . $dsn . "' with username '" . ($config['db_user'] ?? "groupoffice") . " and " . (empty($config['db_pass']) ? "NOT using password" : "USING a password") . ". The database returned this error:<br />".$e->getMessage();
|
||||
}
|
||||
|
||||
if($pdo) {
|
||||
$clientVersion = $pdo->getAttribute(PDO::ATTR_CLIENT_VERSION);
|
||||
if (strpos($clientVersion, 'mysqlnd') === false) {
|
||||
$dbConnectError = "PDO is not using the mysqlnd driver. Please make sure PDO uses mysqlnd. It's now using: " . $clientVersion;
|
||||
$pdo = null;
|
||||
}
|
||||
}
|
||||
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
|
||||
|
||||
$configFile = App::findConfigFile();
|
||||
if(!$configFile) {
|
||||
$configFile = '/etc/groupoffice/config.php';
|
||||
} else
|
||||
{
|
||||
require($configFile);
|
||||
}
|
||||
|
||||
$mustSave = false;
|
||||
|
||||
if($_SERVER['REQUEST_METHOD'] != 'POST')
|
||||
{
|
||||
|
||||
$config['file_storage_path'] = $config['file_storage_path'] ?? "/var/lib/groupoffice";
|
||||
$config['tmpdir'] = $config['tmpdir'] ?? "/tmp/groupoffice";
|
||||
|
||||
//check if config.ini is already prefilled with usable data (Docker)
|
||||
$tmpFolder = new Folder($config['tmpdir']);
|
||||
$dataFolder = new Folder($config['file_storage_path']);
|
||||
if(!$dataFolder->isWritable() && isset($_SERVER['DOCUMENT_ROOT'])) {
|
||||
$dataFolder = new Folder(dirname($_SERVER['DOCUMENT_ROOT']) . '/groupoffice');
|
||||
$mustSave = true;
|
||||
}
|
||||
|
||||
if(dbConnect($config) && $dataFolder->isWritable() && $tmpFolder->isWritable() && !$mustSave) {
|
||||
header("Location: install.php");
|
||||
exit();
|
||||
}
|
||||
|
||||
$_POST['dbPort'] = $config['db_port'] ?? 3306;
|
||||
$_POST['dbHostname'] = $config['db_host'] ?? 'localhost';
|
||||
$_POST['dbName'] = $config['db_name'] ?? "groupoffice";
|
||||
|
||||
$_POST['dbUsername'] = $config['db_user'] ?? "groupoffice";
|
||||
//$_POST['dbPassword'] = $config['db']['password'];
|
||||
} else
|
||||
{
|
||||
// $config['general']['dataPath'] = $_POST['dataPath'];
|
||||
// $config['general']['tmpPath'] = $_POST['tmpPath'];
|
||||
// $config['db']['dsn'] = 'mysql:host='.$_POST['dbHostname'].';port='.$_POST['dbPort'].';dbname='.$_POST['dbName'];
|
||||
// $config['db']['username'] = $_POST['dbUsername'];
|
||||
// $config['db']['password'] = $_POST['dbPassword'];
|
||||
|
||||
$config['file_storage_path'] = $_POST['dataPath'];
|
||||
$config['tmpdir'] = $_POST['tmpPath'];
|
||||
$config['db_host'] =$_POST['dbHostname'];
|
||||
$config['db_name'] =$_POST['dbName'];
|
||||
$config['db_user'] =$_POST['dbUsername'];
|
||||
$config['db_pass'] =$_POST['dbPassword'];
|
||||
$config['db_port'] =$_POST['dbPort'];
|
||||
|
||||
$tmpFolder = new Folder($config['tmpdir']);
|
||||
$dataFolder = new Folder($config['file_storage_path']);
|
||||
|
||||
|
||||
if($dataFolder->isWritable() && $tmpFolder->isWritable() && dbConnect($config)) {
|
||||
$cFile = new \go\core\fs\File($configFile);
|
||||
|
||||
if(!$cFile->putContents("<?php\n\n\$config = ".var_export($config, true) .";\n\n")) {
|
||||
$configError = "Could not write config.php";
|
||||
} else
|
||||
{
|
||||
if(function_exists("opcache_invalidate")) {
|
||||
opcache_invalidate($cFile->getPath());
|
||||
}
|
||||
|
||||
header('Location: install.php');
|
||||
exit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
require('header.php');
|
||||
?>
|
||||
|
||||
<?php
|
||||
try {
|
||||
$isWritable = is_writable($configFile);
|
||||
} catch(Throwable $e) {
|
||||
//open basedir restriction
|
||||
$isWritable = false;
|
||||
}
|
||||
if(!$isWritable):
|
||||
|
||||
?>
|
||||
|
||||
<section>
|
||||
<fieldset>
|
||||
<h2><?= go()->t("Config file"); ?></h2>
|
||||
<p class="error">Please create a writeable config.php file here: <?= $configFile; ?>.</p>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
|
||||
<?php endif ?>
|
||||
|
||||
<section>
|
||||
<form method="POST" action="" onsubmit="submitButton.disabled = true;">
|
||||
|
||||
|
||||
<fieldset>
|
||||
<h2>Data storage</h2>
|
||||
|
||||
<p>
|
||||
Please create a folder for storing Group-Office files and a temporary files folder and make sure the webserver user can write to these folders.
|
||||
</p>
|
||||
|
||||
<?php
|
||||
if(!$dataFolder->isWritable()) {
|
||||
echo '<p class="error">Not writable</p>';
|
||||
}
|
||||
?>
|
||||
|
||||
<p>
|
||||
<input type="text" name="dataPath" value="<?= htmlspecialchars($dataFolder->getPath()) ?>" placeholder="" required />
|
||||
<label>Data folder</label>
|
||||
</p>
|
||||
|
||||
|
||||
<?php
|
||||
if(!$tmpFolder->isWritable()) {
|
||||
echo '<p class="error">Not writable</p>';
|
||||
}
|
||||
?>
|
||||
<p>
|
||||
<input type="text" name="tmpPath" value="<?= htmlspecialchars($config['tmpdir']); ?>" placeholder="Temp folder" required />
|
||||
<label>Temp folder</label>
|
||||
</p>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<h2>Configure the database</h2>
|
||||
|
||||
<?php
|
||||
if(isset($config['db_host']) && !dbConnect($config)) {
|
||||
echo '<p class="error">'.$dbConnectError.'</p>';
|
||||
}
|
||||
?>
|
||||
<p>
|
||||
<label>Database user</label>
|
||||
<input type="text" name="dbUsername" value="<?= htmlspecialchars($_POST['dbUsername'] ?? "groupoffice"); ?>" required />
|
||||
</p>
|
||||
<p>
|
||||
<label>Database password</label>
|
||||
<input type="password" name="dbPassword" value="<?= htmlspecialchars($_POST['dbPassword'] ?? "");?>" required />
|
||||
</p>
|
||||
<p>
|
||||
<label>Database name</label>
|
||||
<input type="text" name="dbName" value="<?= htmlspecialchars($_POST['dbName'] ?? "groupoffice");?>" required />
|
||||
</p>
|
||||
<p>
|
||||
<label>Database hostname</label>
|
||||
<input type="text" name="dbHostname" value="<?= htmlspecialchars($_POST['dbHostname'] ?? "localhost");?>" required />
|
||||
</p>
|
||||
<p>
|
||||
<label>Database port</label>
|
||||
<input type="text" name="dbPort" value="<?= htmlspecialchars($_POST['dbPort'] ?? 3306); ?>" required />
|
||||
</p>
|
||||
|
||||
<button class="primary right" name="submitButton"><?= go()->t('Continue'); ?></button>
|
||||
</fieldset>
|
||||
|
||||
|
||||
</form>
|
||||
</section>
|
||||
|
||||
|
||||
|
||||
<?php require('footer.php'); ?>
|
||||
@@ -0,0 +1 @@
|
||||
openssl.cafile=/etc/ssl/groupoffice/{{ root_cert_filename }}
|
||||
@@ -0,0 +1,41 @@
|
||||
[Quadlet]
|
||||
DefaultDependencies=false
|
||||
|
||||
[Unit]
|
||||
Description=GroupOffice
|
||||
|
||||
[Container]
|
||||
Image=docker.io/intermesh/groupoffice:{{ version['containers']['groupoffice'] }}
|
||||
|
||||
ContainerName=groupoffice
|
||||
HostName=groupoffice
|
||||
|
||||
PublishPort={{ services['groupoffice']['ports']['http'] }}:80/tcp
|
||||
|
||||
Volume=%h/data/containers/groupoffice/data:/var/lib/groupoffice:rw
|
||||
Volume=%h/containers/groupoffice/ssl/{{ root_cert_filename }}:/etc/ssl/groupoffice/{{ root_cert_filename }}:ro
|
||||
# Keep the official image intact.
|
||||
Volume=%h/containers/groupoffice/config/config.php:/etc/groupoffice/config.php:ro
|
||||
Volume=%h/containers/groupoffice/config/groupoffice-ca.ini:/usr/local/etc/php/conf.d/99-groupoffice-ca.ini:ro
|
||||
Volume=%h/containers/groupoffice/config/Authenticator.php:/usr/local/share/groupoffice/go/modules/community/imapauthenticator/model/Authenticator.php:ro
|
||||
Volume=%h/containers/groupoffice/config/db_Connection.php:/usr/local/share/groupoffice/go/core/db/Connection.php:ro
|
||||
Volume=%h/containers/groupoffice/config/configfile.php:/usr/local/share/groupoffice/install/configfile.php:ro
|
||||
|
||||
Environment="TZ={{ timezone }}"
|
||||
|
||||
# GroupOffice official Docker DB configuration
|
||||
Environment="MYSQL_HOST={{ services['mariadb']['domain']['internal'] }}.{{ domain['internal'] }}"
|
||||
Environment="MYSQL_DATABASE=groupoffice_db"
|
||||
Environment="MYSQL_USER=groupoffice"
|
||||
Secret=GROUPOFFICE_DB_PASSWORD,type=env,target=MYSQL_PASSWORD
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/usr/bin/nc -zv {{ services['mariadb']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['mariadb']['ports']['tcp'] }}
|
||||
ExecStartPre=/usr/bin/nc -zv {{ services['maddy']['domain']['internal'] }}.{{ domain['internal'] }} 993
|
||||
ExecStartPost=-/usr/bin/podman exec -u www-data groupoffice php /usr/local/share/groupoffice/cli.php core/System/upgrade
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=GroupOffice cron.php
|
||||
Requires=groupoffice.service
|
||||
After=groupoffice.service
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/usr/bin/podman exec -u www-data groupoffice \
|
||||
php /usr/local/share/groupoffice/cron.php
|
||||
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Run GroupOffice cron every 1 minutes
|
||||
|
||||
[Timer]
|
||||
OnBootSec=1min
|
||||
OnUnitActiveSec=1min
|
||||
Unit=groupoffice-cron.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
@@ -1,6 +1,7 @@
|
||||
{% set icon_map = {
|
||||
'ldap': 'lldap',
|
||||
'tandoor': 'tandoor-recipes',
|
||||
'ldap': 'lldap'
|
||||
'openwebui': 'open-webui'
|
||||
} %}
|
||||
|
||||
- Services:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
[Quadlet]
|
||||
DefaultDependencies=false
|
||||
|
||||
[Unit]
|
||||
Description=Kaneo
|
||||
|
||||
[Container]
|
||||
Image=ghcr.io/usekaneo/kaneo:{{ version['containers']['kaneo'] }}
|
||||
|
||||
ContainerName=kaneo
|
||||
HostName=kaneo
|
||||
|
||||
PublishPort={{ services['kaneo']['ports']['http'] }}:5173/tcp
|
||||
|
||||
Volume=%h/containers/kaneo/ssl:/etc/ssl/kaneo:ro
|
||||
|
||||
Environment="TZ={{ timezone }}"
|
||||
Environment="DISABLE_PASSWORD_REGISTRATION=true"
|
||||
Environment="DISABLE_REGISTRATION=false"
|
||||
Environment="DISABLE_GUEST_ACCESS=true"
|
||||
Environment="KANEO_CLIENT_URL=https://{{ services['kaneo']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
Secret=KANEO_AUTH_SECRET,type=env,target=AUTH_SECRET
|
||||
|
||||
# PostgreSQL
|
||||
Environment="POSTGRES_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}"
|
||||
Environment="POSTGRES_PORT={{ services['postgresql']['ports']['tcp'] }}"
|
||||
Environment="POSTGRES_DB=kaneo_db"
|
||||
Environment="POSTGRES_USER=kaneo"
|
||||
Environment="PGSSLMODE=verify-full"
|
||||
Environment="NODE_EXTRA_CA_CERTS=/etc/ssl/kaneo/{{ root_cert_filename }}"
|
||||
Secret=KANEO_DB_PASSWORD,type=env,target=POSTGRES_PASSWORD
|
||||
|
||||
Environment="CUSTOM_OAUTH_CLIENT_ID=kaneo"
|
||||
Environment="CUSTOM_OAUTH_DISCOVERY_URL=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}/.well-known/openid-configuration"
|
||||
Environment="CUSTOM_OAUTH_SCOPES=openid,profile,email"
|
||||
Environment="CUSTOM_AUTH_PKCE=true"
|
||||
Secret=KANEO_OIDC_CLIENT_SECRET,type=env,target=CUSTOM_OAUTH_CLIENT_SECRET
|
||||
|
||||
|
||||
[Service]
|
||||
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
|
||||
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,13 @@
|
||||
version = 1
|
||||
|
||||
{% for models in (services['llama']['dependency']['models'] | dict2items | list) %}
|
||||
{% if models['key'] == 'default' %}
|
||||
[*]
|
||||
{{ models['value'] }}
|
||||
|
||||
{% else %}
|
||||
[{{ models['key'] }}]
|
||||
{{ models['value'] }}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}{{ '' }}
|
||||
@@ -0,0 +1,34 @@
|
||||
[Quadlet]
|
||||
DefaultDependencies=false
|
||||
|
||||
[Unit]
|
||||
Description=llama.cpp Model Server
|
||||
|
||||
[Container]
|
||||
Image=ghcr.io/ggml-org/llama.cpp:{{ version['containers']['llama'] }}
|
||||
|
||||
ContainerName=llama
|
||||
HostName=llama
|
||||
PublishPort={{ services['llama']['ports']['http'] }}:9931/tcp
|
||||
|
||||
# iGPU access for Vulkan
|
||||
AddDevice=/dev/dri:/dev/dri
|
||||
PodmanArgs=--group-add keep-groups
|
||||
|
||||
Volume=%h/containers/llama/models:/models:ro
|
||||
Volume=%h/containers/llama/config:/config:ro
|
||||
|
||||
Environment="TZ={{ timezone }}"
|
||||
|
||||
Exec=--models-preset /config/models.ini \
|
||||
--models-max 2 \
|
||||
--host 0.0.0.0 \
|
||||
--port 9931
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,84 @@
|
||||
[Quadlet]
|
||||
DefaultDependencies=false
|
||||
|
||||
[Unit]
|
||||
Description=Open WebUI
|
||||
|
||||
[Container]
|
||||
Image=ghcr.io/open-webui/open-webui:{{ version['containers']['openwebui'] }}
|
||||
ContainerName=openwebui
|
||||
HostName=openwebui
|
||||
PublishPort={{ services['openwebui']['ports']['http'] }}:8080/tcp
|
||||
|
||||
Volume=%h/data/containers/openwebui/data:/app/backend/data:rw
|
||||
Volume=%h/containers/openwebui/ssl:/etc/ssl/openwebui:ro
|
||||
|
||||
# General
|
||||
Environment="TZ={{ timezone }}"
|
||||
Environment="WEBUI_URL=https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
Environment="CORS_ALLOW_ORIGIN=https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}"
|
||||
Environment="ENABLE_PERSISTENT_CONFIG=false"
|
||||
Environment="ENABLE_SIGNUP=false"
|
||||
Environment="ENABLE_LOGIN_FORM=false"
|
||||
Environment="ENABLE_PASSWORD_AUTH=false"
|
||||
Environment="ENABLE_PASSWORD_CHANGE_FORM=false"
|
||||
Secret=OPEN_WEBUI_SECRET_KEY,type=env,target=WEBUI_SECRET_KEY
|
||||
# We use OpenAI-compatible backends, not Ollama
|
||||
Environment="ENABLE_OLLAMA_API=false"
|
||||
|
||||
# PostgreSQL
|
||||
Environment="DATABASE_TYPE=postgresql"
|
||||
Environment="DATABASE_HOST={{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }}"
|
||||
Environment="DATABASE_PORT={{ services['postgresql']['ports']['tcp'] }}"
|
||||
Environment="DATABASE_NAME=openwebui_db"
|
||||
Environment="DATABASE_USER=openwebui"
|
||||
Environment="PGSSLMODE=verify-full"
|
||||
Environment="PGSSLROOTCERT=/etc/ssl/openwebui/{{ root_cert_filename }}"
|
||||
Environment="PGSSLCERTMODE=disable"
|
||||
Secret=OPEN_WEBUI_POSTGRES_PASSWORD,type=env,target=DATABASE_PASSWORD
|
||||
|
||||
# OIDC
|
||||
Environment="ENABLE_OAUTH_PERSISTENT_CONFIG=false"
|
||||
Environment="ENABLE_OAUTH_SIGNUP=true"
|
||||
Environment="OAUTH_AUTO_REDIRECT=false"
|
||||
Environment="OAUTH_MERGE_ACCOUNTS_BY_EMAIL=true"
|
||||
Environment="OAUTH_CLIENT_ID=openwebui"
|
||||
Environment="OPENID_PROVIDER_URL=https://{{ services['authelia']['domain']['public'] }}.{{ domain['public'] }}/.well-known/openid-configuration"
|
||||
Environment="OAUTH_PROVIDER_NAME=Authelia"
|
||||
Environment="OAUTH_SCOPES=openid email profile groups"
|
||||
Environment="ENABLE_OAUTH_ROLE_MANAGEMENT=true"
|
||||
Environment="OAUTH_ALLOWED_ROLES=admins,users"
|
||||
Environment="OAUTH_ADMIN_ROLES=admins"
|
||||
Environment="OAUTH_ROLES_CLAIM=groups"
|
||||
Environment="OAUTH_CODE_CHALLENGE_METHOD=S256"
|
||||
Secret=OPEN_WEBUI_OAUTH_CLIENT_SECRET,type=env,target=OAUTH_CLIENT_SECRET
|
||||
|
||||
# AI backend server
|
||||
# llama.cpp
|
||||
Environment="OPENAI_API_BASE_URL=http://host.containers.internal:{{ services['llama']['ports']['http'] }}/v1"
|
||||
Environment="OPENAI_API_KEY=none"
|
||||
# stable-diffusion.cpp
|
||||
Environment="ENABLE_IMAGE_GENERATION=true"
|
||||
Environment="IMAGE_GENERATION_ENGINE=openai"
|
||||
Environment="IMAGES_OPENAI_API_BASE_URL=http://host.containers.internal:{{ services['stablediffusion']['ports']['http'] }}/v1"
|
||||
Environment="IMAGES_OPENAI_API_KEY=none"
|
||||
Environment="IMAGE_GENERATION_MODEL=sd-cpp-local"
|
||||
Environment="IMAGE_SIZE=1024x1024"
|
||||
Environment="AIOHTTP_CLIENT_TIMEOUT=3600"
|
||||
# Disable background LLM tasks
|
||||
Environment="ENABLE_TITLE_GENERATION=false"
|
||||
Environment="ENABLE_TAGS_GENERATION=false"
|
||||
Environment="ENABLE_FOLLOW_UP_GENERATION=false"
|
||||
Environment="ENABLE_AUTOCOMPLETE_GENERATION=false"
|
||||
Environment="DEFAULT_MODEL_METADATA={\"builtinTools\":{\"user_input\":false}}"
|
||||
|
||||
|
||||
[Service]
|
||||
MemoryKSM=yes
|
||||
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=120
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -0,0 +1,36 @@
|
||||
[Quadlet]
|
||||
DefaultDependencies=false
|
||||
|
||||
[Unit]
|
||||
Description=stable-diffusion.cpp Image Generation Server
|
||||
|
||||
[Container]
|
||||
Image=ghcr.io/leejet/stable-diffusion.cpp:{{ version['containers']['stablediffusion'] }}
|
||||
|
||||
ContainerName=stablediffusion
|
||||
HostName=stablediffusion
|
||||
PublishPort={{ services['stablediffusion']['ports']['http'] }}:1234/tcp
|
||||
|
||||
# iGPU access for container
|
||||
AddDevice=/dev/dri:/dev/dri
|
||||
PodmanArgs=--group-add keep-groups
|
||||
|
||||
Volume=%h/containers/stablediffusion/models:/models:ro
|
||||
|
||||
# General
|
||||
Environment="TZ={{ timezone }}"
|
||||
# Prevent Fence expiration time out f9r CYCL
|
||||
Environment="SYCL_PI_LEVEL_ZERO_USE_IMMEDIATE_COMMANDLISTS=1"
|
||||
|
||||
Entrypoint=/sd-server
|
||||
Exec=--listen-ip 0.0.0.0 \
|
||||
--listen-port 1234 \
|
||||
{{ services['stablediffusion']['dependency']['models'][stabeldiffusion_active_model] }}
|
||||
|
||||
[Service]
|
||||
Restart=always
|
||||
RestartSec=10s
|
||||
TimeoutStopSec=300
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
@@ -57,6 +57,17 @@ Environment="WEBAUTHN_ALLOWED_ORIGINS=https://{{ services['sure']['domain']['pub
|
||||
Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance"
|
||||
Environment="SECURITIES_PROVIDER=yahoo_finance"
|
||||
|
||||
# AI backend
|
||||
Environment="ASSISTANT_TYPE=builtin"
|
||||
Environment="OPENAI_URI_BASE=http://host.containers.internal:10000/v1"
|
||||
Environment="OPENAI_ACCESS_TOKEN=local"
|
||||
Environment="OPENAI_MODEL={{ services['llama']['dependency']['models'] | dict2items | rejectattr('key', 'equalto', 'default') | map(attribute='key') | last }}"
|
||||
Environment="LLM_CONTEXT_WINDOW=8192"
|
||||
Environment="OPENAI_REQUEST_TIMEOUT=600"
|
||||
Environment="ASSISTANT_MAX_TOOL_CALL_ITERATIONS=2"
|
||||
Environment="AI_RESPONSE_TIMEOUT=2400"
|
||||
|
||||
|
||||
[Service]
|
||||
MemoryKSM=yes
|
||||
ExecStartPre=/usr/bin/nc -zv {{ services['postgresql']['domain']['internal'] }}.{{ domain['internal'] }} {{ services['postgresql']['ports']['tcp'] }}
|
||||
|
||||
@@ -33,6 +33,16 @@ Secret=SURE_POSTGRES_PASSWORD,type=env,target=POSTGRES_PASSWORD
|
||||
# Redis
|
||||
Environment="REDIS_URL=redis://host.containers.internal:{{ services['sure']['ports']['redis'] }}/1"
|
||||
|
||||
# AI backend
|
||||
Environment="ASSISTANT_TYPE=builtin"
|
||||
Environment="OPENAI_URI_BASE=http://host.containers.internal:10000/v1"
|
||||
Environment="OPENAI_ACCESS_TOKEN=local"
|
||||
Environment="OPENAI_MODEL={{ services['llama']['dependency']['models'] | dict2items | rejectattr('key', 'equalto', 'default') | map(attribute='key') | last }}"
|
||||
Environment="LLM_CONTEXT_WINDOW=8192"
|
||||
Environment="OPENAI_REQUEST_TIMEOUT=600"
|
||||
Environment="ASSISTANT_MAX_TOOL_CALL_ITERATIONS=2"
|
||||
Environment="AI_RESPONSE_TIMEOUT=2400"
|
||||
|
||||
# Provider
|
||||
## Currency
|
||||
Environment="EXCHANGE_RATE_PROVIDER=yahoo_finance"
|
||||
|
||||
@@ -361,4 +361,48 @@ identity_providers:
|
||||
- 'profile'
|
||||
access_token_signed_response_alg: 'none'
|
||||
userinfo_signed_response_alg: 'none'
|
||||
token_endpoint_auth_method: 'client_secret_post'
|
||||
token_endpoint_auth_method: 'client_secret_post'
|
||||
# https://www.authelia.com/integration/openid-connect/clients/open-webui/
|
||||
- client_id: 'openwebui'
|
||||
client_name: 'openwebui'
|
||||
consent_mode: 'implicit'
|
||||
client_secret: '{{ hostvars['console']['openwebui']['oidc']['hash'] }}'
|
||||
public: false
|
||||
authorization_policy: 'one_factor'
|
||||
require_pkce: true
|
||||
pkce_challenge_method: 'S256'
|
||||
redirect_uris:
|
||||
- 'https://{{ services['openwebui']['domain']['public'] }}.{{ domain['public'] }}/oauth/oidc/callback'
|
||||
scopes:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'groups'
|
||||
- 'email'
|
||||
response_types:
|
||||
- 'code'
|
||||
grant_types:
|
||||
- 'authorization_code'
|
||||
access_token_signed_response_alg: 'none'
|
||||
userinfo_signed_response_alg: 'none'
|
||||
token_endpoint_auth_method: 'client_secret_basic'
|
||||
- client_id: 'kaneo'
|
||||
client_name: 'kaneo'
|
||||
consent_mode: 'implicit'
|
||||
client_secret: '{{ hostvars['console']['kaneo']['oidc']['hash'] }}'
|
||||
public: false
|
||||
authorization_policy: 'one_factor'
|
||||
require_pkce: true
|
||||
pkce_challenge_method: 'S256'
|
||||
redirect_uris:
|
||||
- 'https://{{ services['kaneo']['domain']['public'] }}.{{ domain['public'] }}/api/auth/oauth2/callback/custom'
|
||||
scopes:
|
||||
- 'openid'
|
||||
- 'profile'
|
||||
- 'email'
|
||||
response_types:
|
||||
- 'code'
|
||||
grant_types:
|
||||
- 'authorization_code'
|
||||
access_token_signed_response_alg: 'none'
|
||||
userinfo_signed_response_alg: 'none'
|
||||
token_endpoint_auth_method: 'client_secret_post'
|
||||
|
||||
@@ -60,6 +60,10 @@
|
||||
import crowdsec_log
|
||||
route {
|
||||
crowdsec
|
||||
{% if service['route'] is defined and service['route']['dav'] is defined and service['route']['dav'] %}
|
||||
redir /.well-known/caldav /dav 301
|
||||
redir /.well-known/carddav /dav 301
|
||||
{% endif %}
|
||||
{% if web_type == 'auth-public' %}
|
||||
reverse_proxy host.containers.internal:{{ service['ports']['http'] }}
|
||||
{% else %}
|
||||
|
||||
Reference in New Issue
Block a user