Compare commits
90
Commits
cd00fe057d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cadb633d51 | ||
|
|
9518bb8c6a | ||
|
|
9501fdcd25 | ||
|
|
4b9a5d7e78 | ||
|
|
67c7b5ccff | ||
|
|
7a143dc18a | ||
|
|
59b1a95e02 | ||
|
|
5011f714e1 | ||
|
|
8ac18050a0 | ||
|
|
60d70072b1 | ||
|
|
3bbf1f8ddc | ||
|
|
2d7d975c8b | ||
|
|
a914efc011 | ||
|
|
262943a425 | ||
|
|
0148b41353 | ||
|
|
9a8cc0689a | ||
|
|
5f01b21c58 | ||
|
|
65fb0f9dce | ||
|
|
d706864eed | ||
|
|
1bc4e6e36f | ||
|
|
0fcfa01262 | ||
|
|
c18294336f | ||
|
|
df937c43cb | ||
|
|
467fc2219a | ||
|
|
dd08482038 | ||
|
|
ac82e9b5cc | ||
|
|
f29f97dd5f | ||
|
|
ac56383edf | ||
|
|
881e877760 | ||
|
|
dbce5f71b9 | ||
|
|
6e39c4c71e | ||
|
|
c0bfad4445 | ||
|
|
33f0836479 | ||
|
|
4f6bd7de91 | ||
|
|
6ec770f27c | ||
|
|
8bd927dd23 | ||
|
|
11380e213c | ||
|
|
0de1f0dab4 | ||
|
|
d8f6eb147a | ||
|
|
535e374427 | ||
|
|
0b46873ed3 | ||
|
|
1ea18c23f8 | ||
|
|
b5c26c48bf | ||
|
|
24a8554368 | ||
|
|
5bddc3be0d | ||
|
|
31b080a2a0 | ||
|
|
9d08aa7b99 | ||
|
|
e82490018c | ||
|
|
74816f193b | ||
|
|
3565c271d4 | ||
|
|
a2c4e969ff | ||
|
|
4faf98e38a | ||
|
|
57c13785b9 | ||
|
|
3370231a63 | ||
|
|
9c74f58bce | ||
|
|
8c903efb51 | ||
|
|
867f2589c7 | ||
|
|
4f8ea5704e | ||
|
|
9a59a4116e | ||
|
|
278a926ddb | ||
|
|
76fc8f9c37 | ||
|
|
313a976090 | ||
|
|
84c7a3ffaf | ||
|
|
3fa9fa6d79 | ||
|
|
b178df10e3 | ||
|
|
bca61d2b9d | ||
|
|
01095093d7 | ||
|
|
91d85232ca | ||
|
|
de681f37b0 | ||
|
|
2f5232e1c7 | ||
|
|
2366370e67 | ||
|
|
9ede060821 | ||
|
|
b2332b790b | ||
|
|
3dd7361f45 | ||
|
|
39f500feec | ||
|
|
0d4fe60f45 | ||
|
|
d294253a27 | ||
|
|
f9701e936f | ||
|
|
52287c622f | ||
|
|
9493205995 | ||
|
|
ca8e9ab474 | ||
|
|
1352b1f33a | ||
|
|
6d1fcf67b3 | ||
|
|
28661a198a | ||
|
|
d8e56b297e | ||
|
|
f98863ad2a | ||
|
|
a5efbc50a6 | ||
|
|
8182aed0f7 | ||
|
|
abefba9b4d | ||
|
|
8a4db6b905 |
@@ -0,0 +1,47 @@
|
||||
# Copy this file one folder above the project as .env and replace every placeholder.
|
||||
# The application loads that parent .env automatically.
|
||||
|
||||
# Application
|
||||
APP_ENV=production
|
||||
PUBLIC_BASE_URL=https://zefix.silias.ch
|
||||
APP_SECRET=replace-with-output-of-openssl-rand-hex-32
|
||||
# Leave empty to use ../zefix-private next to the project directory.
|
||||
ZEFIX_PRIVATE_DIR=
|
||||
|
||||
# Cloudflare Turnstile
|
||||
TURNSTILE_SECRET=replace-with-turnstile-secret
|
||||
TURNSTILE_ALLOWED_HOSTNAME=zefix.silias.ch
|
||||
|
||||
# Zefix API
|
||||
username=replace-with-zefix-api-username
|
||||
password=replace-with-zefix-api-password
|
||||
|
||||
# SMTP
|
||||
SMTP_HOST=mxe98c.netcup.net
|
||||
SMTP_PORT=465
|
||||
SMTP_SECURE=ssl
|
||||
SMTP_USERNAME=info@silias.ch
|
||||
smtppassword=replace-with-smtp-password
|
||||
EXPORT_BCC_EMAIL=
|
||||
|
||||
# Abuse protection and job limits
|
||||
RATE_LIMIT_IP_MAX=3
|
||||
RATE_LIMIT_IP_WINDOW_SECONDS=900
|
||||
RATE_LIMIT_EMAIL_MAX=5
|
||||
RATE_LIMIT_EMAIL_WINDOW_SECONDS=86400
|
||||
FORM_MIN_SECONDS=2
|
||||
FORM_MAX_SECONDS=7200
|
||||
MAX_SEATS_PER_JOB=500
|
||||
MAX_LEGAL_FORMS_PER_JOB=50
|
||||
MAX_REQUESTS_PER_JOB=5000
|
||||
MAX_QUEUE_SIZE=20
|
||||
|
||||
# Retention and background processing
|
||||
DOWNLOAD_RETENTION_HOURS=48
|
||||
STALE_TASK_RETENTION_HOURS=168
|
||||
REFERENCE_CACHE_SECONDS=86400
|
||||
TASK_EXECUTOR_MAX_SECONDS=120
|
||||
TASK_MIN_AGE_SECONDS=10
|
||||
|
||||
# Only add IP addresses of reverse proxies you control.
|
||||
TRUSTED_PROXY_IPS=
|
||||
@@ -0,0 +1 @@
|
||||
*.sh text eol=lf
|
||||
@@ -0,0 +1,9 @@
|
||||
/env_vars.php
|
||||
/.env
|
||||
/.env.*
|
||||
!/.env.example
|
||||
/tasks/
|
||||
/download/
|
||||
/var/
|
||||
/*.log
|
||||
/.phpunit.cache/
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer Exception class.
|
||||
* PHP Version 5.5.
|
||||
*
|
||||
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
||||
*
|
||||
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
|
||||
namespace PHPMailer\PHPMailer;
|
||||
|
||||
/**
|
||||
* PHPMailer exception handler.
|
||||
*
|
||||
* @author Marcus Bointon <phpmailer@synchromedia.co.uk>
|
||||
*/
|
||||
class Exception extends \Exception
|
||||
{
|
||||
/**
|
||||
* Prettify error message output.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function errorMessage()
|
||||
{
|
||||
return '<strong>' . htmlspecialchars($this->getMessage()) . "</strong><br />\n";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer - PHP email creation and transport class.
|
||||
* PHP Version 5.5.
|
||||
*
|
||||
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
||||
*
|
||||
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
|
||||
namespace PHPMailer\PHPMailer;
|
||||
|
||||
use League\OAuth2\Client\Grant\RefreshToken;
|
||||
use League\OAuth2\Client\Provider\AbstractProvider;
|
||||
use League\OAuth2\Client\Token\AccessToken;
|
||||
|
||||
/**
|
||||
* OAuth - OAuth2 authentication wrapper class.
|
||||
* Uses the oauth2-client package from the League of Extraordinary Packages.
|
||||
*
|
||||
* @see http://oauth2-client.thephpleague.com
|
||||
*
|
||||
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
||||
*/
|
||||
class OAuth
|
||||
{
|
||||
/**
|
||||
* An instance of the League OAuth Client Provider.
|
||||
*
|
||||
* @var AbstractProvider
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* The current OAuth access token.
|
||||
*
|
||||
* @var AccessToken
|
||||
*/
|
||||
protected $oauthToken;
|
||||
|
||||
/**
|
||||
* The user's email address, usually used as the login ID
|
||||
* and also the from address when sending email.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $oauthUserEmail = '';
|
||||
|
||||
/**
|
||||
* The client secret, generated in the app definition of the service you're connecting to.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $oauthClientSecret = '';
|
||||
|
||||
/**
|
||||
* The client ID, generated in the app definition of the service you're connecting to.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $oauthClientId = '';
|
||||
|
||||
/**
|
||||
* The refresh token, used to obtain new AccessTokens.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $oauthRefreshToken = '';
|
||||
|
||||
/**
|
||||
* OAuth constructor.
|
||||
*
|
||||
* @param array $options Associative array containing
|
||||
* `provider`, `userName`, `clientSecret`, `clientId` and `refreshToken` elements
|
||||
*/
|
||||
public function __construct($options)
|
||||
{
|
||||
$this->provider = $options['provider'];
|
||||
$this->oauthUserEmail = $options['userName'];
|
||||
$this->oauthClientSecret = $options['clientSecret'];
|
||||
$this->oauthClientId = $options['clientId'];
|
||||
$this->oauthRefreshToken = $options['refreshToken'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new RefreshToken.
|
||||
*
|
||||
* @return RefreshToken
|
||||
*/
|
||||
protected function getGrant()
|
||||
{
|
||||
return new RefreshToken();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a new AccessToken.
|
||||
*
|
||||
* @return AccessToken
|
||||
*/
|
||||
protected function getToken()
|
||||
{
|
||||
return $this->provider->getAccessToken(
|
||||
$this->getGrant(),
|
||||
['refresh_token' => $this->oauthRefreshToken]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a base64-encoded OAuth token.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getOauth64()
|
||||
{
|
||||
//Get a new token if it's not available or has expired
|
||||
if (null === $this->oauthToken || $this->oauthToken->hasExpired()) {
|
||||
$this->oauthToken = $this->getToken();
|
||||
}
|
||||
|
||||
return base64_encode(
|
||||
'user=' .
|
||||
$this->oauthUserEmail .
|
||||
"\001auth=Bearer " .
|
||||
$this->oauthToken .
|
||||
"\001\001"
|
||||
);
|
||||
}
|
||||
}
|
||||
+4933
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,448 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* PHPMailer POP-Before-SMTP Authentication Class.
|
||||
* PHP Version 5.5.
|
||||
*
|
||||
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
|
||||
*
|
||||
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
* @author Brent R. Matzelle (original founder)
|
||||
* @copyright 2012 - 2020 Marcus Bointon
|
||||
* @copyright 2010 - 2012 Jim Jagielski
|
||||
* @copyright 2004 - 2009 Andy Prevost
|
||||
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
|
||||
* @note This program is distributed in the hope that it will be useful - WITHOUT
|
||||
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
* FITNESS FOR A PARTICULAR PURPOSE.
|
||||
*/
|
||||
|
||||
namespace PHPMailer\PHPMailer;
|
||||
|
||||
/**
|
||||
* PHPMailer POP-Before-SMTP Authentication Class.
|
||||
* Specifically for PHPMailer to use for RFC1939 POP-before-SMTP authentication.
|
||||
* 1) This class does not support APOP authentication.
|
||||
* 2) Opening and closing lots of POP3 connections can be quite slow. If you need
|
||||
* to send a batch of emails then just perform the authentication once at the start,
|
||||
* and then loop through your mail sending script. Providing this process doesn't
|
||||
* take longer than the verification period lasts on your POP3 server, you should be fine.
|
||||
* 3) This is really ancient technology; you should only need to use it to talk to very old systems.
|
||||
* 4) This POP3 class is deliberately lightweight and incomplete, implementing just
|
||||
* enough to do authentication.
|
||||
* If you want a more complete class there are other POP3 classes for PHP available.
|
||||
*
|
||||
* @author Richard Davey (original author) <rich@corephp.co.uk>
|
||||
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
|
||||
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
|
||||
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
|
||||
*/
|
||||
class POP3
|
||||
{
|
||||
/**
|
||||
* The POP3 PHPMailer Version number.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const VERSION = '6.3.0';
|
||||
|
||||
/**
|
||||
* Default POP3 port number.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_PORT = 110;
|
||||
|
||||
/**
|
||||
* Default timeout in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEFAULT_TIMEOUT = 30;
|
||||
|
||||
/**
|
||||
* POP3 class debug output mode.
|
||||
* Debug output level.
|
||||
* Options:
|
||||
* @see POP3::DEBUG_OFF: No output
|
||||
* @see POP3::DEBUG_SERVER: Server messages, connection/server errors
|
||||
* @see POP3::DEBUG_CLIENT: Client and Server messages, connection/server errors
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $do_debug = self::DEBUG_OFF;
|
||||
|
||||
/**
|
||||
* POP3 mail server hostname.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $host;
|
||||
|
||||
/**
|
||||
* POP3 port number.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $port;
|
||||
|
||||
/**
|
||||
* POP3 Timeout Value in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $tval;
|
||||
|
||||
/**
|
||||
* POP3 username.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $username;
|
||||
|
||||
/**
|
||||
* POP3 password.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $password;
|
||||
|
||||
/**
|
||||
* Resource handle for the POP3 connection socket.
|
||||
*
|
||||
* @var resource
|
||||
*/
|
||||
protected $pop_conn;
|
||||
|
||||
/**
|
||||
* Are we connected?
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $connected = false;
|
||||
|
||||
/**
|
||||
* Error container.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $errors = [];
|
||||
|
||||
/**
|
||||
* Line break constant.
|
||||
*/
|
||||
const LE = "\r\n";
|
||||
|
||||
/**
|
||||
* Debug level for no output.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_OFF = 0;
|
||||
|
||||
/**
|
||||
* Debug level to show server -> client messages
|
||||
* also shows clients connection errors or errors from server
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_SERVER = 1;
|
||||
|
||||
/**
|
||||
* Debug level to show client -> server and server -> client messages.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const DEBUG_CLIENT = 2;
|
||||
|
||||
/**
|
||||
* Simple static wrapper for all-in-one POP before SMTP.
|
||||
*
|
||||
* @param string $host The hostname to connect to
|
||||
* @param int|bool $port The port number to connect to
|
||||
* @param int|bool $timeout The timeout value
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param int $debug_level
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function popBeforeSmtp(
|
||||
$host,
|
||||
$port = false,
|
||||
$timeout = false,
|
||||
$username = '',
|
||||
$password = '',
|
||||
$debug_level = 0
|
||||
) {
|
||||
$pop = new self();
|
||||
|
||||
return $pop->authorise($host, $port, $timeout, $username, $password, $debug_level);
|
||||
}
|
||||
|
||||
/**
|
||||
* Authenticate with a POP3 server.
|
||||
* A connect, login, disconnect sequence
|
||||
* appropriate for POP-before SMTP authorisation.
|
||||
*
|
||||
* @param string $host The hostname to connect to
|
||||
* @param int|bool $port The port number to connect to
|
||||
* @param int|bool $timeout The timeout value
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
* @param int $debug_level
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorise($host, $port = false, $timeout = false, $username = '', $password = '', $debug_level = 0)
|
||||
{
|
||||
$this->host = $host;
|
||||
//If no port value provided, use default
|
||||
if (false === $port) {
|
||||
$this->port = static::DEFAULT_PORT;
|
||||
} else {
|
||||
$this->port = (int) $port;
|
||||
}
|
||||
//If no timeout value provided, use default
|
||||
if (false === $timeout) {
|
||||
$this->tval = static::DEFAULT_TIMEOUT;
|
||||
} else {
|
||||
$this->tval = (int) $timeout;
|
||||
}
|
||||
$this->do_debug = $debug_level;
|
||||
$this->username = $username;
|
||||
$this->password = $password;
|
||||
//Reset the error log
|
||||
$this->errors = [];
|
||||
//Connect
|
||||
$result = $this->connect($this->host, $this->port, $this->tval);
|
||||
if ($result) {
|
||||
$login_result = $this->login($this->username, $this->password);
|
||||
if ($login_result) {
|
||||
$this->disconnect();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
//We need to disconnect regardless of whether the login succeeded
|
||||
$this->disconnect();
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a POP3 server.
|
||||
*
|
||||
* @param string $host
|
||||
* @param int|bool $port
|
||||
* @param int $tval
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function connect($host, $port = false, $tval = 30)
|
||||
{
|
||||
//Are we already connected?
|
||||
if ($this->connected) {
|
||||
return true;
|
||||
}
|
||||
|
||||
//On Windows this will raise a PHP Warning error if the hostname doesn't exist.
|
||||
//Rather than suppress it with @fsockopen, capture it cleanly instead
|
||||
set_error_handler([$this, 'catchWarning']);
|
||||
|
||||
if (false === $port) {
|
||||
$port = static::DEFAULT_PORT;
|
||||
}
|
||||
|
||||
//Connect to the POP3 server
|
||||
$errno = 0;
|
||||
$errstr = '';
|
||||
$this->pop_conn = fsockopen(
|
||||
$host, //POP3 Host
|
||||
$port, //Port #
|
||||
$errno, //Error Number
|
||||
$errstr, //Error Message
|
||||
$tval
|
||||
); //Timeout (seconds)
|
||||
//Restore the error handler
|
||||
restore_error_handler();
|
||||
|
||||
//Did we connect?
|
||||
if (false === $this->pop_conn) {
|
||||
//It would appear not...
|
||||
$this->setError(
|
||||
"Failed to connect to server $host on port $port. errno: $errno; errstr: $errstr"
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//Increase the stream time-out
|
||||
stream_set_timeout($this->pop_conn, $tval, 0);
|
||||
|
||||
//Get the POP3 server response
|
||||
$pop3_response = $this->getResponse();
|
||||
//Check for the +OK
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
//The connection is established and the POP3 server is talking
|
||||
$this->connected = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Log in to the POP3 server.
|
||||
* Does not support APOP (RFC 2828, 4949).
|
||||
*
|
||||
* @param string $username
|
||||
* @param string $password
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function login($username = '', $password = '')
|
||||
{
|
||||
if (!$this->connected) {
|
||||
$this->setError('Not connected to POP3 server');
|
||||
}
|
||||
if (empty($username)) {
|
||||
$username = $this->username;
|
||||
}
|
||||
if (empty($password)) {
|
||||
$password = $this->password;
|
||||
}
|
||||
|
||||
//Send the Username
|
||||
$this->sendString("USER $username" . static::LE);
|
||||
$pop3_response = $this->getResponse();
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
//Send the Password
|
||||
$this->sendString("PASS $password" . static::LE);
|
||||
$pop3_response = $this->getResponse();
|
||||
if ($this->checkResponse($pop3_response)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the POP3 server.
|
||||
*/
|
||||
public function disconnect()
|
||||
{
|
||||
$this->sendString('QUIT');
|
||||
//The QUIT command may cause the daemon to exit, which will kill our connection
|
||||
//So ignore errors here
|
||||
try {
|
||||
@fclose($this->pop_conn);
|
||||
} catch (Exception $e) {
|
||||
//Do nothing
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a response from the POP3 server.
|
||||
*
|
||||
* @param int $size The maximum number of bytes to retrieve
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function getResponse($size = 128)
|
||||
{
|
||||
$response = fgets($this->pop_conn, $size);
|
||||
if ($this->do_debug >= self::DEBUG_SERVER) {
|
||||
echo 'Server -> Client: ', $response;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send raw data to the POP3 server.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
protected function sendString($string)
|
||||
{
|
||||
if ($this->pop_conn) {
|
||||
if ($this->do_debug >= self::DEBUG_CLIENT) { //Show client messages when debug >= 2
|
||||
echo 'Client -> Server: ', $string;
|
||||
}
|
||||
|
||||
return fwrite($this->pop_conn, $string, strlen($string));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the POP3 server response.
|
||||
* Looks for for +OK or -ERR.
|
||||
*
|
||||
* @param string $string
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
protected function checkResponse($string)
|
||||
{
|
||||
if (strpos($string, '+OK') !== 0) {
|
||||
$this->setError("Server reported an error: $string");
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an error to the internal error store.
|
||||
* Also display debug output if it's enabled.
|
||||
*
|
||||
* @param string $error
|
||||
*/
|
||||
protected function setError($error)
|
||||
{
|
||||
$this->errors[] = $error;
|
||||
if ($this->do_debug >= self::DEBUG_SERVER) {
|
||||
echo '<pre>';
|
||||
foreach ($this->errors as $e) {
|
||||
print_r($e);
|
||||
}
|
||||
echo '</pre>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of error messages, if any.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getErrors()
|
||||
{
|
||||
return $this->errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* POP3 connection error handler.
|
||||
*
|
||||
* @param int $errno
|
||||
* @param string $errstr
|
||||
* @param string $errfile
|
||||
* @param int $errline
|
||||
*/
|
||||
protected function catchWarning($errno, $errstr, $errfile, $errline)
|
||||
{
|
||||
$this->setError(
|
||||
'Connecting to the POP3 server raised a PHP warning:' .
|
||||
"errno: $errno errstr: $errstr; errfile: $errfile; errline: $errline"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,2 +1,115 @@
|
||||
# Zefix_search
|
||||
# Silias ZEFIX Export
|
||||
|
||||
Ein PHP-Dienst, der ZEFIX-Suchaufträge asynchron verarbeitet und den fertigen CSV-Export über einen zeitlich begrenzten Download-Link zustellt.
|
||||
|
||||
Vorausgesetzt werden PHP 8.1 oder neuer, die Erweiterungen cURL und JSON sowie ein korrekt konfigurierter CA-Zertifikatsspeicher für die TLS-Prüfung von Cloudflare, ZEFIX und SMTP.
|
||||
|
||||
## Missbrauchsschutz
|
||||
|
||||
Der Export ist standardmässig **fail-closed**: Ohne vollständig konfigurierte Cloudflare-Turnstile-Schlüssel bleibt der Absende-Button deaktiviert und `submit.php` nimmt keine Aufträge an.
|
||||
|
||||
Aktivierte Schutzschichten:
|
||||
|
||||
- Cloudflare Turnstile im Managed-Modus, inklusive serverseitiger Siteverify-Prüfung
|
||||
- Hostname- und Action-Prüfung für Turnstile-Tokens
|
||||
- signierte Formularzeit und Honeypot-Feld ohne Session-Cookie
|
||||
- Rate-Limits pro IP-Adresse und E-Mail-Adresse
|
||||
- harte Grenzen für Orte, Rechtsformen, resultierende API-Aufrufe und Queue-Grösse
|
||||
- Deduplizierung identischer offener Aufträge
|
||||
- private Speicherung von Auftragsdaten und Exportdateien ausserhalb des Webroots
|
||||
- 128-Bit-Download-Token und automatische Ablaufzeit
|
||||
- CLI-Sperre für Executor, Cleanup und administratives Löschen
|
||||
- eintägiger Cache für Gemeinden und Rechtsformen
|
||||
|
||||
## Erforderliche Umgebungsvariablen
|
||||
|
||||
| Variable | Bedeutung |
|
||||
| --- | --- |
|
||||
| `TURNSTILE_SECRET` | Geheimer Schlüssel des bestehenden Turnstile-Widgets für Siteverify |
|
||||
| `APP_SECRET` | Zufälliger geheimer Wert für Formulartoken und pseudonymisierte Rate-Limits |
|
||||
| `ZEFIX_PRIVATE_DIR` | Optionaler absoluter, dauerhaft beschreibbarer Pfad **ausserhalb** des Webroots; ohne Wert wird `../zefix-private` verwendet |
|
||||
| `username` | Benutzername der ZEFIX-API |
|
||||
| `password` | Passwort der ZEFIX-API |
|
||||
| `smtppassword` | Passwort des SMTP-Kontos |
|
||||
|
||||
Verwendet wird ausschliesslich das bestehende Turnstile-Widget `Zefix` mit dem Sitekey `0x4AAAAAAD7CJrWpNiK0-A7h`. Es darf nicht neu erstellt oder rotiert werden. Das Widget muss im Cloudflare-Dashboard als **Managed** mit dem Hostnamen `zefix.silias.ch` konfiguriert sein; Pre-Clearance bleibt deaktiviert. Das Secret gehört ausschliesslich als `TURNSTILE_SECRET` in die Serverumgebung und niemals ins Repository.
|
||||
|
||||
Empfohlene Werte:
|
||||
|
||||
```text
|
||||
TURNSTILE_ALLOWED_HOSTNAME=zefix.silias.ch
|
||||
PUBLIC_BASE_URL=https://zefix.silias.ch
|
||||
ZEFIX_PRIVATE_DIR=
|
||||
APP_SECRET=<mindestens 32 zufällige Bytes>
|
||||
```
|
||||
|
||||
Das bisherige, nicht versionierte `env_vars.php` wird für eine schonende Migration weiterhin geladen. Neue Installationen sollten echte Prozess-/PHP-FPM-Umgebungsvariablen verwenden.
|
||||
|
||||
Eine vollständige Vorlage ohne echte Zugangsdaten liegt in [`.env.example`](.env.example). Für eine klassische Installation kann sie als `.env` **einen Ordner über dem Projektverzeichnis** abgelegt werden. Diese Datei wird beim Start automatisch geladen. Bereits gesetzte Prozessvariablen und Werte aus der bisherigen `env_vars.php` haben Vorrang.
|
||||
|
||||
Beispiel bei einem Projekt unter `/var/www/zefix`:
|
||||
|
||||
```text
|
||||
/var/www/.env # echte, nicht versionierte Zugangsdaten
|
||||
/var/www/zefix/app.php # Projekt
|
||||
```
|
||||
|
||||
Alternativ kann die Vorlage weiterhin für ein Deployment-Panel, Docker Compose (`env_file`) oder die PHP-FPM-Konfiguration verwendet werden.
|
||||
|
||||
## Optionale Konfiguration
|
||||
|
||||
| Variable | Standard | Bedeutung |
|
||||
| --- | ---: | --- |
|
||||
| `RATE_LIMIT_IP_MAX` | `3` | Aufträge pro IP-Zeitfenster |
|
||||
| `RATE_LIMIT_IP_WINDOW_SECONDS` | `900` | IP-Zeitfenster |
|
||||
| `RATE_LIMIT_EMAIL_MAX` | `5` | Aufträge pro E-Mail-Zeitfenster |
|
||||
| `RATE_LIMIT_EMAIL_WINDOW_SECONDS` | `86400` | E-Mail-Zeitfenster |
|
||||
| `MAX_SEATS_PER_JOB` | `500` | maximale Orte pro Auftrag |
|
||||
| `MAX_LEGAL_FORMS_PER_JOB` | `50` | maximale Rechtsformen pro Auftrag |
|
||||
| `MAX_REQUESTS_PER_JOB` | `5000` | maximales Produkt aus Orten × Rechtsformen |
|
||||
| `MAX_QUEUE_SIZE` | `20` | maximale offene Aufträge |
|
||||
| `DOWNLOAD_RETENTION_HOURS` | `48` | Gültigkeit fertiger Exporte |
|
||||
| `STALE_TASK_RETENTION_HOURS` | `168` | maximale Lebensdauer offener Aufträge |
|
||||
| `REFERENCE_CACHE_SECONDS` | `86400` | Cache für Gemeinden und Rechtsformen |
|
||||
| `TRUSTED_PROXY_IPS` | leer | kommaseparierte IPs eigener Reverse-Proxies |
|
||||
| `EXPORT_BCC_EMAIL` | leer | optionale BCC-Adresse; standardmässig kein BCC |
|
||||
| `APP_ENV` | leer | nur lokal auf `development` setzen; erlaubt die offiziellen Turnstile-Testschlüssel auf Loopback |
|
||||
| `SMTP_HOST` | `mxe98c.netcup.net` | SMTP-Server |
|
||||
| `SMTP_USERNAME` | `info@silias.ch` | SMTP-Benutzer |
|
||||
| `SMTP_SECURE` | `ssl` | SMTP-Verschlüsselung |
|
||||
| `SMTP_PORT` | `465` | SMTP-Port |
|
||||
|
||||
`TRUSTED_PROXY_IPS` darf nur tatsächlich kontrollierte Proxy-Adressen enthalten. Ohne Eintrag werden vom Browser gelieferte `X-Forwarded-For`- oder `CF-Connecting-IP`-Header bewusst ignoriert.
|
||||
|
||||
## Cronjob
|
||||
|
||||
`cronjobs.sh` führt zuerst die automatische Bereinigung und danach den Task-Executor aus. Beide PHP-Skripte akzeptieren ausschliesslich CLI-Aufrufe.
|
||||
|
||||
Beispiel:
|
||||
|
||||
```cron
|
||||
*/2 * * * * /absoluter/pfad/cronjobs.sh
|
||||
```
|
||||
|
||||
Ein manueller Cleanup kann mit `php cleanup.php` gestartet werden. Alle offenen Aufträge werden nur nach ausdrücklicher Bestätigung gelöscht:
|
||||
|
||||
```bash
|
||||
php deleteFiles.php --confirm
|
||||
```
|
||||
|
||||
## Nginx-Härtung
|
||||
|
||||
Die PHP-Skripte sperren Webzugriffe selbst. Zusätzlich sollten Altpfade und Wartungsskripte bereits in Nginx blockiert werden:
|
||||
|
||||
```nginx
|
||||
location ~ ^/(taskExecuter|cleanup|deleteFiles)\.php$ { return 404; }
|
||||
location ^~ /tasks/ { return 404; }
|
||||
location ^~ /download/ { return 404; }
|
||||
location = /env_vars.php { return 404; }
|
||||
```
|
||||
|
||||
Nach dem ersten CLI-Lauf werden alte JSON-Aufträge aus `tasks/` in den privaten Speicher migriert. Anschliessend sollten die alten öffentlichen Verzeichnisse `tasks/` und `download/` nach Kontrolle entfernt werden.
|
||||
|
||||
## Datenschutz
|
||||
|
||||
Die öffentliche Datenschutzerklärung liegt unter `datenschutz.php`. Sie beschreibt Turnstile, die verarbeiteten Daten und die implementierten Löschfristen. Wenn später Analyse-, Marketing- oder weitere Drittanbieter-Dienste ergänzt werden, müssen Text und gegebenenfalls die Einwilligungsverwaltung erneut geprüft werden.
|
||||
|
||||
+1220
File diff suppressed because it is too large
Load Diff
+1015
File diff suppressed because it is too large
Load Diff
@@ -1,69 +0,0 @@
|
||||
<?php
|
||||
include 'zefixAPI.php';
|
||||
|
||||
|
||||
/*
|
||||
* Request Types:
|
||||
* Count
|
||||
* List
|
||||
* Export
|
||||
*/
|
||||
// Request data
|
||||
$data = array();
|
||||
|
||||
$results = array();
|
||||
|
||||
// Wenn Firmenname vorhanden
|
||||
if(isset($_POST['firma']) && !empty($_POST['firma'])) {
|
||||
$data["name"] = $_POST['firma'];
|
||||
}
|
||||
|
||||
// Wenn gelöschte auch gesucht werden sollen
|
||||
if(isset($_POST['geloeschteRechtseinheiten'])) {
|
||||
$data["activeOnly"] = false;
|
||||
} else {
|
||||
$data["activeOnly"] = true;
|
||||
}
|
||||
|
||||
$rechtsformen = array();
|
||||
if (isset($_POST['rechtsform']) && is_array($_POST['rechtsform']) && count($_POST['rechtsform']) > 0) {
|
||||
$rechtsformen = $_POST['rechtsform'];
|
||||
} else {
|
||||
$rechtsformen = array(); //TODO alle Rechtsformen hinzufügen
|
||||
$rechtsformen[] = 2;
|
||||
}
|
||||
|
||||
$sitze = array();
|
||||
if (isset($_POST['sitze']) && is_array($_POST['sitze']) && count($_POST['sitze']) > 0) {
|
||||
$sitze = $_POST['sitze'];
|
||||
} else {
|
||||
$sitze = array(); //TODO alle Sitze hinzufügen
|
||||
$sitze[] = 27;
|
||||
}
|
||||
// Loop through the selected values
|
||||
foreach ($rechtsformen as $rechtsform) {
|
||||
$data["legalFormId"] = $rechtsform;
|
||||
|
||||
// Loop through the selected values
|
||||
foreach ($sitze as $sitz) {
|
||||
$data["legalSeatId"] = $sitz;
|
||||
$response = sendAPICompanySearchRequest($username, $password, $data);
|
||||
|
||||
// echo $response;
|
||||
echo communityCSV($username, $password);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Process the Zefix response and construct your own response
|
||||
// ...
|
||||
$constructed_response = $response;
|
||||
|
||||
// Send the response back to the client
|
||||
//header('Content-Type: application/json');
|
||||
//echo $constructed_response;
|
||||
|
||||
|
||||
|
||||
?>
|
||||
@@ -1,147 +0,0 @@
|
||||
<?php
|
||||
include 'env_vars.php';
|
||||
$username = getenv("username");
|
||||
$password = getenv("password");
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @param string $
|
||||
* @param string $
|
||||
* @param array $data
|
||||
* @return bool|string
|
||||
*/
|
||||
function sendAPICompanySearchRequest(string $username, string $password, array $data): string|bool
|
||||
{
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixPublicREST/api/v1/company/search';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL verification
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
echo 'Curl error: ' . curl_error($ch);
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
|
||||
// Close cURL
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
function sendAPICommunityRequest(string $username, string $password): string|bool
|
||||
{
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixPublicREST/api/v1/community';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL verification
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
echo 'Curl error: ' . curl_error($ch);
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
|
||||
// Close cURL
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
function communityCSV(string $username, string $password): string {
|
||||
$response = sendAPICommunityRequest($username, $password);
|
||||
$communityArray = json_decode($response, true);
|
||||
$csvOutput = 'bfsId,Kanton,Gemeindename,registryOfCommerceId\n';
|
||||
// Create CSV rows
|
||||
|
||||
foreach ($communityArray as $item) {
|
||||
$csvOutput .= implode(',', $item) . "\n";
|
||||
}
|
||||
return $csvOutput;
|
||||
}
|
||||
|
||||
function sendAPILegalFormRequest(string $username, string $password): string|bool
|
||||
{
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixPublicREST/api/v1/legalForm';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); // Disable SSL verification
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
echo 'Curl error: ' . curl_error($ch);
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
|
||||
// Close cURL
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
function legalFormCSV(string $username, string $password): string {
|
||||
$response = sendAPILegalFormRequest($username, $password);
|
||||
$legalformArray = json_decode($response, true);
|
||||
$csvOutput = 'id,name\n';
|
||||
// Create CSV rows
|
||||
|
||||
foreach ($legalformArray as $item) {
|
||||
$csvOutput .= $item["id"].",".$item["name"]["de"] . "\n";
|
||||
}
|
||||
return $csvOutput;
|
||||
}
|
||||
|
||||
?>
|
||||
@@ -0,0 +1,544 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Shared configuration, storage and abuse-protection helpers.
|
||||
*
|
||||
* Secrets are read from process variables or from ../.env. The legacy
|
||||
* env_vars.php file is loaded when present so existing installations keep
|
||||
* working during migration.
|
||||
*/
|
||||
$appFileEnvironment = [];
|
||||
|
||||
function app_load_env_file(string $filename): void
|
||||
{
|
||||
global $appFileEnvironment;
|
||||
|
||||
if (!is_file($filename)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_readable($filename)) {
|
||||
error_log('Environment file is not readable: ' . $filename);
|
||||
return;
|
||||
}
|
||||
|
||||
$size = filesize($filename);
|
||||
if ($size !== false && $size > 65536) {
|
||||
error_log('Environment file is unexpectedly large: ' . $filename);
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = file($filename, FILE_IGNORE_NEW_LINES);
|
||||
if ($lines === false) {
|
||||
error_log('Environment file could not be read: ' . $filename);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($lines as $lineNumber => $line) {
|
||||
if ($lineNumber === 0) {
|
||||
$line = preg_replace('/^\xEF\xBB\xBF/', '', $line) ?? $line;
|
||||
}
|
||||
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strncmp($line, 'export ', 7) === 0) {
|
||||
$line = ltrim(substr($line, 7));
|
||||
}
|
||||
|
||||
$separator = strpos($line, '=');
|
||||
if ($separator === false) {
|
||||
error_log('Ignoring invalid environment entry on line ' . ($lineNumber + 1) . '.');
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = trim(substr($line, 0, $separator));
|
||||
if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name) !== 1) {
|
||||
error_log('Ignoring invalid environment variable name on line ' . ($lineNumber + 1) . '.');
|
||||
continue;
|
||||
}
|
||||
|
||||
// Non-empty process variables and legacy configuration take precedence.
|
||||
$processValue = getenv($name);
|
||||
if ($processValue !== false && trim((string)$processValue) !== '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = trim(substr($line, $separator + 1));
|
||||
$valueLength = strlen($value);
|
||||
if ($valueLength >= 2 && $value[0] === '"' && $value[$valueLength - 1] === '"') {
|
||||
$value = stripcslashes(substr($value, 1, -1));
|
||||
} elseif ($valueLength >= 2 && $value[0] === "'" && $value[$valueLength - 1] === "'") {
|
||||
$value = substr($value, 1, -1);
|
||||
} else {
|
||||
$value = preg_replace('/\s+#.*$/', '', $value) ?? $value;
|
||||
$value = rtrim($value);
|
||||
}
|
||||
|
||||
if (strpos($value, "\0") !== false) {
|
||||
error_log('Ignoring environment value containing a null byte on line ' . ($lineNumber + 1) . '.');
|
||||
continue;
|
||||
}
|
||||
|
||||
$appFileEnvironment[$name] = $value;
|
||||
$_ENV[$name] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$legacyEnvFile = __DIR__ . '/env_vars.php';
|
||||
if (is_file($legacyEnvFile)) {
|
||||
require_once $legacyEnvFile;
|
||||
}
|
||||
|
||||
app_load_env_file(dirname(__DIR__) . DIRECTORY_SEPARATOR . '.env');
|
||||
|
||||
function app_env(string $name, string $default = ''): string
|
||||
{
|
||||
global $appFileEnvironment;
|
||||
|
||||
$value = getenv($name);
|
||||
if ($value !== false && trim((string)$value) !== '') {
|
||||
return trim((string)$value);
|
||||
}
|
||||
|
||||
if (array_key_exists($name, $appFileEnvironment)) {
|
||||
return trim((string)$appFileEnvironment[$name]);
|
||||
}
|
||||
|
||||
return $default;
|
||||
}
|
||||
|
||||
function app_env_int(string $name, int $default, int $min, int $max): int
|
||||
{
|
||||
$value = filter_var(app_env($name), FILTER_VALIDATE_INT);
|
||||
if ($value === false) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
return max($min, min($max, (int)$value));
|
||||
}
|
||||
|
||||
function app_private_dir(): string
|
||||
{
|
||||
$configured = app_env('ZEFIX_PRIVATE_DIR');
|
||||
return $configured !== ''
|
||||
? rtrim($configured, '/\\')
|
||||
: dirname(__DIR__) . DIRECTORY_SEPARATOR . 'zefix-private';
|
||||
}
|
||||
|
||||
function app_ensure_dir(string $directory): void
|
||||
{
|
||||
if (!is_dir($directory) && !mkdir($directory, 0700, true) && !is_dir($directory)) {
|
||||
throw new RuntimeException('Private storage could not be created.');
|
||||
}
|
||||
}
|
||||
|
||||
function app_task_dir(): string
|
||||
{
|
||||
$directory = app_private_dir() . DIRECTORY_SEPARATOR . 'tasks';
|
||||
app_ensure_dir($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
function app_download_dir(): string
|
||||
{
|
||||
$directory = app_private_dir() . DIRECTORY_SEPARATOR . 'downloads';
|
||||
app_ensure_dir($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
function app_rate_limit_dir(): string
|
||||
{
|
||||
$directory = app_private_dir() . DIRECTORY_SEPARATOR . 'rate-limits';
|
||||
app_ensure_dir($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
function app_cache_dir(): string
|
||||
{
|
||||
$directory = app_private_dir() . DIRECTORY_SEPARATOR . 'cache';
|
||||
app_ensure_dir($directory);
|
||||
return $directory;
|
||||
}
|
||||
|
||||
function app_send_security_headers(): void
|
||||
{
|
||||
if (headers_sent()) {
|
||||
return;
|
||||
}
|
||||
|
||||
header("Content-Security-Policy: default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; object-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' https://challenges.cloudflare.com; frame-src https://challenges.cloudflare.com; connect-src 'self' https://challenges.cloudflare.com; upgrade-insecure-requests");
|
||||
header('Referrer-Policy: strict-origin-when-cross-origin');
|
||||
header('X-Content-Type-Options: nosniff');
|
||||
header('X-Frame-Options: DENY');
|
||||
header('Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()');
|
||||
header('Cross-Origin-Opener-Policy: same-origin');
|
||||
}
|
||||
|
||||
function app_turnstile_site_key(): string
|
||||
{
|
||||
return '0x4AAAAAAD7CJrWpNiK0-A7h';
|
||||
}
|
||||
|
||||
function app_turnstile_secret_key(): string
|
||||
{
|
||||
return app_env('TURNSTILE_SECRET');
|
||||
}
|
||||
|
||||
function app_turnstile_is_configured(): bool
|
||||
{
|
||||
return app_turnstile_site_key() !== '' && app_turnstile_secret_key() !== '';
|
||||
}
|
||||
|
||||
function app_security_secret(): string
|
||||
{
|
||||
$secret = app_env('APP_SECRET');
|
||||
if ($secret === '') {
|
||||
$secret = app_turnstile_secret_key();
|
||||
}
|
||||
return $secret;
|
||||
}
|
||||
|
||||
function app_base64url_encode(string $value): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($value), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
function app_base64url_decode(string $value): string|false
|
||||
{
|
||||
$padding = strlen($value) % 4;
|
||||
if ($padding !== 0) {
|
||||
$value .= str_repeat('=', 4 - $padding);
|
||||
}
|
||||
return base64_decode(strtr($value, '-_', '+/'), true);
|
||||
}
|
||||
|
||||
function app_create_form_started_token(?int $timestamp = null): string
|
||||
{
|
||||
$secret = app_security_secret();
|
||||
if ($secret === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$timestamp ??= time();
|
||||
$payload = (string)$timestamp;
|
||||
$signature = hash_hmac('sha256', $payload, $secret, true);
|
||||
return app_base64url_encode($payload . '.' . $signature);
|
||||
}
|
||||
|
||||
function app_validate_form_started_token(string $token): bool
|
||||
{
|
||||
$secret = app_security_secret();
|
||||
$decoded = app_base64url_decode($token);
|
||||
if ($secret === '' || $decoded === false || strlen($decoded) < 34) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$separator = strpos($decoded, '.');
|
||||
if ($separator === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$payload = substr($decoded, 0, $separator);
|
||||
$signature = substr($decoded, $separator + 1);
|
||||
if (!ctype_digit($payload) || strlen($signature) !== 32) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$expected = hash_hmac('sha256', $payload, $secret, true);
|
||||
if (!hash_equals($expected, $signature)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$age = time() - (int)$payload;
|
||||
$minimumSeconds = app_env_int('FORM_MIN_SECONDS', 2, 0, 60);
|
||||
$maximumSeconds = app_env_int('FORM_MAX_SECONDS', 7200, 60, 86400);
|
||||
return $age >= $minimumSeconds && $age <= $maximumSeconds;
|
||||
}
|
||||
|
||||
function app_client_ip(): string
|
||||
{
|
||||
$remoteAddress = $_SERVER['REMOTE_ADDR'] ?? '';
|
||||
if (!filter_var($remoteAddress, FILTER_VALIDATE_IP)) {
|
||||
$remoteAddress = 'unknown';
|
||||
}
|
||||
|
||||
$trustedProxies = array_values(array_filter(array_map(
|
||||
'trim',
|
||||
explode(',', app_env('TRUSTED_PROXY_IPS'))
|
||||
)));
|
||||
|
||||
if (!in_array($remoteAddress, $trustedProxies, true)) {
|
||||
return $remoteAddress;
|
||||
}
|
||||
|
||||
$candidates = [];
|
||||
if (isset($_SERVER['HTTP_CF_CONNECTING_IP'])) {
|
||||
$candidates[] = trim((string)$_SERVER['HTTP_CF_CONNECTING_IP']);
|
||||
}
|
||||
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
|
||||
$forwarded = explode(',', (string)$_SERVER['HTTP_X_FORWARDED_FOR']);
|
||||
$candidates[] = trim($forwarded[0]);
|
||||
}
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
if (filter_var($candidate, FILTER_VALIDATE_IP)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return $remoteAddress;
|
||||
}
|
||||
|
||||
/** @return array{allowed: bool, retry_after: int} */
|
||||
function app_rate_limit(string $scope, string $identity, int $limit, int $windowSeconds): array
|
||||
{
|
||||
$now = time();
|
||||
$secret = app_security_secret();
|
||||
$hash = $secret !== ''
|
||||
? hash_hmac('sha256', strtolower($identity), $secret)
|
||||
: hash('sha256', strtolower($identity));
|
||||
$filename = app_rate_limit_dir() . DIRECTORY_SEPARATOR . preg_replace('/[^a-z0-9_-]/i', '_', $scope) . '-' . $hash . '.json';
|
||||
$handle = fopen($filename, 'c+');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Rate-limit storage could not be opened.');
|
||||
}
|
||||
|
||||
try {
|
||||
if (!flock($handle, LOCK_EX)) {
|
||||
throw new RuntimeException('Rate-limit storage could not be locked.');
|
||||
}
|
||||
|
||||
$contents = stream_get_contents($handle);
|
||||
$state = $contents !== false && $contents !== '' ? json_decode($contents, true) : null;
|
||||
if (!is_array($state) || !isset($state['started'], $state['count']) || ($now - (int)$state['started']) >= $windowSeconds) {
|
||||
$state = ['started' => $now, 'count' => 0];
|
||||
}
|
||||
|
||||
$allowed = (int)$state['count'] < $limit;
|
||||
if ($allowed) {
|
||||
$state['count'] = (int)$state['count'] + 1;
|
||||
}
|
||||
|
||||
rewind($handle);
|
||||
ftruncate($handle, 0);
|
||||
fwrite($handle, json_encode($state, JSON_THROW_ON_ERROR));
|
||||
fflush($handle);
|
||||
@chmod($filename, 0600);
|
||||
flock($handle, LOCK_UN);
|
||||
|
||||
return [
|
||||
'allowed' => $allowed,
|
||||
'retry_after' => max(1, $windowSeconds - ($now - (int)$state['started'])),
|
||||
];
|
||||
} finally {
|
||||
fclose($handle);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return array{success: bool, error: string} */
|
||||
function app_verify_turnstile(string $token, string $remoteIp): array
|
||||
{
|
||||
$secret = app_turnstile_secret_key();
|
||||
if ($secret === '' || $token === '' || strlen($token) > 2048) {
|
||||
return ['success' => false, 'error' => 'missing-input'];
|
||||
}
|
||||
|
||||
$curl = curl_init('https://challenges.cloudflare.com/turnstile/v0/siteverify');
|
||||
if ($curl === false) {
|
||||
return ['success' => false, 'error' => 'internal-error'];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'secret' => $secret,
|
||||
'response' => $token,
|
||||
];
|
||||
if ($remoteIp !== 'unknown') {
|
||||
$payload['remoteip'] = $remoteIp;
|
||||
}
|
||||
|
||||
curl_setopt_array($curl, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query($payload),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_CONNECTTIMEOUT => 5,
|
||||
CURLOPT_TIMEOUT => 10,
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2,
|
||||
CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded'],
|
||||
]);
|
||||
|
||||
$response = curl_exec($curl);
|
||||
$httpCode = (int)curl_getinfo($curl, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($curl);
|
||||
curl_close($curl);
|
||||
|
||||
if (!is_string($response) || $httpCode !== 200) {
|
||||
error_log('Turnstile request failed: HTTP ' . $httpCode . ($curlError !== '' ? ' / ' . $curlError : ''));
|
||||
return ['success' => false, 'error' => 'verification-unavailable'];
|
||||
}
|
||||
|
||||
$result = json_decode($response, true);
|
||||
if (!is_array($result) || ($result['success'] ?? false) !== true) {
|
||||
$errors = is_array($result['error-codes'] ?? null) ? implode(',', $result['error-codes']) : 'invalid-response';
|
||||
error_log('Turnstile validation rejected: ' . $errors);
|
||||
return ['success' => false, 'error' => 'challenge-failed'];
|
||||
}
|
||||
|
||||
$officialTestSecrets = [
|
||||
'1x0000000000000000000000000000000AA',
|
||||
'2x0000000000000000000000000000000AA',
|
||||
'3x0000000000000000000000000000000AA',
|
||||
];
|
||||
$isLocalDevelopmentTest = app_env('APP_ENV') === 'development'
|
||||
&& in_array($remoteIp, ['127.0.0.1', '::1'], true)
|
||||
&& in_array($secret, $officialTestSecrets, true);
|
||||
|
||||
// Cloudflare's dummy testing responses do not always contain the widget's
|
||||
// hostname and action. This exception is restricted to loopback requests,
|
||||
// development mode and Cloudflare's documented test secrets.
|
||||
if ($isLocalDevelopmentTest) {
|
||||
return ['success' => true, 'error' => ''];
|
||||
}
|
||||
|
||||
$allowedHostname = strtolower(app_env('TURNSTILE_ALLOWED_HOSTNAME', 'zefix.silias.ch'));
|
||||
$hostname = strtolower((string)($result['hostname'] ?? ''));
|
||||
if ($allowedHostname !== '' && !hash_equals($allowedHostname, $hostname)) {
|
||||
error_log('Turnstile hostname mismatch.');
|
||||
return ['success' => false, 'error' => 'hostname-mismatch'];
|
||||
}
|
||||
|
||||
if (($result['action'] ?? '') !== 'turnstile-spin-v2') {
|
||||
error_log('Turnstile action mismatch.');
|
||||
return ['success' => false, 'error' => 'action-mismatch'];
|
||||
}
|
||||
|
||||
return ['success' => true, 'error' => ''];
|
||||
}
|
||||
|
||||
/** @return int[]|null */
|
||||
function app_normalize_positive_id_list(mixed $value, int $maximumItems): ?array
|
||||
{
|
||||
if (!is_array($value) || $value === [] || count($value) > $maximumItems) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($value as $item) {
|
||||
if (is_array($item) || !is_scalar($item)) {
|
||||
return null;
|
||||
}
|
||||
$item = (string)$item;
|
||||
if (!ctype_digit($item)) {
|
||||
return null;
|
||||
}
|
||||
$integer = (int)$item;
|
||||
if ($integer <= 0) {
|
||||
return null;
|
||||
}
|
||||
$normalized[$integer] = $integer;
|
||||
}
|
||||
|
||||
return array_values($normalized);
|
||||
}
|
||||
|
||||
function app_public_base_url(): string
|
||||
{
|
||||
return rtrim(app_env('PUBLIC_BASE_URL', 'https://zefix.silias.ch'), '/');
|
||||
}
|
||||
|
||||
function app_download_path(string $token): string
|
||||
{
|
||||
if (!preg_match('/^[a-f0-9]{32}$/', $token)) {
|
||||
throw new InvalidArgumentException('Invalid download token.');
|
||||
}
|
||||
return app_download_dir() . DIRECTORY_SEPARATOR . $token . '.csv';
|
||||
}
|
||||
|
||||
function app_cached_string(string $key, int $ttlSeconds, callable $loader): string
|
||||
{
|
||||
$safeKey = preg_replace('/[^a-z0-9_-]/i', '_', $key);
|
||||
$filename = app_cache_dir() . DIRECTORY_SEPARATOR . $safeKey . '.cache';
|
||||
if (is_file($filename) && filemtime($filename) !== false && filemtime($filename) >= time() - $ttlSeconds) {
|
||||
$cached = file_get_contents($filename);
|
||||
if (is_string($cached) && $cached !== '') {
|
||||
return $cached;
|
||||
}
|
||||
}
|
||||
|
||||
$lock = fopen($filename . '.lock', 'c+');
|
||||
if ($lock === false || !flock($lock, LOCK_EX)) {
|
||||
if (is_resource($lock)) {
|
||||
fclose($lock);
|
||||
}
|
||||
return (string)$loader();
|
||||
}
|
||||
|
||||
try {
|
||||
clearstatcache(true, $filename);
|
||||
if (is_file($filename) && filemtime($filename) !== false && filemtime($filename) >= time() - $ttlSeconds) {
|
||||
return (string)file_get_contents($filename);
|
||||
}
|
||||
|
||||
$fresh = (string)$loader();
|
||||
if ($fresh !== '') {
|
||||
$temporary = $filename . '.' . bin2hex(random_bytes(4)) . '.tmp';
|
||||
file_put_contents($temporary, $fresh, LOCK_EX);
|
||||
@chmod($temporary, 0600);
|
||||
if (!@rename($temporary, $filename)) {
|
||||
// Windows cannot atomically replace an existing file with rename().
|
||||
@unlink($filename);
|
||||
if (!@rename($temporary, $filename)) {
|
||||
@unlink($temporary);
|
||||
}
|
||||
}
|
||||
return $fresh;
|
||||
}
|
||||
|
||||
return is_file($filename) ? (string)file_get_contents($filename) : '';
|
||||
} finally {
|
||||
flock($lock, LOCK_UN);
|
||||
fclose($lock);
|
||||
}
|
||||
}
|
||||
|
||||
function app_cleanup_directory(string $directory, int $maximumAgeSeconds, string $extension): int
|
||||
{
|
||||
if (!is_dir($directory)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
$cutoff = time() - $maximumAgeSeconds;
|
||||
foreach (glob($directory . DIRECTORY_SEPARATOR . '*.' . $extension) ?: [] as $filename) {
|
||||
$modified = filemtime($filename);
|
||||
if ($modified !== false && $modified < $cutoff && is_file($filename) && unlink($filename)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
return $deleted;
|
||||
}
|
||||
|
||||
function app_migrate_legacy_tasks(): int
|
||||
{
|
||||
$legacyDirectory = __DIR__ . DIRECTORY_SEPARATOR . 'tasks';
|
||||
if (!is_dir($legacyDirectory)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$migrated = 0;
|
||||
foreach (glob($legacyDirectory . DIRECTORY_SEPARATOR . '*.json') ?: [] as $source) {
|
||||
$target = app_task_dir() . DIRECTORY_SEPARATOR . basename($source);
|
||||
if (is_file($target)) {
|
||||
continue;
|
||||
}
|
||||
if (@rename($source, $target) || (@copy($source, $target) && @unlink($source))) {
|
||||
@chmod($target, 0600);
|
||||
$migrated++;
|
||||
}
|
||||
}
|
||||
return $migrated;
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
|
||||
$downloadRetention = app_env_int('DOWNLOAD_RETENTION_HOURS', 48, 1, 720) * 3600;
|
||||
$staleTaskRetention = app_env_int('STALE_TASK_RETENTION_HOURS', 168, 24, 2160) * 3600;
|
||||
|
||||
$migrated = app_migrate_legacy_tasks();
|
||||
$downloads = app_cleanup_directory(app_download_dir(), $downloadRetention, 'csv');
|
||||
$legacyDownloads = app_cleanup_directory(__DIR__ . DIRECTORY_SEPARATOR . 'download', $downloadRetention, 'csv');
|
||||
$tasks = app_cleanup_directory(app_task_dir(), $staleTaskRetention, 'json');
|
||||
$rateLimits = app_cleanup_directory(app_rate_limit_dir(), 8 * 86400, 'json');
|
||||
|
||||
fwrite(STDOUT, sprintf(
|
||||
"Cleanup complete: %d tasks migrated; %d downloads, %d legacy downloads, %d stale tasks and %d rate-limit files deleted.\n",
|
||||
$migrated,
|
||||
$downloads,
|
||||
$legacyDownloads,
|
||||
$tasks,
|
||||
$rateLimits
|
||||
));
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
#!/bin/sh
|
||||
|
||||
BASE_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PHP_BIN="php"
|
||||
LOG_FILE="$BASE_DIR/cron.log"
|
||||
|
||||
echo "=========================================" >> "$LOG_FILE"
|
||||
echo "CRON START $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
|
||||
echo "BASE_DIR: $BASE_DIR" >> "$LOG_FILE"
|
||||
|
||||
cd "$BASE_DIR" || { echo "cd failed: $BASE_DIR" >> "$LOG_FILE"; exit 1; }
|
||||
|
||||
start_ts=$(date +%s)
|
||||
|
||||
$PHP_BIN "$BASE_DIR/cleanup.php" >> "$LOG_FILE" 2>&1
|
||||
$PHP_BIN "$BASE_DIR/taskExecuter.php" >> "$LOG_FILE" 2>&1
|
||||
status=$?
|
||||
|
||||
end_ts=$(date +%s)
|
||||
duration=$((end_ts - start_ts))
|
||||
|
||||
echo "Exit code: $status" >> "$LOG_FILE"
|
||||
echo "Duration: ${duration}s" >> "$LOG_FILE"
|
||||
echo "CRON END $(date '+%Y-%m-%d %H:%M:%S')" >> "$LOG_FILE"
|
||||
echo "" >> "$LOG_FILE"
|
||||
|
||||
exit $status
|
||||
+298
-67
@@ -1,73 +1,304 @@
|
||||
.activeWatch {
|
||||
background-color: lightgray;
|
||||
}
|
||||
/* css/custom.css */
|
||||
/* Keep Bootstrap in control. Only add gentle, modern enhancements. */
|
||||
|
||||
h2 {
|
||||
margin-left: 10px;
|
||||
margin-top: 25px !important;
|
||||
}
|
||||
|
||||
.table-bordered {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
text-align: center;
|
||||
/*border: 1px solid #0e0017 !important;*/
|
||||
|
||||
}
|
||||
|
||||
td {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
button {
|
||||
/*background-color: lightgray;*/
|
||||
/*border: none;*/
|
||||
/*color: white;*/
|
||||
padding: 1% 5%;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
/*font-size: 16px;*/
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
button, .form-group{
|
||||
margin: 5px !important;
|
||||
}
|
||||
|
||||
input {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stopButton, .resetButton {
|
||||
width: 98% !important;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
width: 98%;
|
||||
}
|
||||
|
||||
.tableHeader {
|
||||
min-width: 100px;
|
||||
:root {
|
||||
--silias-radius: 14px;
|
||||
--silias-ink: #0b1236;
|
||||
--silias-muted: #667085;
|
||||
--silias-primary: #3b4cca;
|
||||
--silias-primary-dark: #25339b;
|
||||
--silias-surface: #ffffff;
|
||||
--silias-soft: #f3f5ff;
|
||||
--silias-border: #e4e8f2;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
|
||||
/* Equal to height of footer */
|
||||
/* But also accounting for potential margin-bottom of last child */
|
||||
margin-bottom: -240px;
|
||||
}
|
||||
footer,
|
||||
.push {
|
||||
height: 300px;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
vertical-align: top;
|
||||
text-align: left;
|
||||
}
|
||||
body {
|
||||
/* Bootstrap uses system font stack, keep it */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: var(--silias-ink);
|
||||
background:
|
||||
radial-gradient(circle at 8% 0%, rgba(90, 103, 216, .12), transparent 28rem),
|
||||
#f7f8fc !important;
|
||||
background-repeat: no-repeat !important;
|
||||
background-size: 100% 720px !important;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
.site-header {
|
||||
background: rgba(255, 255, 255, .94);
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
|
||||
.brand-lockup {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: .85rem;
|
||||
color: var(--silias-ink);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.brand-logo-crop {
|
||||
position: relative;
|
||||
width: 126px;
|
||||
height: 45px;
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.brand-logo-crop img {
|
||||
position: absolute;
|
||||
left: -2%;
|
||||
top: 50%;
|
||||
width: 104%;
|
||||
height: auto;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.brand-divider {
|
||||
width: 1px;
|
||||
height: 28px;
|
||||
background: var(--silias-border);
|
||||
}
|
||||
|
||||
.brand-product {
|
||||
font-size: .9rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.brand-product small {
|
||||
display: block;
|
||||
margin-top: .18rem;
|
||||
color: var(--silias-muted);
|
||||
font-size: .72rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hero-eyebrow {
|
||||
color: var(--silias-primary);
|
||||
font-size: .78rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: .09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
max-width: 760px;
|
||||
letter-spacing: -.025em;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
max-width: 720px;
|
||||
color: var(--silias-muted);
|
||||
}
|
||||
|
||||
/* Card polish */
|
||||
.card {
|
||||
border-radius: var(--silias-radius);
|
||||
}
|
||||
.card.shadow-sm {
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,.06) !important;
|
||||
}
|
||||
|
||||
.export-shell {
|
||||
border: 1px solid rgba(228, 232, 242, .9);
|
||||
box-shadow: 0 18px 48px rgba(21, 31, 80, .09) !important;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 1.25rem;
|
||||
border: 1px solid var(--silias-border);
|
||||
border-radius: 1rem;
|
||||
background: var(--silias-surface);
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .8rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.section-number {
|
||||
display: inline-grid;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
place-items: center;
|
||||
flex: 0 0 auto;
|
||||
border-radius: .65rem;
|
||||
background: var(--silias-soft);
|
||||
color: var(--silias-primary-dark);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
margin: .2rem 0 0;
|
||||
color: var(--silias-muted);
|
||||
font-size: .9rem;
|
||||
}
|
||||
|
||||
.selection-meta {
|
||||
color: var(--silias-muted);
|
||||
font-size: .84rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.privacy-note {
|
||||
padding: .9rem 1rem;
|
||||
border: 1px solid var(--silias-border);
|
||||
border-radius: .85rem;
|
||||
background: #fafbff;
|
||||
}
|
||||
|
||||
/* Form */
|
||||
.form-label {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.form-text {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.form-control,
|
||||
.form-select {
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
|
||||
.form-control:focus,
|
||||
.form-select:focus {
|
||||
border-color: #7c89e8;
|
||||
box-shadow: 0 0 0 .2rem rgba(59, 76, 202, .14);
|
||||
}
|
||||
|
||||
/* Buttons: DO NOT globally style "button" tag */
|
||||
.btn {
|
||||
border-radius: 0.85rem;
|
||||
}
|
||||
.btn-lg {
|
||||
padding-top: .85rem;
|
||||
padding-bottom: .85rem;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
border-color: var(--silias-primary);
|
||||
background: var(--silias-primary);
|
||||
}
|
||||
|
||||
.btn-primary:hover,
|
||||
.btn-primary:focus {
|
||||
border-color: var(--silias-primary-dark);
|
||||
background: var(--silias-primary-dark);
|
||||
}
|
||||
|
||||
/* Scroll panels (if you keep class from index.php) */
|
||||
.scrollWindow {
|
||||
border-radius: 1rem;
|
||||
scrollbar-color: #b8bfdc transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* Checkbox rows */
|
||||
.form-check {
|
||||
padding: .3rem 0 .3rem 1.75rem;
|
||||
}
|
||||
.form-check-input {
|
||||
cursor: pointer;
|
||||
margin-left: -1.75rem;
|
||||
}
|
||||
.form-check-label {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Alerts */
|
||||
.alert {
|
||||
border-radius: 0.9rem;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
footer {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.site-footer {
|
||||
color: var(--silias-muted);
|
||||
background: #fff;
|
||||
}
|
||||
footer a {
|
||||
text-decoration: none;
|
||||
}
|
||||
footer a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* Small badges look nicer */
|
||||
.badge {
|
||||
border-radius: 999px;
|
||||
padding: .45rem .7rem;
|
||||
}
|
||||
|
||||
/* Optional: make long checkbox lists easier to scan */
|
||||
.scrollWindow .form-check + .form-check {
|
||||
border-top: 1px solid rgba(0,0,0,.04);
|
||||
}
|
||||
.scrollWindow .form-check {
|
||||
padding: .35rem .25rem;
|
||||
}
|
||||
|
||||
/* Ensure links inside muted text are readable */
|
||||
.text-muted a {
|
||||
color: inherit;
|
||||
}
|
||||
.text-muted a:hover {
|
||||
color: #0d6efd;
|
||||
}
|
||||
|
||||
/* Mobile tweaks */
|
||||
@media (max-width: 576px) {
|
||||
.card-body {
|
||||
padding: 1.25rem !important;
|
||||
}
|
||||
|
||||
.brand-logo-crop {
|
||||
width: 104px;
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.brand-divider,
|
||||
.brand-product small {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.form-section {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
/* Fix: checkboxes not clipped and never overlap text */
|
||||
.scrollWindow .form-check {
|
||||
padding: .6rem 1.1rem !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.scrollWindow .form-check-input {
|
||||
margin-left: 0 !important; /* remove negative margin */
|
||||
float: left; /* classic layout */
|
||||
}
|
||||
|
||||
.scrollWindow .form-check-label {
|
||||
display: block;
|
||||
padding-left: 1.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
app_send_security_headers();
|
||||
$downloadRetentionHours = app_env_int('DOWNLOAD_RETENTION_HOURS', 48, 1, 720);
|
||||
$staleTaskRetentionHours = app_env_int('STALE_TASK_RETENTION_HOURS', 168, 24, 2160);
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/custom.css?v=2.2">
|
||||
<title>Datenschutzerklärung – Silias Zefix Export</title>
|
||||
</head>
|
||||
<body class="bg-light">
|
||||
<nav class="navbar navbar-light site-header border-bottom py-2">
|
||||
<div class="container">
|
||||
<a class="brand-lockup" href="index.php" aria-label="Silias Zefix Export – Startseite">
|
||||
<span class="brand-logo-crop" aria-hidden="true"><img src="img/silias-logo.png" alt=""></span>
|
||||
<span class="brand-divider" aria-hidden="true"></span>
|
||||
<span class="brand-product">Zefix Export<small>Ein kostenloses Silias-Tool</small></span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4 py-md-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-9">
|
||||
<article class="card shadow-sm">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
<h1 class="h3 mb-4">Datenschutzerklärung</h1>
|
||||
<p class="text-muted">Stand: 22. Juli 2026</p>
|
||||
|
||||
<h2 class="h5 mt-4">1. Verantwortlicher</h2>
|
||||
<p>
|
||||
Silias KLG<br>
|
||||
Toggenburgstrasse 31<br>
|
||||
8245 Feuerthalen, Schweiz<br>
|
||||
E-Mail: <a href="mailto:info@silias.ch">info@silias.ch</a>
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">2. Zweck und Umfang der Datenbearbeitung</h2>
|
||||
<p>
|
||||
Der Dienst erstellt auf Ihren Wunsch einen Export aus dem Zentralen Firmenindex ZEFIX und sendet
|
||||
den zugehörigen Download-Link an die von Ihnen angegebene E-Mail-Adresse. Dafür bearbeiten wir die
|
||||
E-Mail-Adresse, die ausgewählten Suchparameter, den Zeitpunkt des Auftrags sowie technisch notwendige
|
||||
Sicherheits- und Protokolldaten.
|
||||
</p>
|
||||
<p>
|
||||
Die Bearbeitung dient der Bereitstellung des angeforderten Exports, der Betriebssicherheit sowie der
|
||||
Erkennung und Verhinderung automatisierter oder missbräuchlicher Anfragen. Soweit die DSGVO anwendbar
|
||||
ist, stützen wir diese Bearbeitung auf die Durchführung Ihrer Anfrage sowie unser berechtigtes Interesse
|
||||
an einem sicheren und zuverlässigen Betrieb.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">3. Cloudflare Turnstile</h2>
|
||||
<p>
|
||||
Zum Schutz des Exportformulars verwenden wir Cloudflare Turnstile im verwalteten Modus. Dabei werden
|
||||
technisch notwendige Signale wie IP-Adresse, TLS-Fingerprint, User-Agent, Website-Herkunft und
|
||||
Sicherheitsmerkmale des Browsers an Cloudflare übermittelt. Die Daten werden zur Unterscheidung zwischen
|
||||
Menschen und automatisierten Zugriffen verwendet. Wir verwenden Turnstile nicht zu Werbe- oder
|
||||
Marketingzwecken und haben die Pre-Clearance-Funktion deaktiviert.
|
||||
</p>
|
||||
<p>
|
||||
Anbieter ist Cloudflare, Inc., 101 Townsend Street, San Francisco, CA 94107, USA. Cloudflare kann Daten
|
||||
auch ausserhalb der Schweiz bearbeiten. Informationen zu Zweck, Rollenverteilung und internationalen
|
||||
Übermittlungen finden Sie im
|
||||
<a href="https://www.cloudflare.com/turnstile-privacy-policy/" target="_blank" rel="noopener">Turnstile Privacy Addendum</a>,
|
||||
in der <a href="https://www.cloudflare.com/policies/privacy/" target="_blank" rel="noopener">Datenschutzerklärung von Cloudflare</a>
|
||||
und im <a href="https://www.cloudflare.com/cloudflare-customer-dpa/" target="_blank" rel="noopener">Data Processing Addendum</a>.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">4. Cookies und ähnliche Technologien</h2>
|
||||
<p>
|
||||
Diese Website verwendet keine Analyse- oder Marketing-Cookies. Turnstile kann technisch notwendige
|
||||
Cookies oder lokalen Browserspeicher zur Sicherheitsprüfung einsetzen. Diese Sicherheitsfunktionen sind
|
||||
für die Nutzung des öffentlich zugänglichen Exportformulars erforderlich. Die Cloudflare-Funktion
|
||||
«Pre-Clearance», die ein <code>cf_clearance</code>-Cookie auf unserer Domain setzen würde, ist deaktiviert.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">5. Empfänger und Datenquellen</h2>
|
||||
<p>
|
||||
Daten erhalten nur Dienstleister, soweit dies für Hosting, E-Mail-Versand, Missbrauchsschutz oder Betrieb
|
||||
erforderlich ist. Die ausgewählten Suchkriterien werden serverseitig an die ZEFIX-Schnittstelle des
|
||||
Bundesamts für Justiz übermittelt. Ihre E-Mail-Adresse wird nicht an ZEFIX übermittelt. Eine Weitergabe
|
||||
zu Werbezwecken findet nicht statt.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">6. Aufbewahrungsdauer</h2>
|
||||
<ul>
|
||||
<li>Offene Aufträge einschliesslich E-Mail-Adresse: bis zur Verarbeitung, spätestens <?php echo $staleTaskRetentionHours; ?> Stunden.</li>
|
||||
<li>Fertige Exportdateien: <?php echo $downloadRetentionHours; ?> Stunden nach ihrer Erstellung.</li>
|
||||
<li>Pseudonymisierte Rate-Limit-Einträge: höchstens acht Tage.</li>
|
||||
<li>Technische Serverprotokolle: nur so lange, wie dies für Sicherheit, Fehleranalyse und Betrieb erforderlich ist.</li>
|
||||
</ul>
|
||||
<p>Nach Ablauf der jeweiligen Frist werden die Daten automatisch gelöscht oder überschrieben.</p>
|
||||
|
||||
<h2 class="h5 mt-4">7. Datensicherheit</h2>
|
||||
<p>
|
||||
Die Übertragung erfolgt verschlüsselt über HTTPS. Auftragsdaten und Exportdateien werden ausserhalb des
|
||||
öffentlichen Webverzeichnisses gespeichert. Download-Links enthalten ein zufälliges Zugriffstoken und
|
||||
sind zeitlich begrenzt. Bitte leiten Sie einen Download-Link nicht an unbefugte Personen weiter.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">8. Ihre Rechte</h2>
|
||||
<p>
|
||||
Im Rahmen des anwendbaren Datenschutzrechts können Sie Auskunft, Berichtigung, Löschung oder
|
||||
Einschränkung der Bearbeitung verlangen sowie einer Bearbeitung widersprechen. Zur Ausübung Ihrer Rechte
|
||||
kontaktieren Sie uns unter <a href="mailto:info@silias.ch">info@silias.ch</a>. Sie können sich ausserdem
|
||||
an den Eidgenössischen Datenschutz- und Öffentlichkeitsbeauftragten oder eine andere zuständige
|
||||
Datenschutzaufsichtsbehörde wenden.
|
||||
</p>
|
||||
|
||||
<h2 class="h5 mt-4">9. Änderungen</h2>
|
||||
<p>
|
||||
Wir können diese Datenschutzerklärung anpassen, wenn sich der Dienst oder die rechtlichen Anforderungen
|
||||
ändern. Es gilt die jeweils auf dieser Seite veröffentlichte Fassung.
|
||||
</p>
|
||||
|
||||
<a class="btn btn-primary mt-3" href="index.php">Zurück zur Startseite</a>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer border-top">
|
||||
<div class="container py-3 text-center text-muted small">
|
||||
© <?php echo date('Y'); ?> Silias KLG
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
|
||||
if (($argv[1] ?? '') !== '--confirm') {
|
||||
fwrite(STDERR, "This command deletes every pending export task. Run: php deleteFiles.php --confirm\n");
|
||||
exit(2);
|
||||
}
|
||||
|
||||
$deleted = 0;
|
||||
foreach (glob(app_task_dir() . DIRECTORY_SEPARATOR . '*.json') ?: [] as $filename) {
|
||||
if (is_file($filename) && unlink($filename)) {
|
||||
$deleted++;
|
||||
}
|
||||
}
|
||||
|
||||
fwrite(STDOUT, $deleted . " pending task(s) deleted.\n");
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
app_send_security_headers();
|
||||
header('Cache-Control: private, no-store, max-age=0');
|
||||
header('Pragma: no-cache');
|
||||
header('X-Robots-Tag: noindex, nofollow, noarchive');
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET') {
|
||||
header('Allow: GET');
|
||||
http_response_code(405);
|
||||
exit('Methode nicht erlaubt.');
|
||||
}
|
||||
|
||||
$tokenValue = $_GET['token'] ?? '';
|
||||
$token = is_scalar($tokenValue) ? strtolower(trim((string)$tokenValue)) : '';
|
||||
if (!preg_match('/^[a-f0-9]{32}$/', $token)) {
|
||||
http_response_code(404);
|
||||
exit('Download nicht gefunden.');
|
||||
}
|
||||
|
||||
$filename = app_download_path($token);
|
||||
$retentionSeconds = app_env_int('DOWNLOAD_RETENTION_HOURS', 48, 1, 720) * 3600;
|
||||
$modified = is_file($filename) ? filemtime($filename) : false;
|
||||
if ($modified === false || $modified < time() - $retentionSeconds) {
|
||||
if (is_file($filename)) {
|
||||
@unlink($filename);
|
||||
}
|
||||
http_response_code(404);
|
||||
exit('Der Download ist nicht vorhanden oder bereits abgelaufen.');
|
||||
}
|
||||
|
||||
$size = filesize($filename);
|
||||
header('Content-Type: text/csv; charset=UTF-8');
|
||||
header('Content-Disposition: attachment; filename="zefix-export.csv"');
|
||||
if ($size !== false) {
|
||||
header('Content-Length: ' . $size);
|
||||
}
|
||||
|
||||
$handle = fopen($filename, 'rb');
|
||||
if ($handle === false) {
|
||||
http_response_code(500);
|
||||
exit('Der Download konnte nicht geöffnet werden.');
|
||||
}
|
||||
fpassthru($handle);
|
||||
fclose($handle);
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
require_once "PHPMailer.php";
|
||||
require_once "SMTP.php";
|
||||
require_once "Exception.php";
|
||||
|
||||
use PHPMailer\PHPMailer\PHPMailer;
|
||||
|
||||
|
||||
function sendEmail(string $emailAddress, string $downloadToken, string $smtpPassword): bool
|
||||
{
|
||||
$mail = new PHPMailer();
|
||||
|
||||
$mail->isSMTP();
|
||||
$mail->Host = app_env('SMTP_HOST', 'mxe98c.netcup.net');
|
||||
$mail->SMTPAuth = true;
|
||||
$mail->Username = app_env('SMTP_USERNAME', 'info@silias.ch');
|
||||
$mail->Password = $smtpPassword;
|
||||
$mail->SMTPSecure = app_env('SMTP_SECURE', 'ssl');
|
||||
$mail->Port = app_env_int('SMTP_PORT', 465, 1, 65535);
|
||||
$mail->CharSet = 'UTF-8';
|
||||
|
||||
$mail->isHTML(true);
|
||||
$mail->setFrom('info@silias.ch', 'Silias Zefix Export');
|
||||
$mail->addAddress($emailAddress);
|
||||
$bccAddress = app_env('EXPORT_BCC_EMAIL');
|
||||
if ($bccAddress !== '' && filter_var($bccAddress, FILTER_VALIDATE_EMAIL)) {
|
||||
$mail->addBCC($bccAddress);
|
||||
}
|
||||
$mail->addReplyTo('info@silias.ch', 'Silias KLG');
|
||||
$mail->Subject = 'Ihr Export von Zefix ist bereit';
|
||||
$downloadUrl = app_public_base_url() . '/download.php?token=' . rawurlencode($downloadToken);
|
||||
$escapedUrl = htmlspecialchars($downloadUrl, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
$retentionHours = app_env_int('DOWNLOAD_RETENTION_HOURS', 48, 1, 720);
|
||||
$mail->Body = 'Nutzen Sie den folgenden Link, um Ihre Daten herunterzuladen:<br><a href="' . $escapedUrl . '">' . $escapedUrl . '</a><br><br>Der Link ist ' . $retentionHours . ' Stunden gültig.';
|
||||
$mail->AltBody = "Nutzen Sie den folgenden Link, um Ihre Daten herunterzuladen:\n" . $downloadUrl . "\n\nDer Link ist " . $retentionHours . ' Stunden gültig.';
|
||||
|
||||
if ($smtpPassword === '') {
|
||||
error_log('SMTP password is not configured.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!$mail->send()) {
|
||||
error_log('Export email failed: ' . $mail->ErrorInfo);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
-3487
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -1,4 +1,13 @@
|
||||
|
||||
<?php
|
||||
echo "communityCsvData = `". communityCSV($username, $password) ."`;";
|
||||
?>
|
||||
$communityCsv = '';
|
||||
try {
|
||||
$communityCsv = app_cached_string(
|
||||
'communities',
|
||||
app_env_int('REFERENCE_CACHE_SECONDS', 86400, 300, 604800),
|
||||
static fn(): string => communityCSV((string)$username, (string)$password)
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Community reference data could not be loaded: ' . $exception->getMessage());
|
||||
}
|
||||
echo 'communityCsvData = ' . json_encode($communityCsv, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';';
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 40 KiB |
@@ -1,322 +1,557 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/app.php';
|
||||
app_send_security_headers();
|
||||
include "zefixAPI.php";
|
||||
$turnstileSiteKey = app_turnstile_is_configured() ? app_turnstile_site_key() : '';
|
||||
$formStartedToken = app_create_form_started_token();
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/custom.css?v=1.2">
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<title>Silias Zefix Suche</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
|
||||
<style>
|
||||
#sitzInput {
|
||||
box-sizing: border-box;
|
||||
background-position: 14px 12px;
|
||||
background-repeat: no-repeat;
|
||||
font-size: 16px;
|
||||
padding: 14px 20px 12px 45px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/custom.css?v=2.2">
|
||||
|
||||
#sitzInput {outline: 3px solid #ddd;}
|
||||
<?php if ($turnstileSiteKey !== ''): ?>
|
||||
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
|
||||
<?php endif; ?>
|
||||
|
||||
.button-small {
|
||||
font-size: 0.7rem;
|
||||
padding: 0% 1%;
|
||||
}
|
||||
<title>Silias Zefix Export</title>
|
||||
|
||||
.dropdown a:hover {background-color: #ddd;}
|
||||
<style>
|
||||
/* Nice input for the search box */
|
||||
#sitzInput {
|
||||
box-sizing: border-box;
|
||||
font-size: 16px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: .5rem;
|
||||
padding: .6rem .8rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.show {display: block;}
|
||||
.scrollWindow {
|
||||
max-height: 320px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: .75rem;
|
||||
background: #fff;
|
||||
padding: .75rem;
|
||||
}
|
||||
|
||||
.scrollWindow {
|
||||
max-height: 300px; /* Adjust the value as needed */
|
||||
overflow-y: auto; /* This will add a scrollbar when content exceeds max-height */
|
||||
border: 1px solid #ccc; /* Optional: Add a border for visual clarity */
|
||||
padding: 10px; /* Optional: Add padding for better spacing */
|
||||
}
|
||||
</style>
|
||||
.button-small {
|
||||
font-size: 0.8rem;
|
||||
padding: .35rem .6rem;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
||||
.gemeindeeintrag { margin-bottom: .25rem; }
|
||||
|
||||
.bot-trap {
|
||||
position: absolute !important;
|
||||
left: -10000px !important;
|
||||
width: 1px !important;
|
||||
height: 1px !important;
|
||||
overflow: hidden !important;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<body class="bg-light">
|
||||
|
||||
<div class="wrapper">
|
||||
<section>
|
||||
<nav class="navbar navbar-light site-header border-bottom py-2">
|
||||
<div class="container">
|
||||
<a class="brand-lockup" href="index.php" aria-label="Silias Zefix Export – Startseite">
|
||||
<span class="brand-logo-crop" aria-hidden="true">
|
||||
<img src="img/silias-logo.png" alt="">
|
||||
</span>
|
||||
<span class="brand-divider" aria-hidden="true"></span>
|
||||
<span class="brand-product">Zefix Export<small>Ein kostenloses Silias-Tool</small></span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4 py-md-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-lg-10 col-xl-9">
|
||||
|
||||
<div class="container">
|
||||
<h1>Silias Zefix Suche</h1>
|
||||
<div class="mb-4 mb-md-5">
|
||||
<div class="hero-eyebrow mb-2">Zentraler Firmenindex</div>
|
||||
<h1 class="display-6 fw-bold hero-title mb-3">Firmendaten gezielt auswählen und als CSV exportieren</h1>
|
||||
<p class="lead hero-copy mb-0">
|
||||
Filtern Sie Unternehmen nach Firmenname, Ort und Rechtsform. Sobald der Export bereit ist,
|
||||
erhalten Sie einen sicheren Download-Link per E-Mail.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="card export-shell shadow-sm">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<p>Diese Webseite wird gerade neu entwickelt und steht demnächst zur Verfügung.</p>
|
||||
<form action="submit.php" method="post" class="needs-validation" novalidate>
|
||||
|
||||
<form action="submit.php" method="post">
|
||||
<label for="firma">Firmenname:</label>
|
||||
<p>* kann als Platzhalter verwendet werden</p>
|
||||
<input type="text" id="firma" name="firma"><br><br>
|
||||
|
||||
<label for="kanton">Kanton:</label>
|
||||
<div id="kantonauswahl" class="scrollWindow" onclick="filterFunction()">
|
||||
|
||||
</div><br><br>
|
||||
|
||||
<label for="sitz">Sitz (Ort):</label><br>
|
||||
<div class="dropdown">
|
||||
<div id="gemeindeDropdown" class="dropdown-content">
|
||||
<table style="width: 100%">
|
||||
<td>
|
||||
<input type="text" placeholder="suchen (Ort)" id="sitzInput" onkeyup="filterFunction()" onchange="filterFunction()" name="sitzfilter" autocomplete="off">
|
||||
</td>
|
||||
<td>
|
||||
<button type="button" class="button-small" onclick="gefundeneGemeindenAuswählen()">Alle Ergebnise auswählen</button>
|
||||
<button type="button" class="button-small" onclick="ausgewählteGemeindenLöschen()">Auswahl löschen</button>
|
||||
<button type="button" class="button-small" onclick="alleGemeindenAnzeigen()">Alle anzeigen</button>
|
||||
<button type="button" class="button-small" onclick="nurAusgewählteGemeindenAnzeigen()">Nur ausgewählte anzeigen</button>
|
||||
</td>
|
||||
</table>
|
||||
|
||||
<script>
|
||||
|
||||
function alleGemeindenAnzeigen() {
|
||||
sitzInput.value = ""
|
||||
filterFunction()
|
||||
}
|
||||
|
||||
function nurAusgewählteGemeindenAnzeigen() {
|
||||
sitzInput.value = "(nur ausgewählte)"
|
||||
filterFunction()
|
||||
}
|
||||
|
||||
function ausgewählteGemeindenLöschen() {
|
||||
sitzauswahl.querySelectorAll('input[type="checkbox"]').forEach(function(checkbox) {
|
||||
checkbox.checked = false;
|
||||
})
|
||||
filterFunction()
|
||||
}
|
||||
|
||||
function gefundeneGemeindenAuswählen() { // TODO diese Funktion aufrufen wenn keine Gemeinde ausgewählt und gesucht wird.
|
||||
sitzauswahl.querySelectorAll('input[type="checkbox"]').forEach(function(checkbox) {
|
||||
if(checkbox.parentNode.parentNode.style.display !== "none")
|
||||
checkbox.checked = true;
|
||||
})
|
||||
filterFunction()
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
<div id="sitzauswahl" class="scrollWindow">
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div><br><br><br><br>
|
||||
|
||||
|
||||
|
||||
<label for="rechtsform">Rechtsform:</label><br>
|
||||
<div id = "rechtsformenauswahl" class="scrollWindow">
|
||||
<input type="hidden" name="form_started" value="<?php echo htmlspecialchars($formStartedToken, ENT_QUOTES); ?>">
|
||||
<div class="bot-trap" aria-hidden="true" hidden>
|
||||
<label for="website">Website</label>
|
||||
<input type="text" id="website" name="website" value="" tabindex="-1" autocomplete="off">
|
||||
</div>
|
||||
|
||||
<div class="form-section mb-3">
|
||||
<div class="section-heading">
|
||||
<span class="section-number" aria-hidden="true">1</span>
|
||||
<div>
|
||||
<h2>Firmenname</h2>
|
||||
<p>Optional: Schränken Sie den Export auf passende Firmennamen ein.</p>
|
||||
</div>
|
||||
<br><br>
|
||||
</div>
|
||||
<label for="firma" class="form-label">Firmenname oder Suchmuster</label>
|
||||
<input type="text" id="firma" name="firma" class="form-control" placeholder="Zum Beispiel *solar*" maxlength="200">
|
||||
<div class="form-text mt-2">Das Sternchen (*) kann als Platzhalter verwendet werden.</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="exakteSuche" name="exakteSuche" disabled>
|
||||
<label for="exakteSuche">Exakte Suche (noch in Entwicklung)</label><br>
|
||||
<div class="form-section mb-3">
|
||||
<div class="section-heading">
|
||||
<span class="section-number" aria-hidden="true">2</span>
|
||||
<div>
|
||||
<h2>Ort auswählen</h2>
|
||||
<p>Filtern Sie zuerst nach Kanton oder Ortsname und übernehmen Sie danach die gewünschten Orte.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="geloeschteRechtseinheiten" name="geloeschteRechtseinheiten">
|
||||
<label for="geloeschteRechtseinheiten">Gelöschte Rechtseinheiten suchen</label><br>
|
||||
<div class="alert alert-primary border-0 py-2 mb-3" role="note">
|
||||
<strong>Hinweis:</strong> Die Kantone filtern nur die Ortsliste. Klicken Sie danach auf
|
||||
<strong>«Gefilterte Orte auswählen»</strong>, damit die Orte in den Export übernommen werden.
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="inBisherigenFirmen" name="inBisherigenFirmen" disabled>
|
||||
<label for="inBisherigenFirmen">In bisherigen Firmen/Namen suchen (noch in Entwicklung)</label><br>
|
||||
<div class="mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label class="form-label mb-0">Kanton</label>
|
||||
<span class="selection-meta" id="cantonCount">0 ausgewählt</span>
|
||||
</div>
|
||||
<div id="kantonauswahl" class="scrollWindow" onclick="filterFunction()"></div>
|
||||
</div>
|
||||
|
||||
<input type="checkbox" id="phonetischeSuche" name="phonetischeSuche" disabled>
|
||||
<label for="phonetischeSuche">Phonetische Suche (noch in Entwicklung)</label><br><br>
|
||||
<div>
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<label for="sitzInput" class="form-label mb-0">Sitz (Ort)</label>
|
||||
<span class="selection-meta" id="seatCount">0 ausgewählt</span>
|
||||
</div>
|
||||
|
||||
<input type="submit" value="Suchergebnisse exportieren">
|
||||
</form>
|
||||
<div class="card border-0 bg-white">
|
||||
<div class="card-body p-0">
|
||||
<div class="d-flex flex-column flex-lg-row gap-2 mb-2">
|
||||
<div class="flex-grow-1">
|
||||
<input type="text"
|
||||
placeholder="Ort suchen"
|
||||
id="sitzInput"
|
||||
onkeyup="filterFunction()"
|
||||
onchange="filterFunction()"
|
||||
name="sitzfilter"
|
||||
autocomplete="off">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<div class="d-flex flex-wrap gap-2">
|
||||
<button type="button" class="btn btn-outline-primary button-small" onclick="gefundeneGemeindenAuswählen()">
|
||||
Gefilterte Orte auswählen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary button-small" onclick="ausgewählteGemeindenLöschen()">
|
||||
Auswahl löschen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary button-small" onclick="alleGemeindenAnzeigen()">
|
||||
Alle anzeigen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary button-small" onclick="nurAusgewählteGemeindenAnzeigen()">
|
||||
Nur ausgewählte anzeigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="sitzauswahl" class="scrollWindow"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
// Parse CSV-Daten und fülle das Dropdown-Feld
|
||||
<div class="form-section mb-3">
|
||||
<div class="section-heading">
|
||||
<span class="section-number" aria-hidden="true">3</span>
|
||||
<div>
|
||||
<h2>Rechtsform auswählen</h2>
|
||||
<p>Wählen Sie mindestens eine Rechtsform für den Export.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2 mb-2">
|
||||
<div>
|
||||
<button type="button" class="btn btn-outline-primary button-small" onclick="alleRechtsformenAuswählen()">
|
||||
Alle auswählen
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-secondary button-small" onclick="rechtsformenLöschen()">
|
||||
Auswahl löschen
|
||||
</button>
|
||||
</div>
|
||||
<span class="selection-meta" id="legalFormCount">0 ausgewählt</span>
|
||||
</div>
|
||||
|
||||
let communityCsvData;
|
||||
let legalFormsCsvData;
|
||||
<?php
|
||||
include 'api/env_vars.php';
|
||||
include 'api/zefixAPI.php';
|
||||
include 'gemeinden.php';
|
||||
include 'legalForms.php';
|
||||
?>
|
||||
<div id="rechtsformenauswahl" class="scrollWindow"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-section mb-3">
|
||||
<div class="section-heading">
|
||||
<span class="section-number" aria-hidden="true">4</span>
|
||||
<div>
|
||||
<h2>Export anfordern</h2>
|
||||
<p>Geben Sie die Zieladresse an und starten Sie den geschützten Export.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
const legalFormslines = legalFormsCsvData.trim().split('\n');
|
||||
const legalFormsheaders = legalFormslines[0].split(',');
|
||||
const legalformIdIndex = legalFormsheaders.indexOf('id');
|
||||
const legalformNameIndex = legalFormsheaders.indexOf('name');
|
||||
for (let i = 1; i < legalFormslines.length; i++) {
|
||||
const cells = legalFormslines[i].split(',');
|
||||
const id = cells[legalformIdIndex].trim();
|
||||
const name = cells[legalformNameIndex].trim();
|
||||
const label = document.createElement('label');
|
||||
const rechtsformenauswahl = document.getElementById("rechtsformenauswahl")
|
||||
rechtsformenauswahl.appendChild(label);
|
||||
const input = document.createElement('input');
|
||||
input.type = "checkbox";
|
||||
input.name = "rechtsformen[]";
|
||||
input.value = id;
|
||||
label.textContent = " " + name;
|
||||
label.style.marginBottom = '0';
|
||||
label.prepend(input);
|
||||
rechtsformenauswahl.appendChild(document.createElement("br"))
|
||||
<div class="mb-4">
|
||||
<div class="form-label mb-2">Weitere Optionen</div>
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="exakteSuche" name="exakteSuche" disabled>
|
||||
<label class="form-check-label" for="exakteSuche">Exakte Suche (noch in Entwicklung)</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="geloeschteRechtseinheiten" name="geloeschteRechtseinheiten">
|
||||
<label class="form-check-label" for="geloeschteRechtseinheiten">Gelöschte Rechtseinheiten suchen</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="inBisherigenFirmen" name="inBisherigenFirmen" disabled>
|
||||
<label class="form-check-label" for="inBisherigenFirmen">In bisherigen Firmen/Namen suchen (noch in Entwicklung)</label>
|
||||
</div>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" id="phonetischeSuche" name="phonetischeSuche" disabled>
|
||||
<label class="form-check-label" for="phonetischeSuche">Phonetische Suche (noch in Entwicklung)</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label for="email" class="form-label">Ihre E-Mail-Adresse</label>
|
||||
<div class="form-text mb-2">
|
||||
Die Aufbereitung kann einige Zeit dauern. Sie erhalten den Download-Link per E-Mail, sobald die Daten bereit sind.
|
||||
</div>
|
||||
<input type="email" id="email" name="email" class="form-control" required placeholder="name@firma.ch">
|
||||
<div class="invalid-feedback">Bitte geben Sie eine gültige E-Mail-Adresse ein.</div>
|
||||
</div>
|
||||
|
||||
<?php if ($turnstileSiteKey !== ''): ?>
|
||||
<div class="privacy-note mb-3">
|
||||
<div class="cf-turnstile"
|
||||
data-sitekey="<?php echo htmlspecialchars($turnstileSiteKey, ENT_QUOTES); ?>"
|
||||
data-action="turnstile-spin-v2"
|
||||
data-theme="auto"
|
||||
data-language="de"
|
||||
data-feedback-enabled="false"></div>
|
||||
<div class="form-text mt-2">
|
||||
Der Missbrauchsschutz verarbeitet technisch notwendige Browser- und Verbindungsdaten.
|
||||
Details finden Sie in der <a href="datenschutz.php">Datenschutzerklärung</a>.
|
||||
</div>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="alert alert-warning" role="alert">
|
||||
Der Export ist vorübergehend deaktiviert, da der Missbrauchsschutz noch nicht konfiguriert ist.
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg"<?php echo $turnstileSiteKey === '' ? ' disabled' : ''; ?>>Export starten</button>
|
||||
</div>
|
||||
<div id="selectionValidationMessage" class="text-danger mt-2" style="display:none;">
|
||||
Wählen Sie mindestens einen Ort und eine Rechtsform aus.
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</form>
|
||||
|
||||
<script>
|
||||
// Bootstrap validation (optional)
|
||||
(function () {
|
||||
'use strict'
|
||||
const forms = document.querySelectorAll('.needs-validation')
|
||||
Array.prototype.slice.call(forms).forEach(function (form) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
const hasSelectedSitz = document.querySelector('input[name="sitze[]"]:checked') !== null
|
||||
const hasSelectedRechtsform = document.querySelector('input[name="rechtsformen[]"]:checked') !== null
|
||||
const selectionValidationMessage = document.getElementById('selectionValidationMessage')
|
||||
|
||||
if (selectionValidationMessage) {
|
||||
selectionValidationMessage.style.display = hasSelectedSitz && hasSelectedRechtsform ? 'none' : 'block'
|
||||
}
|
||||
|
||||
if (!form.checkValidity() || !hasSelectedSitz || !hasSelectedRechtsform) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
}
|
||||
form.classList.add('was-validated')
|
||||
}, false)
|
||||
})
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
// CSV data injected by PHP
|
||||
let communityCsvData;
|
||||
let legalFormsCsvData;
|
||||
<?php
|
||||
include 'gemeinden.php';
|
||||
include 'legalForms.php';
|
||||
?>
|
||||
|
||||
// --- Helpers to create nicer checkbox rows (Bootstrap form-check) ---
|
||||
function addCheckboxRow(container, checkboxName, checkboxValue, labelText) {
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = "form-check";
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = "form-check-input";
|
||||
input.type = "checkbox";
|
||||
input.name = checkboxName;
|
||||
input.value = checkboxValue;
|
||||
input.id = checkboxName.replace(/[^a-z0-9]/gi, '-') + '-' + String(checkboxValue).replace(/[^a-z0-9]/gi, '-');
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = "form-check-label";
|
||||
label.htmlFor = input.id;
|
||||
label.textContent = labelText;
|
||||
|
||||
wrapper.appendChild(input);
|
||||
wrapper.appendChild(label);
|
||||
container.appendChild(wrapper);
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
// --- Legal forms ---
|
||||
const legalFormslines = legalFormsCsvData.trim().split('\n');
|
||||
const legalFormsheaders = legalFormslines[0].split(',');
|
||||
const legalformIdIndex = legalFormsheaders.indexOf('id');
|
||||
const legalformNameIndex = legalFormsheaders.indexOf('name');
|
||||
|
||||
const rechtsformenauswahl = document.getElementById("rechtsformenauswahl");
|
||||
for (let i = 1; i < legalFormslines.length; i++) {
|
||||
const cells = legalFormslines[i].split(',');
|
||||
const id = (cells[legalformIdIndex] || '').trim();
|
||||
const name = (cells[legalformNameIndex] || '').trim();
|
||||
if (!id || !name) continue;
|
||||
addCheckboxRow(rechtsformenauswahl, "rechtsformen[]", id, " " + name);
|
||||
}
|
||||
|
||||
// --- Communities + Cantons ---
|
||||
const lines = communityCsvData.trim().split('\n');
|
||||
const headers = lines[0].split(',');
|
||||
const gemeindeNameIndex = headers.indexOf('Gemeindename');
|
||||
const bfsIdIndex = headers.indexOf('id');
|
||||
const kantonIndex = headers.indexOf('Kanton');
|
||||
const registryOfCommerceIdIndex = headers.indexOf('registryOfCommerceId');
|
||||
|
||||
const sitze = [];
|
||||
const kantone = [];
|
||||
const kantonauswahl = document.getElementById("kantonauswahl");
|
||||
const sitzauswahl = document.getElementById("sitzauswahl");
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const cells = lines[i].split(',');
|
||||
if (!cells || cells.length < 4) continue;
|
||||
|
||||
const bfsId = (cells[bfsIdIndex] || '').trim();
|
||||
const gemeindeName = (cells[gemeindeNameIndex] || '').trim();
|
||||
const kanton = (cells[kantonIndex] || '').trim();
|
||||
const registryOfCommerceId = (cells[registryOfCommerceIdIndex] || '').trim();
|
||||
|
||||
if (!bfsId || !gemeindeName) continue;
|
||||
|
||||
// Add canton if new
|
||||
if (kanton && !kantone.includes(kanton)) {
|
||||
kantone.push(kanton);
|
||||
addCheckboxRow(kantonauswahl, "kantone[]", kanton, " " + kanton);
|
||||
}
|
||||
|
||||
// Store row for filtering logic
|
||||
sitze.push(cells);
|
||||
|
||||
// Create nicer entry
|
||||
const gemeindeEintrag = document.createElement("div");
|
||||
gemeindeEintrag.classList.add("gemeindeeintrag");
|
||||
|
||||
const wrapper = document.createElement('div');
|
||||
wrapper.className = "form-check";
|
||||
|
||||
const input = document.createElement('input');
|
||||
input.className = "form-check-input";
|
||||
input.type = "checkbox";
|
||||
input.name = "sitze[]";
|
||||
input.value = bfsId;
|
||||
input.id = "sitz-" + bfsId;
|
||||
|
||||
const label = document.createElement('label');
|
||||
label.className = "form-check-label";
|
||||
label.htmlFor = input.id;
|
||||
label.textContent = " " + gemeindeName;
|
||||
|
||||
wrapper.appendChild(input);
|
||||
wrapper.appendChild(label);
|
||||
gemeindeEintrag.appendChild(wrapper);
|
||||
|
||||
sitzauswahl.appendChild(gemeindeEintrag);
|
||||
}
|
||||
|
||||
const sitzInput = document.getElementById('sitzInput');
|
||||
|
||||
function updateSelectionCounts() {
|
||||
const cantonTotal = document.querySelectorAll('input[name="kantone[]"]:checked').length;
|
||||
const seatTotal = document.querySelectorAll('input[name="sitze[]"]:checked').length;
|
||||
const legalFormTotal = document.querySelectorAll('input[name="rechtsformen[]"]:checked').length;
|
||||
|
||||
document.getElementById('cantonCount').textContent = cantonTotal + ' ausgewählt';
|
||||
document.getElementById('seatCount').textContent = seatTotal + ' ausgewählt';
|
||||
document.getElementById('legalFormCount').textContent = legalFormTotal + ' ausgewählt';
|
||||
}
|
||||
|
||||
document.addEventListener('change', function(event) {
|
||||
if (event.target && event.target.matches('input[type="checkbox"]')) {
|
||||
updateSelectionCounts();
|
||||
}
|
||||
});
|
||||
|
||||
function kantoneAusgewählt() {
|
||||
const checkboxes = document.querySelectorAll('input[name="kantone[]"]');
|
||||
let countChecked = 0;
|
||||
for (let i = 0; i < checkboxes.length; i++) {
|
||||
if (checkboxes[i].checked) countChecked++;
|
||||
}
|
||||
return countChecked;
|
||||
}
|
||||
|
||||
function filterFunction() {
|
||||
const numKantoneAusgewählt = kantoneAusgewählt();
|
||||
const filter = (sitzInput.value || "").toUpperCase();
|
||||
const entries = sitzauswahl.getElementsByClassName("gemeindeeintrag");
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const txtValue = entries[i].textContent || entries[i].innerText;
|
||||
|
||||
const selectedCheckbox = entries[i].querySelector('input[type="checkbox"]');
|
||||
const isSelected = selectedCheckbox ? selectedCheckbox.checked : false;
|
||||
|
||||
// map entry->sitz row index: still aligned because we push sitze once per entry
|
||||
const row = sitze[i];
|
||||
const rowKanton = row ? row[kantonIndex] : "";
|
||||
|
||||
let cantonOk = true;
|
||||
if (numKantoneAusgewählt > 0 && rowKanton) {
|
||||
const cantonBox = document.querySelector('input[name="kantone[]"][value="' + rowKanton + '"]');
|
||||
cantonOk = cantonBox ? cantonBox.checked : false;
|
||||
}
|
||||
|
||||
const matchesText = txtValue.toUpperCase().indexOf(filter) > -1;
|
||||
|
||||
|
||||
|
||||
const lines = communityCsvData.trim().split('\n');
|
||||
const headers = lines[0].split(',');
|
||||
const gemeindeNameIndex = headers.indexOf('Gemeindename');
|
||||
const bfsIdIndex = headers.indexOf('bfsId');
|
||||
const kantonIndex = headers.indexOf('Kanton');
|
||||
const registryOfCommerceIdIndex = headers.indexOf('registryOfCommerceId');
|
||||
|
||||
const sitze = [];
|
||||
const kantone = []
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
|
||||
const cells = lines[i].split(',');
|
||||
const bfsId = cells[bfsIdIndex].trim();
|
||||
const gemeindeName = cells[gemeindeNameIndex].trim();
|
||||
const kanton = cells[kantonIndex].trim();
|
||||
const registryOfCommerceId = cells[registryOfCommerceIdIndex].trim();
|
||||
|
||||
// Falls Kanton noch nicht vorhanden -> hinzufügen
|
||||
if (!kantone.includes(kanton)) {
|
||||
kantone.push(kanton)
|
||||
const label = document.createElement('label');
|
||||
const kantonsauswahl = document.getElementById("kantonauswahl")
|
||||
kantonsauswahl.appendChild(label);
|
||||
const input = document.createElement('input');
|
||||
input.type = "checkbox";
|
||||
input.name = "kantone[]";
|
||||
input.value = kanton;
|
||||
label.textContent = " " + kanton;
|
||||
label.style.marginBottom = '0';
|
||||
label.prepend(input);
|
||||
kantonsauswahl.appendChild(document.createElement("br"))
|
||||
}
|
||||
|
||||
// Sitz hinzufügen
|
||||
sitze.push(cells)
|
||||
const sitzString = gemeindeName
|
||||
const gemeindeEintrag = document.createElement("div");
|
||||
gemeindeEintrag.classList.add("gemeindeeintrag");
|
||||
const label = document.createElement('label');
|
||||
const sitzauswahl = document.getElementById("sitzauswahl")
|
||||
sitzauswahl.appendChild(gemeindeEintrag);
|
||||
gemeindeEintrag.appendChild(label);
|
||||
const input = document.createElement('input');
|
||||
input.type = "checkbox";
|
||||
input.name = "sitze[]";
|
||||
input.value = bfsId;
|
||||
label.textContent = " " + sitzString;
|
||||
label.style.marginBottom = '0';
|
||||
label.prepend(input);
|
||||
gemeindeEintrag.appendChild(document.createElement("br"))
|
||||
if ((matchesText && (cantonOk || numKantoneAusgewählt === 0)) || isSelected) {
|
||||
entries[i].style.display = "";
|
||||
} else {
|
||||
entries[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function filterFunction() {
|
||||
const numKantoneAusgewählt = kantoneAusgewählt()
|
||||
var input, filter, ul, li, a, i;
|
||||
input = document.getElementById("sitzInput");
|
||||
filter = input.value.toUpperCase();
|
||||
div = document.getElementById("gemeindeDropdown");
|
||||
a = div.getElementsByClassName("gemeindeeintrag");
|
||||
for (i = 0; i < a.length; i++) {
|
||||
txtValue = a[i].textContent || a[i].innerText;
|
||||
if ((txtValue.toUpperCase().indexOf(filter) > -1 && (document.querySelector('input[name="kantone[]"][value=' + sitze[i][kantonIndex] + ']').checked || numKantoneAusgewählt === 0) ) || a[i].firstElementChild.firstElementChild.checked) {
|
||||
a[i].style.display = "";
|
||||
} else {
|
||||
a[i].style.display = "none";
|
||||
}
|
||||
}
|
||||
}
|
||||
function alleGemeindenAnzeigen() {
|
||||
sitzInput.value = "";
|
||||
filterFunction();
|
||||
}
|
||||
|
||||
function kantoneAusgewählt() {
|
||||
const checkboxes = document.querySelectorAll('input[name="kantone[]"]');
|
||||
let countChecked = 0;
|
||||
function nurAusgewählteGemeindenAnzeigen() {
|
||||
sitzInput.value = "(nur ausgewählte)";
|
||||
filterFunction();
|
||||
}
|
||||
|
||||
for (var i = 0; i < checkboxes.length; i++) {
|
||||
if (checkboxes[i].checked) {
|
||||
countChecked = countChecked + 1;
|
||||
}
|
||||
}
|
||||
return countChecked;
|
||||
}
|
||||
function ausgewählteGemeindenLöschen() {
|
||||
sitzauswahl.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
filterFunction();
|
||||
updateSelectionCounts();
|
||||
}
|
||||
|
||||
function gefundeneGemeindenAuswählen() {
|
||||
sitzauswahl.querySelectorAll('input[type="checkbox"]').forEach(function(cb) {
|
||||
// show/hide is on the gemeindeeintrag wrapper
|
||||
const entry = cb.closest('.gemeindeeintrag');
|
||||
if (entry && entry.style.display !== "none") cb.checked = true;
|
||||
});
|
||||
filterFunction();
|
||||
updateSelectionCounts();
|
||||
}
|
||||
|
||||
function alleRechtsformenAuswählen() {
|
||||
document.querySelectorAll('input[name="rechtsformen[]"]').forEach(function(cb) {
|
||||
cb.checked = true;
|
||||
});
|
||||
updateSelectionCounts();
|
||||
}
|
||||
|
||||
const sitzInput = document.getElementById('sitzInput');
|
||||
const sitzauswahl = document.getElementById('sitzauswahl');
|
||||
const gemeindedropdown = document.getElementById('gemeindeDropdown');
|
||||
sitzauswahl.style.display = 'block';
|
||||
function rechtsformenLöschen() {
|
||||
document.querySelectorAll('input[name="rechtsformen[]"]').forEach(function(cb) {
|
||||
cb.checked = false;
|
||||
});
|
||||
updateSelectionCounts();
|
||||
}
|
||||
|
||||
gemeindedropdown.addEventListener('focusin', () => {
|
||||
console.log("focusin")
|
||||
sitzauswahl.style.display = 'block';
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
updateSelectionCounts();
|
||||
</script>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
<div class="push"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<footer class="text-center">
|
||||
<div class="container footer">
|
||||
<div class="row">
|
||||
<table>
|
||||
<tr>
|
||||
<td class="footer">
|
||||
<div class="col-12">
|
||||
<p><span><b>Impressum</b></span><br>
|
||||
Silias KLG<br>
|
||||
Toggenburgstrasse 31<br>
|
||||
8245 Feuerthalen<br>
|
||||
<a href="https://www.silias.ch" target="_blank">www.silias.ch</a><br>
|
||||
<a href="mailto:info@silias.ch">info@silias.ch</a><br>
|
||||
<a href="https://gitea.silias.ch/Silias-Public/Zefix_search">Projekt Repository</a>
|
||||
</div>
|
||||
</td>
|
||||
<td class="footer">
|
||||
<div class="col-12">
|
||||
<p><span><b>Datenquelle</b></span><br>
|
||||
Zentraler Firmenindex<br>
|
||||
Eidgenössisches Justiz- und Polizeidepartement (EJPD)<br>
|
||||
Bundesamt für Justiz (BJ)<br>
|
||||
Eidgenössisches Amt für das Handelsregister (EHRA)<br>
|
||||
Bundesrain 20<br>
|
||||
3003 Bern<br>
|
||||
<a href="https://www.zefix.admin.ch" target="_blank">www.zefix.admin.ch</a><br>
|
||||
<a href="tel:+41584624197">+41 (0) 58 462 41 97</a><br>
|
||||
<a href="zefix@bj.admin.ch">zefix@bj.admin.ch</a>
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer border-top">
|
||||
<div class="container py-4">
|
||||
<div class="row g-4">
|
||||
<div class="col-md-4">
|
||||
<h2 class="h6 mb-2">Impressum</h2>
|
||||
<div class="text-muted" style="font-size:.95rem;">
|
||||
Silias KLG<br>
|
||||
Toggenburgstrasse 31<br>
|
||||
8245 Feuerthalen<br>
|
||||
<a href="https://www.silias.ch" target="_blank" rel="noopener">www.silias.ch</a><br>
|
||||
<a href="mailto:info@silias.ch">info@silias.ch</a><br>
|
||||
<a href="https://gitea.silias.ch/Silias-Public/Zefix_search">Projekt-Repository</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2 class="h6 mb-2">Datenquelle</h2>
|
||||
<div class="text-muted" style="font-size:.95rem;">
|
||||
Zentraler Firmenindex<br>
|
||||
Eidgenössisches Justiz- und Polizeidepartement (EJPD)<br>
|
||||
Bundesamt für Justiz (BJ)<br>
|
||||
Eidgenössisches Amt für das Handelsregister (EHRA)<br>
|
||||
Bundesrain 20, 3003 Bern<br>
|
||||
<a href="https://www.zefix.admin.ch" target="_blank" rel="noopener">www.zefix.admin.ch</a><br>
|
||||
<a href="tel:+41584624197">+41 (0) 58 462 41 97</a><br>
|
||||
<a href="mailto:zefix@bj.admin.ch">zefix@bj.admin.ch</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-4">
|
||||
<h2 class="h6 mb-2">Datenschutz</h2>
|
||||
<div class="text-muted" style="font-size:.95rem;">
|
||||
Wir verwenden Ihre E-Mail-Adresse ausschliesslich zur Bereitstellung des angeforderten Exports.
|
||||
<br><a href="datenschutz.php">Datenschutzerklärung</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-muted mt-4" style="font-size:.9rem;">
|
||||
© <?php echo date('Y'); ?> Silias KLG
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
+11
-2
@@ -1,4 +1,13 @@
|
||||
|
||||
<?php
|
||||
echo "legalFormsCsvData = `". legalFormCSV($username, $password) ."`;";
|
||||
?>
|
||||
$legalFormsCsv = '';
|
||||
try {
|
||||
$legalFormsCsv = app_cached_string(
|
||||
'legal-forms',
|
||||
app_env_int('REFERENCE_CACHE_SECONDS', 86400, 300, 604800),
|
||||
static fn(): string => legalFormCSV((string)$username, (string)$password)
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Legal-form reference data could not be loaded: ' . $exception->getMessage());
|
||||
}
|
||||
echo 'legalFormsCsvData = ' . json_encode($legalFormsCsv, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) . ';';
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
require 'SimpleXLSX.php';
|
||||
require 'SimpleXLSXGen.php';
|
||||
|
||||
function read_xls_file($filename){
|
||||
$fileContent = [];
|
||||
if ($xlsx = SimpleXLSX::parse($filename)) {
|
||||
$fileContent = ($xlsx->rows());
|
||||
} else {
|
||||
echo SimpleXLSX::parseError();
|
||||
}
|
||||
return $fileContent;
|
||||
}
|
||||
|
||||
|
||||
|
||||
function write_xlsxFile($filename, $data) {
|
||||
$xlsx = SimpleXLSXGen::fromArray($data);
|
||||
$xlsx->saveAs($filename);
|
||||
}
|
||||
|
||||
function download_xlsxFile($data) {
|
||||
$xlsxArray = read_xls_file($_SERVER['DOCUMENT_ROOT'] ."/data/data.xlsx");
|
||||
foreach ($data as $row) {
|
||||
array_push($xlsxArray, $row);
|
||||
}
|
||||
$xlsx = SimpleXLSXGen::fromArray($xlsxArray);
|
||||
$xlsx->downloadAs("data.xlsx");
|
||||
}
|
||||
+344
@@ -0,0 +1,344 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
app_send_security_headers();
|
||||
header('Cache-Control: no-store');
|
||||
|
||||
function renderPage(string $title, string $messageHtml, bool $isError = false, int $httpStatus = 200): void
|
||||
{
|
||||
http_response_code($httpStatus);
|
||||
$logoPath = "img/silias-logo.png";
|
||||
|
||||
$statusClass = $isError
|
||||
? "alert-danger border-0 shadow-sm"
|
||||
: "alert-success border-0 shadow-sm";
|
||||
|
||||
$statusTitle = $isError ? "Fehler" : "Auftrag eingegangen";
|
||||
|
||||
echo '<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="css/bootstrap.css">
|
||||
<link rel="stylesheet" href="css/custom.css?v=2.2">
|
||||
<title>' . htmlspecialchars($title) . '</title>
|
||||
|
||||
<style>
|
||||
.brand-card { border-radius: 1rem; }
|
||||
|
||||
.brand-box {
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
|
||||
box-shadow: 0 10px 25px rgba(0,0,0,.04);
|
||||
}
|
||||
|
||||
.brand-eyebrow {
|
||||
font-size: .8rem;
|
||||
letter-spacing: .04em;
|
||||
text-transform: uppercase;
|
||||
color: #6c757d;
|
||||
margin-bottom: .25rem;
|
||||
max-width: 420px;
|
||||
}
|
||||
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body class="bg-light">
|
||||
|
||||
<nav class="navbar navbar-light site-header border-bottom py-2">
|
||||
<div class="container">
|
||||
<a class="brand-lockup" href="index.php" aria-label="Silias Zefix Export – Startseite">
|
||||
<span class="brand-logo-crop" aria-hidden="true"><img src="img/silias-logo.png" alt=""></span>
|
||||
<span class="brand-divider" aria-hidden="true"></span>
|
||||
<span class="brand-product">Zefix Export<small>Ein kostenloses Silias-Tool</small></span>
|
||||
</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<main class="container py-4 py-md-5">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12 col-md-10 col-lg-8 col-xl-7">
|
||||
|
||||
<div class="card shadow-sm brand-card">
|
||||
<div class="card-body p-4 p-md-5">
|
||||
|
||||
<h1 class="h4 mb-3 d-flex align-items-center gap-2">
|
||||
' . ($isError ? '<span class="text-danger">✖</span>' : '<span class="text-success">✔</span>') . '
|
||||
' . htmlspecialchars($statusTitle) . '
|
||||
</h1>
|
||||
|
||||
<div class="alert ' . $statusClass . ' mb-4">
|
||||
' . $messageHtml . '
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 d-sm-flex align-items-center">
|
||||
<a class="btn btn-primary px-4" href="index.php">Zurück zur Startseite</a>
|
||||
<a class="btn btn-link" href="https://www.silias.ch" target="_blank">Mehr über Silias →</a>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
|
||||
<div class="brand-box p-4">
|
||||
|
||||
<div class="d-flex align-items-start mb-3">
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<span class="brand-logo-crop d-block">
|
||||
<img src="' . htmlspecialchars($logoPath) . '" alt="Silias">
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="ms-4">
|
||||
<div class="brand-eyebrow">
|
||||
Ihr Partner für moderne IT-Infrastruktur & Automatisierung
|
||||
</div>
|
||||
<div class="fw-semibold fs-5">Silias KLG</div>
|
||||
<div class="text-muted small">
|
||||
IT, Automatisierung & individuelle Web-Lösungen
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="text-muted" style="font-size:.95rem;">
|
||||
Wir bauen schlanke Tools wie dieses, automatisieren Prozesse und integrieren Systeme sauber in bestehende Umgebungen
|
||||
(Hosting, Mail, ERP/CRM, Schnittstellen).<br><br>
|
||||
|
||||
Als Managed Service Provider betreuen wir KMU ganzheitlich – von der Infrastruktur über Cloud,
|
||||
Netzwerke und Telefonie bis zum laufenden Betrieb, Monitoring und persönlichen Support.
|
||||
</div>
|
||||
|
||||
<div class="mt-3 d-flex flex-wrap gap-2">
|
||||
<a class="btn btn-primary btn-sm" href="https://www.silias.ch" target="_blank">Website besuchen</a>
|
||||
<a class="btn btn-outline-primary btn-sm" href="mailto:info@silias.ch">Kontakt</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer class="border-top bg-white">
|
||||
<div class="container py-3 text-center text-muted small">
|
||||
© ' . date('Y') . ' Silias KLG · <a href="datenschutz.php">Datenschutzerklärung</a>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>';
|
||||
}
|
||||
|
||||
/* ---------------- REQUEST AND ABUSE PROTECTION ---------------- */
|
||||
|
||||
if (($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') {
|
||||
header('Allow: POST');
|
||||
renderPage('Methode nicht erlaubt', 'Bitte starten Sie den Export über das Formular auf der Startseite.', true, 405);
|
||||
exit;
|
||||
}
|
||||
|
||||
// Bots commonly fill fields that are deliberately hidden from real visitors.
|
||||
$honeypot = isset($_POST['website']) && is_scalar($_POST['website']) ? trim((string)$_POST['website']) : '';
|
||||
if ($honeypot !== '') {
|
||||
renderPage(
|
||||
'Auftrag eingegangen',
|
||||
'Wir haben Ihren Auftrag erhalten. Sie erhalten von uns eine E-Mail, sobald die Daten zum Download bereit sind.',
|
||||
false
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!app_turnstile_is_configured()) {
|
||||
renderPage('Export nicht verfügbar', 'Der Missbrauchsschutz ist noch nicht vollständig konfiguriert.', true, 503);
|
||||
exit;
|
||||
}
|
||||
|
||||
$clientIp = app_client_ip();
|
||||
try {
|
||||
$ipLimit = app_rate_limit(
|
||||
'submit-ip',
|
||||
$clientIp,
|
||||
app_env_int('RATE_LIMIT_IP_MAX', 3, 1, 100),
|
||||
app_env_int('RATE_LIMIT_IP_WINDOW_SECONDS', 900, 60, 86400)
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Rate limit failure: ' . $exception->getMessage());
|
||||
renderPage('Export nicht verfügbar', 'Der Export kann momentan nicht sicher verarbeitet werden. Bitte versuchen Sie es später erneut.', true, 503);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$ipLimit['allowed']) {
|
||||
header('Retry-After: ' . $ipLimit['retry_after']);
|
||||
renderPage('Zu viele Anfragen', 'Von dieser Verbindung wurden zu viele Aufträge gesendet. Bitte versuchen Sie es später erneut.', true, 429);
|
||||
exit;
|
||||
}
|
||||
|
||||
$turnstileToken = isset($_POST['cf-turnstile-response']) && is_scalar($_POST['cf-turnstile-response'])
|
||||
? (string)$_POST['cf-turnstile-response']
|
||||
: '';
|
||||
$turnstileResult = app_verify_turnstile($turnstileToken, $clientIp);
|
||||
if (!$turnstileResult['success']) {
|
||||
renderPage('Bot-Prüfung fehlgeschlagen', 'Die Sicherheitsprüfung konnte nicht bestätigt werden. Bitte laden Sie die Startseite neu und versuchen Sie es erneut.', true, 403);
|
||||
exit;
|
||||
}
|
||||
|
||||
$formStarted = isset($_POST['form_started']) && is_scalar($_POST['form_started']) ? (string)$_POST['form_started'] : '';
|
||||
if (!app_validate_form_started_token($formStarted)) {
|
||||
renderPage('Überprüfung fehlgeschlagen', 'Das Formular ist abgelaufen oder wurde zu schnell übermittelt. Bitte laden Sie die Startseite neu.', true, 400);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ---------------- INPUT VALIDATION ---------------- */
|
||||
|
||||
$emailValue = $_POST['email'] ?? '';
|
||||
$email = is_scalar($emailValue) ? strtolower(trim((string)$emailValue)) : '';
|
||||
if ($email === '' || strlen($email) > 254 || !filter_var($email, FILTER_VALIDATE_EMAIL)) {
|
||||
renderPage('Ungültige E-Mail', 'Bitte geben Sie eine gültige E-Mail-Adresse an.', true, 400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$companyValue = $_POST['firma'] ?? '';
|
||||
$companyName = is_scalar($companyValue) ? trim((string)$companyValue) : '';
|
||||
$companyNameLength = function_exists('mb_strlen') ? mb_strlen($companyName) : strlen($companyName);
|
||||
if ($companyNameLength > 200 || preg_match('//u', $companyName) !== 1 || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/u', $companyName)) {
|
||||
renderPage('Ungültiger Firmenname', 'Der Firmenname enthält ungültige Zeichen oder ist zu lang.', true, 400);
|
||||
exit;
|
||||
}
|
||||
|
||||
$maximumSeats = app_env_int('MAX_SEATS_PER_JOB', 500, 1, 5000);
|
||||
$maximumLegalForms = app_env_int('MAX_LEGAL_FORMS_PER_JOB', 50, 1, 200);
|
||||
$seats = app_normalize_positive_id_list($_POST['sitze'] ?? null, $maximumSeats);
|
||||
$legalForms = app_normalize_positive_id_list($_POST['rechtsformen'] ?? null, $maximumLegalForms);
|
||||
if ($seats === null || $legalForms === null) {
|
||||
renderPage('Ungültige Auswahl', 'Bitte wählen Sie eine zulässige Anzahl Orte und Rechtsformen aus.', true, 400);
|
||||
exit;
|
||||
}
|
||||
|
||||
sort($seats, SORT_NUMERIC);
|
||||
sort($legalForms, SORT_NUMERIC);
|
||||
$requestCount = count($seats) * count($legalForms);
|
||||
$maximumRequests = app_env_int('MAX_REQUESTS_PER_JOB', 5000, 1, 50000);
|
||||
if ($requestCount > $maximumRequests) {
|
||||
renderPage(
|
||||
'Auswahl zu gross',
|
||||
'Diese Auswahl würde ' . number_format($requestCount, 0, ',', "'") . ' einzelne Abfragen erzeugen. Erlaubt sind maximal ' . number_format($maximumRequests, 0, ',', "'") . '. Bitte schränken Sie Orte oder Rechtsformen ein.',
|
||||
true,
|
||||
400
|
||||
);
|
||||
exit;
|
||||
}
|
||||
|
||||
try {
|
||||
$emailLimit = app_rate_limit(
|
||||
'submit-email',
|
||||
$email,
|
||||
app_env_int('RATE_LIMIT_EMAIL_MAX', 5, 1, 100),
|
||||
app_env_int('RATE_LIMIT_EMAIL_WINDOW_SECONDS', 86400, 300, 604800)
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Rate limit failure: ' . $exception->getMessage());
|
||||
renderPage('Export nicht verfügbar', 'Der Export kann momentan nicht sicher verarbeitet werden. Bitte versuchen Sie es später erneut.', true, 503);
|
||||
exit;
|
||||
}
|
||||
|
||||
if (!$emailLimit['allowed']) {
|
||||
header('Retry-After: ' . $emailLimit['retry_after']);
|
||||
renderPage('Zu viele Anfragen', 'Für diese E-Mail-Adresse wurden heute bereits mehrere Exporte angefordert.', true, 429);
|
||||
exit;
|
||||
}
|
||||
|
||||
/* ---------------- TASK BUILD AND ATOMIC QUEUE WRITE ---------------- */
|
||||
|
||||
$baseRequest = [
|
||||
'maxEntries' => 50,
|
||||
'offset' => 0,
|
||||
'languageKey' => 'de',
|
||||
'deletedFirms' => isset($_POST['geloeschteRechtseinheiten']),
|
||||
'searchType' => 'exact',
|
||||
];
|
||||
if ($companyName !== '') {
|
||||
$baseRequest['name'] = $companyName;
|
||||
}
|
||||
|
||||
$requestsToDo = [];
|
||||
foreach ($legalForms as $legalForm) {
|
||||
foreach ($seats as $seat) {
|
||||
$request = $baseRequest;
|
||||
$request['legalForms'] = [$legalForm];
|
||||
$request['legalSeats'] = [$seat];
|
||||
$requestsToDo[] = $request;
|
||||
}
|
||||
}
|
||||
|
||||
$fingerprint = hash('sha256', json_encode([
|
||||
'email' => $email,
|
||||
'company' => $companyName,
|
||||
'deleted' => $baseRequest['deletedFirms'],
|
||||
'seats' => $seats,
|
||||
'legalForms' => $legalForms,
|
||||
], JSON_THROW_ON_ERROR));
|
||||
|
||||
try {
|
||||
$taskDirectory = app_task_dir();
|
||||
$queueLock = fopen(app_private_dir() . DIRECTORY_SEPARATOR . 'queue.lock', 'c+');
|
||||
if ($queueLock === false || !flock($queueLock, LOCK_EX)) {
|
||||
throw new RuntimeException('Queue lock could not be acquired.');
|
||||
}
|
||||
|
||||
try {
|
||||
$taskFiles = glob($taskDirectory . DIRECTORY_SEPARATOR . '*.json') ?: [];
|
||||
$maximumQueueSize = app_env_int('MAX_QUEUE_SIZE', 20, 1, 1000);
|
||||
if (count($taskFiles) >= $maximumQueueSize) {
|
||||
renderPage('Warteschlange voll', 'Momentan werden bereits viele Exporte verarbeitet. Bitte versuchen Sie es später erneut.', true, 503);
|
||||
exit;
|
||||
}
|
||||
|
||||
foreach ($taskFiles as $existingTaskFile) {
|
||||
$existing = json_decode((string)file_get_contents($existingTaskFile), true);
|
||||
if (is_array($existing) && hash_equals((string)($existing['fingerprint'] ?? ''), $fingerprint)) {
|
||||
renderPage(
|
||||
'Auftrag bereits vorhanden',
|
||||
'Ein identischer Auftrag wartet bereits auf die Verarbeitung. Sie erhalten den Download-Link per E-Mail.',
|
||||
false
|
||||
);
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
$taskData = [
|
||||
'version' => 2,
|
||||
'createdAt' => time(),
|
||||
'requests' => $requestsToDo,
|
||||
'email' => $email,
|
||||
'fingerprint' => $fingerprint,
|
||||
'downloadToken' => bin2hex(random_bytes(16)),
|
||||
];
|
||||
$taskFilename = $taskDirectory . DIRECTORY_SEPARATOR . time() . '-' . bin2hex(random_bytes(16)) . '.json';
|
||||
$temporaryFilename = $taskFilename . '.tmp';
|
||||
$written = file_put_contents($temporaryFilename, json_encode($taskData, JSON_THROW_ON_ERROR), LOCK_EX);
|
||||
if ($written === false || !rename($temporaryFilename, $taskFilename)) {
|
||||
@unlink($temporaryFilename);
|
||||
throw new RuntimeException('Task file could not be written.');
|
||||
}
|
||||
@chmod($taskFilename, 0600);
|
||||
} finally {
|
||||
flock($queueLock, LOCK_UN);
|
||||
fclose($queueLock);
|
||||
}
|
||||
|
||||
renderPage(
|
||||
'Auftrag eingegangen',
|
||||
'Wir haben Ihren Auftrag erhalten. Sie erhalten von uns eine E-Mail, sobald die Daten zum Download bereit sind. Dies kann je nach Datenmenge länger dauern.',
|
||||
false
|
||||
);
|
||||
} catch (Throwable $exception) {
|
||||
error_log('Task creation failed: ' . $exception->getMessage());
|
||||
renderPage('Fehler', 'Der Auftrag konnte momentan nicht gespeichert werden. Bitte versuchen Sie es später erneut.', true, 503);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
http_response_code(404);
|
||||
exit;
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/app.php';
|
||||
require_once __DIR__ . '/zefixAPI.php';
|
||||
require_once __DIR__ . '/emailSender.php';
|
||||
|
||||
$maxExecutionTime = app_env_int('TASK_EXECUTOR_MAX_SECONDS', 120, 30, 900);
|
||||
$minimumTaskAge = app_env_int('TASK_MIN_AGE_SECONDS', 10, 0, 300);
|
||||
$latestEndTime = time() + $maxExecutionTime;
|
||||
$smtpPassword = app_env('smtppassword');
|
||||
|
||||
app_migrate_legacy_tasks();
|
||||
$downloadRetention = app_env_int('DOWNLOAD_RETENTION_HOURS', 48, 1, 720) * 3600;
|
||||
$staleTaskRetention = app_env_int('STALE_TASK_RETENTION_HOURS', 168, 24, 2160) * 3600;
|
||||
app_cleanup_directory(app_download_dir(), $downloadRetention, 'csv');
|
||||
app_cleanup_directory(app_task_dir(), $staleTaskRetention, 'json');
|
||||
app_cleanup_directory(__DIR__ . DIRECTORY_SEPARATOR . 'download', $downloadRetention, 'csv');
|
||||
|
||||
$executorLock = fopen(app_private_dir() . DIRECTORY_SEPARATOR . 'executor.lock', 'c+');
|
||||
if ($executorLock === false || !flock($executorLock, LOCK_EX | LOCK_NB)) {
|
||||
fwrite(STDOUT, "Another executor is already running.\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int|false|null Next offset, false when complete, null on a temporary error.
|
||||
*/
|
||||
function doRequest(array $data, string $filename, string $username, string $password): int|false|null
|
||||
{
|
||||
$response = sendAPICompanySearchRequest($username, $password, $data);
|
||||
if (!is_string($response) || $response === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$responseObject = json_decode($response, true);
|
||||
if (!is_array($responseObject) || array_key_exists('error', $responseObject) || !is_array($responseObject['list'] ?? null)) {
|
||||
error_log('ZEFIX search returned an invalid or error response.');
|
||||
return null;
|
||||
}
|
||||
|
||||
$companyData = [];
|
||||
foreach ($responseObject['list'] as $company) {
|
||||
if (!is_array($company) || !isset($company['uid'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$detailsResponse = sendAPICompanyInfoRequest($username, $password, (string)$company['uid']);
|
||||
$details = is_string($detailsResponse) ? json_decode($detailsResponse, true) : null;
|
||||
$companyFullData = is_array($details) && is_array($details[0] ?? null) ? $details[0] : null;
|
||||
if ($companyFullData === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$companyData[] = [
|
||||
$companyFullData['name'] ?? '',
|
||||
$companyFullData['address']['careOf'] ?? '',
|
||||
$companyFullData['address']['street'] ?? '',
|
||||
$companyFullData['address']['houseNumber'] ?? '',
|
||||
$companyFullData['address']['swissZipCode'] ?? '',
|
||||
$companyFullData['address']['city'] ?? '',
|
||||
$companyFullData['uid'] ?? '',
|
||||
$companyFullData['legalSeat'] ?? '',
|
||||
$companyFullData['legalForm']['name']['de'] ?? '',
|
||||
$companyFullData['status'] ?? '',
|
||||
$companyFullData['sogcDate'] ?? '',
|
||||
$companyFullData['deletionDate'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
$isNewFile = !is_file($filename);
|
||||
$file = fopen($filename, $isNewFile ? 'x' : 'a');
|
||||
if ($file === false) {
|
||||
error_log('Export file could not be opened.');
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
if ($isNewFile) {
|
||||
fputcsv($file, ['name', 'careOf', 'street', 'houseNumber', 'swissZipCode', 'city', 'uid', 'legalSeat', 'legalForm', 'status', 'sogcDate', 'deletionDate'], ',', '"', '\\');
|
||||
@chmod($filename, 0600);
|
||||
}
|
||||
foreach ($companyData as $row) {
|
||||
fputcsv($file, $row, ',', '"', '\\');
|
||||
}
|
||||
} finally {
|
||||
fclose($file);
|
||||
}
|
||||
|
||||
return ($responseObject['hasMoreResults'] ?? false)
|
||||
? (int)($responseObject['maxOffset'] ?? 0)
|
||||
: false;
|
||||
}
|
||||
|
||||
function saveTask(string $filename, array $task): bool
|
||||
{
|
||||
$temporary = $filename . '.tmp';
|
||||
$written = file_put_contents($temporary, json_encode($task, JSON_THROW_ON_ERROR), LOCK_EX);
|
||||
if ($written === false || !rename($temporary, $filename)) {
|
||||
@unlink($temporary);
|
||||
return false;
|
||||
}
|
||||
@chmod($filename, 0600);
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
while ($latestEndTime - time() > 30) {
|
||||
$taskFiles = glob(app_task_dir() . DIRECTORY_SEPARATOR . '*.json') ?: [];
|
||||
sort($taskFiles, SORT_STRING);
|
||||
|
||||
if ($taskFiles === []) {
|
||||
fwrite(STDOUT, "Nothing to do.\n");
|
||||
break;
|
||||
}
|
||||
|
||||
$taskFilename = $taskFiles[0];
|
||||
$modified = filemtime($taskFilename);
|
||||
if ($modified !== false && $modified + $minimumTaskAge > time()) {
|
||||
sleep(min(5, max(1, ($modified + $minimumTaskAge) - time())));
|
||||
continue;
|
||||
}
|
||||
|
||||
$task = json_decode((string)file_get_contents($taskFilename), true);
|
||||
if (!is_array($task) || !is_array($task['requests'] ?? null) || !filter_var($task['email'] ?? '', FILTER_VALIDATE_EMAIL)) {
|
||||
error_log('Invalid task quarantined: ' . basename($taskFilename));
|
||||
@rename($taskFilename, $taskFilename . '.invalid');
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-f0-9]{32}$/', (string)($task['downloadToken'] ?? ''))) {
|
||||
$task['downloadToken'] = bin2hex(random_bytes(16));
|
||||
|
||||
// Preserve partially generated exports from the previous public-download layout.
|
||||
$legacyCsv = __DIR__ . DIRECTORY_SEPARATOR . 'download' . DIRECTORY_SEPARATOR . pathinfo($taskFilename, PATHINFO_FILENAME) . '.csv';
|
||||
if (is_file($legacyCsv)) {
|
||||
@rename($legacyCsv, app_download_path($task['downloadToken']));
|
||||
}
|
||||
if (!saveTask($taskFilename, $task)) {
|
||||
error_log('Could not migrate legacy task metadata.');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$csvFile = app_download_path((string)$task['downloadToken']);
|
||||
if ($task['requests'] !== []) {
|
||||
$nextOffset = doRequest($task['requests'][0], $csvFile, (string)$username, (string)$password);
|
||||
if ($nextOffset === null) {
|
||||
fwrite(STDOUT, "Temporary API error; task retained for retry.\n");
|
||||
break;
|
||||
}
|
||||
|
||||
if ($nextOffset !== false) {
|
||||
$task['requests'][0]['offset'] = $nextOffset;
|
||||
} else {
|
||||
array_shift($task['requests']);
|
||||
}
|
||||
|
||||
if (!saveTask($taskFilename, $task)) {
|
||||
error_log('Could not save task progress.');
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!is_file($csvFile)) {
|
||||
$emptyFile = fopen($csvFile, 'x');
|
||||
if ($emptyFile !== false) {
|
||||
fputcsv($emptyFile, ['name', 'careOf', 'street', 'houseNumber', 'swissZipCode', 'city', 'uid', 'legalSeat', 'legalForm', 'status', 'sogcDate', 'deletionDate'], ',', '"', '\\');
|
||||
fclose($emptyFile);
|
||||
@chmod($csvFile, 0600);
|
||||
}
|
||||
}
|
||||
|
||||
if (sendEmail((string)$task['email'], (string)$task['downloadToken'], $smtpPassword)) {
|
||||
unlink($taskFilename);
|
||||
fwrite(STDOUT, 'Completed ' . basename($taskFilename) . ".\n");
|
||||
} else {
|
||||
fwrite(STDOUT, "Email delivery failed; task retained for retry.\n");
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
flock($executorLock, LOCK_UN);
|
||||
fclose($executorLock);
|
||||
}
|
||||
+221
@@ -0,0 +1,221 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/app.php';
|
||||
$username = app_env('username');
|
||||
$password = app_env('password');
|
||||
|
||||
function sendAPICompanyInfoRequest(string $username, string $password, string $uid): string|bool {
|
||||
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixPublicREST/api/v1/company/uid/'.$uid;
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
error_log('ZEFIX company info request failed: ' . curl_error($ch));
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $
|
||||
* @param string $
|
||||
* @param array $data
|
||||
* @return bool|string
|
||||
*/
|
||||
function sendAPICompanySearchRequest(string $username, string $password, array $data): string|bool {
|
||||
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixREST/api/v1/firm/search.json';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
error_log('ZEFIX company search request failed: ' . curl_error($ch));
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
function sendAPICommunityRequest(string $username, string $password): string|bool
|
||||
{
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixREST/api/v1/community.json';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
error_log('ZEFIX community request failed: ' . curl_error($ch));
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
function communityCSV(string $username, string $password): string {
|
||||
if ($username === '' || $password === '') {
|
||||
error_log('ZEFIX credentials are not configured.');
|
||||
return '';
|
||||
}
|
||||
|
||||
$response = sendAPICommunityRequest($username, $password);
|
||||
if (!is_string($response)) {
|
||||
return '';
|
||||
}
|
||||
$communityArray = json_decode($response, true);
|
||||
|
||||
if (!is_array($communityArray)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$csvOutput = "id,bfsId,Kanton,Gemeindename,registryOfCommerceId,replacedById\n";
|
||||
|
||||
foreach ($communityArray as $item) {
|
||||
if (!is_array($item)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$row = [
|
||||
$item['id'] ?? '',
|
||||
$item['bfsId'] ?? '',
|
||||
$item['canton'] ?? '',
|
||||
$item['name'] ?? '',
|
||||
$item['registryOfficeId'] ?? '',
|
||||
$item['replacedById'] ?? ''
|
||||
];
|
||||
|
||||
$csvOutput .= implode(',', $row) . "\n";
|
||||
}
|
||||
|
||||
return $csvOutput;
|
||||
}
|
||||
|
||||
function sendAPILegalFormRequest(string $username, string $password): string|bool
|
||||
{
|
||||
// API endpoint
|
||||
$apiUrl = 'https://www.zefix.admin.ch/ZefixREST/api/v1/legalForm.json';
|
||||
|
||||
// Headers
|
||||
$headers = array(
|
||||
"accept: application/json",
|
||||
"Content-Type: application/json"
|
||||
);
|
||||
|
||||
// Initialize cURL session
|
||||
$ch = curl_init();
|
||||
|
||||
// Set cURL options
|
||||
curl_setopt($ch, CURLOPT_USERPWD, $username . ":" . $password);
|
||||
curl_setopt($ch, CURLOPT_URL, $apiUrl);
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_POST, false);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
|
||||
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
|
||||
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
|
||||
// Execute cURL session and get the response
|
||||
$response = curl_exec($ch);
|
||||
// Check for cURL errors
|
||||
if (curl_errno($ch)) {
|
||||
error_log('ZEFIX legal form request failed: ' . curl_error($ch));
|
||||
}
|
||||
// Close cURL session
|
||||
curl_close($ch);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
function legalFormCSV(string $username, string $password): string {
|
||||
if ($username === '' || $password === '') {
|
||||
error_log('ZEFIX credentials are not configured.');
|
||||
return '';
|
||||
}
|
||||
|
||||
$response = sendAPILegalFormRequest($username, $password);
|
||||
if (!is_string($response)) {
|
||||
return '';
|
||||
}
|
||||
$legalformArray = json_decode($response, true);
|
||||
if (!is_array($legalformArray)) {
|
||||
return '';
|
||||
}
|
||||
$csvOutput = "id,name\n";
|
||||
// Create CSV rows
|
||||
|
||||
foreach ($legalformArray as $item) {
|
||||
if (!is_array($item) || !isset($item['name']) || !is_array($item['name'])) {
|
||||
return '';
|
||||
}
|
||||
$csvOutput .= $item["id"].",".$item["name"]["de"] . "\n";
|
||||
}
|
||||
return $csvOutput;
|
||||
}
|
||||
|
||||
?>
|
||||
Reference in New Issue
Block a user